Mybatis基础实战
MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口、POJO对象为数据库中的记录。并支持XML 方式配置,以及硬编码方式配置;不过现在随着微服务的盛行,SpringBoot的流行,从XML向注解方式配置实现流行起来了。
它同其他类型ORM类似,目的都是将开发人员从编写复杂、重复的Java 连接数据库重复的工作中解放处理,促使我们提供效率关注业务接下来会逐步介绍相关特性以及功能点。
特性:
- 使用连接池对连接进行管理;
- SQL和代码分离,集中管理;
- 自定义结果集映射
- 参数映射和动态 SQL;
- 重复 SQL 的提取;
- 缓存管理;
- 插件机制。
# 1. 简单入门示例
其实很多时候使用都是集成Spring框架来使用,那么它是怎么于Spring集成并能工作的,这些并不会很多人知晓。下面就是使用原生方式使用,需要加载配置文件mybatis-config.xml
@Test
public void TestExample() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 加载配置获取工厂
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 获取会话对象
SqlSession session = sqlSessionFactory.openSession();
try {
BlogMapper mapper = session.getMapper(BlogMapper.class);
List<Blog> list = mapper.selectListById(1L);
System.out.println(list);
} finally {
session.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2. 创建SqlSession流程
从上述的示例中,我们可以发现Mybatis有几个核心的对象:SqlSessionFactoryBuilder
、SqlSessionFactory
、SqlSession
、Mapper
,这几个对象在Mybatis至关重要,在工作中集成Spring的时候,这些对象都被Spring管理了,一直用我们感觉不到它的存在,其实Mybatis能够运行工作都与4大对象密切相关。
SqlSessionFactoryBuilder 它是用来构建
SqlSessionFactory
,而SqlSessionFactory
只需要一个,所以只要构建了这一个SqlSessionFactory
,它的使命就完成了,也就没有存在的意义了,所以它的生命周期只存在于方法的局部。SqlSessionFactory
SqlSessionFactory
是用来创建SqlSession
的,每次应用程序访问数据库,都需要创建一个会话。因为每次交互操作都需要会话,因此SqlSessionFactory
存在于应用的整个生命周期中。创建SqlSession
只需要一个实例来做这件事就行了,否则会产生很多的混乱和浪费资源。所以要采用单例模式。SqlSession
SqlSession
表示是一个会话,因为它不是线程安全的,不能在线程间共享。所以我们在请求开始的时候创建一个SqlSession
对象,在请求结束或者说方法执行完毕的时候要及时关闭它(一次请求或者操作中)。Mapper
Mapper
(实际上是一个代理对象)是从SqlSession
中获取的。它的作用是发送 SQL 来操作数据库的数据。它应该在一个SqlSession
事务方法之内。它是基于MapperProxy
类实现代理策略的。
# 3. 配置文件解析
Mybatis启动时都需要加载所有的配置解析封装为 Configuration,后续的工作都需要它,下面简单解析主要的配置,详细可参考官方http://www.mybatis.org/mybatis-3/zh/configuration.html (opens new window)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 外部属性配置,在这里定义的数据源配置,当然可以直接定义属性,不用加载外部文件-->
<!-- 可以用resource引用应用里面的相对路径,也可以用url指定本地服务器或者网络的绝对路径-->
<properties resource="db.properties"></properties>
<settings>
<!-- 指定MyBatis所用日志的具体实现,未指定时将自动查找。 -->
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!-- 控制全局缓存(二级缓存)-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,只针对当前执行的这一statement有效,每次只需清理本地缓存 -->
<!--<setting name="localCacheScope" value="STATEMENT"/>-->
<!-- SESSION级别的缓存,默认值-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<!-- 定义Mapper接口别名,这里定义好了,在具体的Mapper的XML文件中就不用使用完全限定名了,可以使用别名-->
<typeAliases>
<typeAlias alias="blog" type="com.bdr.Blog"/>
</typeAliases>
<!-- <typeHandlers>
<typeHandler handler="com.bdr.type.JSONTypeHandler"></typeHandler>
</typeHandlers>
-->
<!-- 对象工厂 -->
<!-- <objectFactory type="com.bdr.objectfactory.GPObjectFactory">
<property name="name" value="Rick"/>
</objectFactory>
-->
<!-- 定义插件,可以通过插件来实现SQL拦截,可以拦截Executor、ParameterHandler、ResultSetHandler、StatementHandler 4大对象 -->
<plugins>
<plugin interceptor="com.bdr.interceptor.MyPageInterceptor">
</plugin>
</plugins>
<!-- 环境配置,可以配置多个环境配置,default是指定使用哪个环境默认配置-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/><!-- 单独使用时配置成MANAGED没有事务 -->
<!-- 数据源配置 -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- Mapper对应XML引入,它有多中使用方式,具体可以参考官方 -->
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
</configuration>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 4. 动态SQL
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用JDBC或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
可参考官方文档:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html (opens new window)
- 批量操作
在MyBatis里面是支持批量的操作的,包括批量的插入、更新、删除。我们可以直接传入一个 List、Set、Map 或者数组,配合动态 SQL 的标签,MyBatis 会自动帮我们生成语法正确的SQL语句。
注意
动态SQL批量操作数据量特别大的时候,拼接出来的 SQL 语句过大,MySQL 的服务端对于接收的数据包有大小限制,max_allowed_packet 默认是4M,需要修改默认配置才可以解决这个问题;
Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (7188967 > 4194304). You can change this value on the server by setting the max_allowed_packet' variable.
- 插入
在Mapper文件里面,使用foreach标签拼接values部分的语句;
!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true">
<!-- 指定表ID字段,当插入后并将ID赋值给对象 -->
<selectKey resultType="long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into tbl_emp (emp_id, emp_name, gender,email, d_id)
values
<foreach collection="list" item="emps" index="index" separator=",">
( #{emps.empId},#{emps.empName},#{emps.gender},#{emps.email},#{emps.dId} )
</foreach>
</insert>
2
3
4
5
6
7
8
9
10
11
12
- 更新
批量更新可以使用 case when 实现。
update tbl_emp set
emp_name =
case emp_id
when ? then ?
when ? then ?
when ? then ? end ,
gender =
case emp_id
when ? then ?
when ? then ?
when ? then ? end ,
email =
case emp_id
when ? then ?
when ? then ?
when ? then ? end
where emp_id in ( ? , ? , ? )
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<update id="updateBatch">
update tbl_emp set
emp_name =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
when #{emps.empId} then #{emps.empName}
</foreach>
,gender =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
when #{emps.empId} then #{emps.gender}
</foreach>
,email =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id" close="end">
when #{emps.empId} then #{emps.email}
</foreach>
where emp_id in
<foreach collection="list" item="emps" index="index" separator="," open="(" close=")">
#{emps.empId}
</foreach>
</update>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 删除
<delete id="deleteBatch">
delete from blog where bid in
<foreach collection="list" item="item" index="index" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
2
3
4
5
6
当然动态 SQL 也可通通过注解,硬编码的方式实现,无需 XML 定义;可查看注解API,都有对应替代实现。
@SelectProvider(type = SQLProvider.class, method = "buildEmployeeList")
List<Employee> getEmployeeList(Employee emp);
class SQLProvider {
public String buildEmployeeList(Employee emp) {
return new SQL() {{
SELECT("emp_id, emp_name, gender,email, d_id");
FROM("tbl_emp");
WHERE("emp_id = #{empId}");
}}.toString();
}
}
@Select("SELECT emp_id, emp_name, gender,email, d_id FROM tbl_emp WHERE emp_id = #{empId}")
List<Employee> getEmployeeList(Employee emp);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5. 嵌套(关联)查询/N+1/延迟加载
在查询业务数据的时候经常会遇到跨表关联查询的情况,比如查询文章则关联文章评论(一对一
),查询文章关联的评论用户(一对多
)。
结果映射有两个标签, 分别是 resultType、resultMap。resultType是 select 标签的一个属性,适用于返回 JDK 类型(比如 Integer、String等等)和实体类,这种情况下结果集的列和实体类的属性可以直接映射。如果返回的字段无法直接映射,就要用 resultMap 来建立映射关系。
对于关联查询的这种情况,通常不能用 resultType 来映射,需要使用 resultMap 映射,要么就是修改 dto(Data Transfer Object),在里面增加字段,这个会导致增加很多无关的字段。要么就是引用关联的对象,比如Blog里面包含了一个 Author对象,这种情况下就要用到关联查询(association,或者嵌套查询),MyBatis 可以帮我们自动做结果的映射。
一对一的关联查询
- 嵌套结果
<!-- 根据文章查询作者,一对一查询的结果,嵌套查询 -->
<resultMap id="BlogWithAuthorResultMap" type="com.urick.mybatis.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<!-- 联合查询,将author的属性映射到ResultMap -->
<association property="author" javaType="com.urick.mybatis.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
2
3
4
5
6
7
8
9
10
- 嵌套结果
<!-- 另一种联合查询 (一对一)的实现,但是这种方式有“N+1”的问题 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.urick.mybatis.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.urick.mybatis.Author" column="author_id" select="selectAuthor"/>
<!-- selectAuthor 定义在下面-->
</resultMap>
<!-- 嵌套查询 -->
<select id="selectAuthor" parameterType="int" resultType="com.urick.mybatis.Author">
select author_id authorId, author_name authorName from author where author_id = #{authorId}
</select>
2
3
4
5
6
7
8
9
10
11
其中第2种方式:嵌套查询,由于是分两次查询,当我们查询了文章信息之后,会再发送一条SQL到数据库查询作者信息。我们只执行了一次查询文章信息的SQL(所谓的1),如果返回了N条记录,就会再发送N条到数据库查询作者信息(所谓的N),这个就是我们所说的N+1的问题。这样会白白地浪费我们的应用和数据库的性能。
如果我们用了嵌套查询的方式,怎么解决这个问题?能不能等到使用文章信息的时候再去查询?这个就是我们所说的延迟加载,或者叫懒加载。在MyBatis里面可以通过开启延迟加载的开关来解决这个问题。
!--延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过 select标签的fetchType 来覆盖-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认 JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />
2
3
4
5
6
# 6. 主键生成策略
public interface KeyGenerator {
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
2
3
4
基于自增主键返回:支持批量返回插入ID
<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="Student">
**SelectKeyGenerator **
<insert id="insertStudent" parameterType="Student" >
<!-- order="BEFORE"表示insert前执行,order="AFTER"表示insert之后执行 -->
<selectKey keyProperty="studId" resultType="int" order="BEFORE">
SELECT ELEARNING.STUD_ID_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO
STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
VALUES(#{studId}, #{name},
#{email}, #{dob}, #{phone})
</insert>
2
3
4
5
6
7
8
9
10
由于selectKey本身返回单个序列主键值,也就无法支持批量insert操作并返回主键id列表了。如果要执行批量insert,请选择使用for循环执行多次插入操作。
Mybatis批量插入,返回主键id列表为null
<insert id="insertStudents" useGeneratedKeys="true" keyProperty="studId" parameterType="java.util.ArrayList">
INSERT INTO
STUDENTS(STUD_ID, NAME, EMAIL, DOB, PHONE)
VALUES
<foreach collection="list" item="item" index="index" separator=",">
(#{item.studId},#{item.name},#{item.email},#{item.dob}, #{item.phone})
</foreach>
</insert>
2
3
4
5
6
7
8
批量插入,主键返回为null问题解决 (opens new window)
# 7. 综述
由于Mybatis非常成熟,在市面上也能找到许多实战教程,也可以参考官方入门文档,资源非常丰富,本文不做过多阐述。