MyBatis 是是一个ORM 框架,ORM(Object Relational Mapping),即对象关系映射。底层实现是基于 JDBC 的,但是 MyBatis 隐藏了 JDBC 的复杂性,提供了简单易用的 API,将 SQL 语句和 Java 代码分离,让开发者能够通过 XML 或注解来描述 SQL 语句,并把结果映射到 Java 对象上。
简单来说 MyBatis 是更简单完成程序和数据库交互的工具,它可以帮助我们更方便、更快速的操作数据库。
想要真正使用 MyBatis 操作数据库,我们首先要做足前期的准备工作,这其中就包括导入依赖、添加配置等操作。
一般来说需要添加如下两个依赖,分别是MyBatis框架、数据库驱动。
<!-- 添加 MyBatis 框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-starter.version}</version>
</dependency>
<!-- 添加 MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
添加了 MyBatis 之后,为什么还需要添加 MySQL 驱动呢?
MyBatis 就像⼀个平台(类似京东),而数据库相当于商家有很多种,不止有 MySQL,还有 SQL Server、DB2
等等…因此这两个都是需要添加的。
这点和JDBC一样,添加连接配置就是为了让 MyBatis 连接到本机的数据库,下面是详细的配置信息:
# 数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/myblog?characterEncoding=utf8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
Tips:spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 是配置Mysql数据库的驱动。如果使用 mysql-connector-java 是 5.x 之前的使用的是“com.mysql.jdbc.Driver”,如果是大于 5.x 使用的是“com.mysql.cj.jdbc.Driver”。
MyBatis 是一个ORM 框架,能够通过 XML 或注解来描述 SQL 语句。所以一般在使用MyBatis实际操作数据库的时候,主要做两件事:
- 创建
Mapper 接口
,并声明操作数据库的方法。- 编辑
XML 文件
。XML 文件对应着 Mapper 接口中方法的具体实现。
因此,配置 MyBatis 中的 XML 路径是为了告知 MyBatis 框架 SQL 映射文件所在的位置。下面是详细的配置信息:
# 配置 mybatis xml 的⽂件路径和命名格式
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
注: 这条配置信息告知 MyBatis 框架从 “mapper” 目录开始,递归地加载所有以 “Mapper.xml” 结尾的文件作为 SQL 映射文件。这样,当调用Mapper中的接口时,就能够正确加载这些文件以执行相关的数据库操作。
创建 Mapper 接口
@Mapper
public interface UserMapper {
}
当我们使用 @Mapper 注解标记一个接口时,MyBatis 会在运行时动态地生成一个实现类,这个实现类会根据相应的 XML 文件中的配置来实现接口中定义的方法。这样,我们就可以直接调用接口中的方法来执行对应的 SQL 语句。
初始化 XML 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--这里的namespace表明当前xml是实现那个接口的-->
<mapper namespace="com.example.demo.dao.UserMapper">
</mapper>
说明:
- 在创建XML文件时,
路径
和命名格式
一定要和上面的配置相匹配。- 下面XML中的格式化信息,前两行可认为是固定格式,重点了解 mapper 标签。mapper 中的
namespace
属性标明当前的 XML 文件是实现了哪一个接口中的方法的,因此这里的值是对应mapper接口的全限定名,即全包名.类名
。
方法的具体实现是在 xml 文件中 mapper 标签下的,具体实现步骤如下:
- 我们首先在 mapper 标签下添加具体的操作标签。比如查询就是
标签,修改是
……
- 完善操作标签中的属性。主要需要完善两个属性,
id
对应实现的方法名;resultType
对应方法的返回类型,这里同样写全限定名。标签里面编写具体的 SQL 语句。
PS: 如果是 添加
、修改
、删除
默认返回的是影响的行数,在 mapper.xml 中是可以不设置返回的类型的。
方法声明:
// 插入用户信息
int insert(UserInfo userInfo);
方法实现:
<insert id="insert" parameterType="com.lee.demo.model.User">
insert into userinfo(username,password,photo)
values(#{username},#{password},#{photo})
</insert>
注: 在 MyBatis 中当我们传递的参数是一个对象时,我们想要拿到里面的属性值,直接写 属性名
即可,并不需要类.属性名
.
由于添加、修改、删除 默认返回的是 受影响的行数。如果想要返回 自增 id
,我们可以对实现部分稍作修改:
<insert id="insert" useGeneratedKeys="true" keyColumn="id" keyProperty="id" parameterType="com.lee.demo.model.User">
insert into userinfo(username,password,photo)
values(#{username},#{password},#{photo})
</insert>
parameterType
属性:parameterType 用于指定映射语句(Mapper)的输入参数类型,可以是任何Java类的完全限定名。在MyBatis中,parameterType是可以省略的。当parameterType没有指定时,MyBatis会根据传入的参数自动推断其类型。useGeneratedKeys
属性:指定是否使用自动生成的主键。如果设置为 true,则表示插入数据后将获取数据库生成的主键值。keyColumn
属性:指定用于存储自动生成的主键值的表列名。keyProperty
属性:指定用于存储自动生成的主键值的 Java 对象属性名。
注: 做出了上述修改后并不是说,方法返回值变成了自增 id,而是传入的 UserInfo 对象中的 id 属性值被设置成了自增 id 值。
方法声明:
// 根据用户 id 删除指定用户信息
int delById(@Param("id") Integer id);
方法实现:
<delete id="delById" parameterType="java.lang.Integer">
delete from userinfo where id=#{id}
</delete>
@Param
注解的主要作用就是为传入 Mapper 方法的参数进行命名,方便在 XML 配置文件中引用。如果传入的参数是一个业务对象,通常情况下不需要使用 @Param 注解来明确命名的。
方法声明:
// 根据用户 id 修改用户名
int update(@Param("id") Integer id ,@Param("username") String username);
方法实现:
<update id="update" parameterType="java.lang.Integer">
update userinfo set username=#{username} where id=#{id}
</update>
- 创建实体类是为了将数据库表的记录映射到对象上,并提供方便的数据访问和操作。
- 当MyBatis将数据库查询结果映射到实体类时,它会查找实体类中与数据库列对应的属性,并尝试调用该属性的
setter
方法来设置值。如果实体类没有提供 setter 方法,MyBatis将无法将查询结果正确地赋值给实体类的属性。
@Data
public class UserInfo {
private int id;
private String username;
private String password;
private String photo;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int state;
}
方法声明(接口):
方法的声明是放到 mapper 接口中的,在接口中只需要添加一条对应操作的方法声明即可,具体实现放到 xml 中。
@Mapper
public interface UserMapper {
// 查询 userinfo 表中的所有信息
List<UserInfo> getAllUserInfo();
}
方法实现(xml):
<mapper namespace="com.example.demo.dao.UserMapper">
<select id="getAllUserInfo" resultType="com.example.demo.model.UserInfo">
select * from userinfo;
</select>
</mapper>
方法声明(接口):
// 根据登录信息,查询 userinfo
UserInfo getByLogin(@Param("username") String username,@Param("password") String password);
方法实现(xml):
方式一: 使用${}
接收参数
<select id="getByLogin" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username='${username}' and password='${password}'
</select>
方式二: 使用#{}
接收参数(推荐)
<select id="getByLogin" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=#{username} and password=#{password}
</select>
当然,从功能上来说,以上两种方式都可以成功接收参数,并查询结果,但是二者有着本质上的区别。
(1)${}
和 #{}
的区别:
#{}
:预编译处理:MyBatis 在处理 #{} 时,会将 SQL 中的 #{} 替换为?号,使用 PreparedStatement 的 set 方法来赋值。
${}
:直接替换:是MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值。由于 #{} 是预编译处理,会自动进行参数类型转换和安全处理,一般来说是安全的。而 ${} 是直接替换,存在着 SQL 注入的安全性问题。
下面我们演示一下使用 ${} 占位符导致的 SQL 注入
问题:
通过上述结果我们可以看到,当我们使用 ${} 进行占位时,执行时会将参数的值和 ${} 直接替换,如果此时传入一个特殊的数据:' or 1 = '1
,会导致SQL查询语句改变原意或执行额外的恶意操作。
(2)${} 存在的意义:
既然使用 ${} 能实现的功能 #{} 也能实现,并且 ${} 还有 SQL 注入的风险,那么 ${} 存在的意义是什么呢?其实这句话的说法过于绝对,存在即合理,${} 有其独特的应用场景。例如最常见的 排序查询
:
同样是上述查询所有的 userinfo 信息,如果我们想要拿到排序的结果,那么在 xml 中,我们只能使用 ${} 实现,而不能使用 #{} 实现:
<select id="getAllUserInfo" resultType="com.example.demo.model.UserInfo">
select * from userinfo order by id ${sort}
</select>
原因: 因为当使用 #{sort} 查询时,只能传入 asc、desc。如果传递的值为 String 则会加单引号,就会导致 sql 错误,因为 sql 命令不能加引号。这点也是利用了 ${}
“直接替换”的特性。
虽然必要的时候可以使用 ${} ,但是使用不规范还是存在安全问题,下面就归纳了两点使用 ${} 的注意事项:
${}
适用场景:当业务需要传递sql 命令
时,只能使用 ${} 不能使用 #{}。${}
注意事项:如果使用 ${} ,那么传递的参数一定要能够穷举,否则存在 sql 注入风险。
方法声明:
// 模糊查询
List<UserInfo> fuzzyQuery(@Param("str") String str);
这里 like 后从参数有三种书写方式:
- 方式1:
like #{str}
,这种方式需要在传递参数时,传递 %。- 方式2:
like "%${str}%"
,可以实现功能,但是存在 sql 注入风险。- 方式3:
like concat('%',#{str},'%')
,对字符串进行拼接,最优选择。
方法实现:
<select id="fuzzyQuery" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like concat('%',#{str},'%')
</select>
下面我们以查询 articleinfo 表中的信息为例,要求查询结果包含作者名。
创建一个用于接受多表联查结果的实体类:
@Data
public class ArticleInfo {
private int id;
private String title;
private String content;
private LocalDateTime createtime;
private LocalDateTime updatetime;
private int rcount;
private int state;
// 联表字段
private String username;
}
声明 Mapper 方法,并使用注解实现操作 SQL:
@Mapper
public interface ArticleMapper {
@Select("select articleinfo.*,userinfo.username from userinfo,articleinfo where articleinfo.uid=userinfo.id")
List<ArticleInfo> getAllArticle();
}
上面我们说到,如果是 添加
、修改
、删除
默认返回的是影响的行数,在 mapper.xml 中是可以不设置返回的类型的。但是对于 查询标签来说即便是最简单的查询 返回类型
也是不可以省略的。
对于 返回结果主要有两种映射属性
resultMap
和 resultType
,对于 resultType 来说,它的最大优点就是使用方便,直接定义到某个实体类即可。而 resultMap 相对来说就比较麻烦了,下面就详细介绍一下 resultMap 两个高频应用场景:
- 字段名称和程序中的属性名不同的情况,可使用 resultMap 配置映射。
- 一对一和一对多关系可以使用 resultMap 映射并查询数据。
(1)字段名和属性名不一致情况处理
这里的不一致,指的是数据库表中的字段名和类中的属性名不一致的情况。我们知道,MyBatis 框架提供了自动映射功能,如果字段名和属性名一致,能够自动将查询结果映射到对象中的对应属性。
但是开发中可能有些场景下需要使用不同于字段名的属性名,例如上面的实体类中,我们将文章实体类(ArticleInfo)中的连表字段修改为 author,我们又该如何处理呢?下面给出两种解决方案:
方案一:使用 sql 语句中的 as 进行字段名重命名,让字段名等于属性名。
<select id="getAllArticle" resultType="com.example.demo.model.ArticleInfo">
select articleinfo.*,userinfo.username as author
from userinfo,articleinfo
where articleinfo.uid=userinfo.id
</select>
方案二:定义一个 resultMap,将属性名和字段名进行手动映射。
<!-- 定义一个 resultMap -->
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
<id column="id" property="id"></id>
<result column="title" property="title"></result>
<result column="content" property="content"></result>
<result column="createtime" property="createtime"></result>
<result column="updatetime" property="updatetime"></result>
<result column="rcount" property="rcount"></result>
<result column="state" property="state"></result>
<!-- 联表字段 -->
<result column="username" property="author"></result>
</resultMap>
<select id="getAllArticle" resultMap="BaseMap">
select articleinfo.*,userinfo.username
from userinfo,articleinfo
where articleinfo.uid=userinfo.id
</select>
说明:
:定义了一个名为 “BaseMap” 的 resultMap,这个 resultMap 用于将查询结果映射到 ArticleInfo 对象上。
:定义了一个主键映射,将数据库中名为 “id” 的列映射到 ArticleInfo 对象的 “id” 属性上。
:定义一个普通属性映射,将数据库中名为 “username” 的列映射到 ArticleInfo 对象的 “author” 属性上。
resultMap
属性指定了结果映射的名称,这里是 “BaseMap”。
(2)一对一的联表查询结果
在多表查询时,在一个类中包含了另外一个或多个对象,如果使用 resultType 标签,是查询不出来被包含的对象
的(会置为 null)。
那么遇到这种场景具体该怎么处理呢?答案就是在 resultMap 中使用
或
标签 。
例如为了查询到文章表以及文章表管理的用户表、板块表信息,我新建了一个文章实体类 ArticleExt,其中包含了一些文章相关的属性,同时也包含关联的用户对象属性和板块对象属性。现在我进行联表查询,要求将查询出来的结果赋值到当前定义的文章实体类中。
文章实体类:
@Data
public class ArticleExt {
private Long id;
private Long boarId;
private Long userId;
private String title;
// ...
// 拓展所属作者字段
private User user;
// 拓展所属板块字段
private Board board;
}
定义查询语句:
<select id="selectAll" resultMap="AllInfoResultMap">
select u.id as u_id,
u.avatarUrl as u_avatarUrl,
u.nickname as u_nickname,
u.gender as u_gender,
u.isAdmin as u_isAdmin,
u.state as u_state,
u.deleteState as u_deleteState,
b.id as b_id,
b.name as b_name,
b.state as b_state,
b.deleteState as b_deletState,
a.id,
a.boarId,
a.userId,
a.title,
from t_article as a,t_user as u,t_board as b
where
a.userId = u.id
and
a.boarId = b.id
</select>
AllInfoResultMap 具体实现:
- 分别在这三个表对应的 xml 文件下创建三个 resultMap ,分别表示 文章表映射、用户表映射、板块表映射。
- 在文章表对应的 xml 文件下再定义一个 resultMap 用来表示 ArticleExt 的结果映射。
- 在 步骤 2 定义的 resultMap 中使用
标签关联对应映射。
<!-- 自定义结果集映射 -->
<resultMap id="AllInfoResultMap" type="com.lee.forum.model.ArticleExt" extends="com.lee.forum.dao.ArticleMapper.BaseResultMap">
<!-- 关联的用户的映射 -->
<association property="user" resultMap="com.lee.forum.dao.UserMapper.BaseResultMap" columnPrefix="u_"/>
<!-- 关联的板块的映射 -->
<association property="board" resultMap="com.lee.forum.dao.BoardMapper.BaseResultMap" columnPrefix="b_"/>
</resultMap>
说明:
- resultMap中的
extends
属性:用于指定当前resultMap继承自其他 resultMap。- association中的
property
属性:指定 Article 中对应的属性名。- association中的
resultMap
属性:指定关联的结果集映射,将基于该映射配置来组织关联数据。- association中的
columnPrefix
属性:绑定一对一对象时,是通过 columnPrefix+association.resultMap.column 来映射结果集字段。association.resultMap.column 对应的结果集映射中,column字段。
注意: columnPrefix 属性用于区分链表中相同的字段名,通常不能省略,如果省略当联表中如果有相同的字段,那么就会导致查询出错。