一、什么是 MyBatis?
MyBatis 是是一个ORM 框架,ORM(Object Relational Mapping),即对象关系映射。底层实现是基于 JDBC 的,但是 MyBatis 隐藏了 JDBC 的复杂性,提供了简单易用的 API,将 SQL 语句和 Java 代码分离,让开发者能够通过 XML 或注解来描述 SQL 语句,并把结果映射到 Java 对象上。
简单来说 MyBatis 是更简单完成程序和数据库交互的工具,它可以帮助我们更方便、更快速的操作数据库。
二、MyBatis 前期准备工作
想要真正使用 MyBatis 操作数据库,我们首先要做足前期的准备工作,这其中就包括导入依赖、添加配置等操作。
1、导入依赖
一般来说需要添加如下两个依赖,分别是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
等等…因此这两个都是需要添加的。
2、添加数据库连接配置
这点和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”。
3、配置 MyBatis 中的 XML
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中的接口时,就能够正确加载这些文件以执行相关的数据库操作。
4、创建 Mapper 接口、初始化 xml 文件
创建 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 标签下添加具体的操作标签。比如查询就是
<select></select>
标签,修改是<update></update>
……- 完善操作标签中的属性。主要需要完善两个属性,
id
对应实现的方法名;resultType
对应方法的返回类型,这里同样写全限定名。<select></select>
标签里面编写具体的 SQL 语句。
三、MyBatis 增删改
PS: 如果是 添加
、修改
、删除
默认返回的是影响的行数,在 mapper.xml 中是可以不设置返回的类型的。
1、插入用户信息
方法声明:
// 插入用户信息
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 值。
2、删除指定用户信息
方法声明:
// 根据用户 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 注解来明确命名的。
3、修改指定用户信息
方法声明:
// 根据用户 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 查询
1、创建实体类,并实现 setter 方法
- 创建实体类是为了将数据库表的记录映射到对象上,并提供方便的数据访问和操作。
- 当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;
}
2、查询 userinfo 表中的所有信息
方法声明(接口):
方法的声明是放到 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>
3、根据登录信息,查询 userinfo
方法声明(接口):
// 根据登录信息,查询 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>
当然,从功能上来说,以上两种方式都可以成功接收参数,并查询结果,但是二者有着本质上的区别。
4、${}、#{} 与 SQL 注入问题
(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 注入风险。
5、like 模糊查询
方法声明:
// 模糊查询
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>
6、多表联查
下面我们以查询 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();
}
7、返回类型 resultMap
上面我们说到,如果是 添加
、修改
、删除
默认返回的是影响的行数,在 mapper.xml 中是可以不设置返回的类型的。但是对于 查询标签来说即便是最简单的查询 返回类型
也是不可以省略的。
对于 <select></select>
返回结果主要有两种映射属性 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>
说明:
<resultMap id="BaseMap" type="com.example.demo.model.ArticleInfo">
:定义了一个名为 “BaseMap” 的 resultMap,这个 resultMap 用于将查询结果映射到 ArticleInfo 对象上。<id column="id" property="id"></id>
:定义了一个主键映射,将数据库中名为 “id” 的列映射到 ArticleInfo 对象的 “id” 属性上。<result column="username" property="author"></result>
:定义一个普通属性映射,将数据库中名为 “username” 的列映射到 ArticleInfo 对象的 “author” 属性上。resultMap
属性指定了结果映射的名称,这里是 “BaseMap”。
(2)一对一的联表查询结果
在多表查询时,在一个类中包含了另外一个或多个对象,如果使用 resultType 标签,是查询不出来被包含的对象
的(会置为 null)。
那么遇到这种场景具体该怎么处理呢?答案就是在 resultMap 中使用 <association/>
或 <association></association>
标签 。
例如为了查询到文章表以及文章表管理的用户表、板块表信息,我新建了一个文章实体类 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 中使用
<association/>
标签关联对应映射。
<!-- 自定义结果集映射 -->
<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 属性用于区分链表中相同的字段名,通常不能省略,如果省略当联表中如果有相同的字段,那么就会导致查询出错。