1. 前言
Mybatis
的基础知识以及相关用法因为在工作工作过程中天天都在使用,本来不打算写的。但在看 Mybatis
的源码过程中发现很多地方都对 XML
在解析,还是觉得有必要记录一下 Mybatis
的基本概念。
2. Mybatis
基本概念
Mybatis
: 官网地址
Mybatis
是一款优秀的 持久层框架/半自动 的 ORM
框架,半自动的原因是因为 移植性不行,例如 Oracle
转 MySQL
就会存在大量关键字不可用。
优点
与 JDBC
相比,减少了50%的代码量。(加载驱动、获取数据库链接、设置参数和获取结果集等)
上手简单,学习成本很低。
实现了代码与 SQL
的解耦(提供XML标签,支持编写动态SQL
)
缺点
3. Mybatis
快速入门
总体来说上手成本还是挺低的,比较简单。后面我们一般会集成到 Spring
中,不会像如下这样操作。
目录结构如下
具体步骤如下
导入依赖
创建数据表
创建实体对象
创建Mapper接口
编写配置文件
编写测试类
3.1. 导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 <dependencies > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis</artifactId > <version > 3.5.4</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.47</version > </dependency > </dependencies >
3.2. 创建数据表
1 2 3 4 5 6 7 8 9 10 CREATE TABLE `emp` ( `id` int (11 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) COLLATE utf8mb4_unicode_ci DEFAULT NULL , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 4 DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci; INSERT INTO emp ( id, username )VALUES ( 1 , 'Tom' ), ( 2 , 'Jack' ), ( 3 , 'Tony' );
3.3. 创建实体对象
1 2 3 4 5 6 7 public class Employee { private Integer id; private String username; }
3.4. 创建Mapper接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface EmployeeMapper { List<Employee> listByEmployee () ; Employee selectEmployee (Integer id) ; Integer insertEmployee (Employee emp) ; Integer updateEmployee (Employee emp) ; Integer deleteEmployee (Integer id) ; }
3.5. 编写配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <configuration > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://localhost:3306/mybatis" /> <property name ="username" value ="root" /> <property name ="password" value ="root" /> </dataSource > </environment > </environments > <mappers > <mapper resource ="EmployeeMapper.xml" /> </mappers > </configuration >
1 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 <?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" > <mapper namespace ="com.wickson.mapper.EmployeeMapper" > <select id ="selectEmployee" resultType ="com.wickson.entity.Employee" > select * from emp where id = #{id} </select > <select id ="listByEmployee" resultType ="com.wickson.entity.Employee" > SELECT * FROM emp </select > <insert id ="insertEmployee" > INSERT INTO emp ( `username`) VALUES (#{username}); </insert > <update id ="updateEmployee" > UPDATE emp SET username = #{username} WHERE id = #{id} </update > <delete id ="deleteEmployee" > DELETE FROM emp WHERE id = #{id} </delete > </mapper >
3.6. 编写测试类
1 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 public class EmployeeMapperTest { SqlSessionFactory sqlSessionFactory; @Before public void init () { String resource = "mybatis-config.xml" ; InputStream inputStream = null ; try { inputStream = Resources.getResourceAsStream(resource); } catch (IOException e) { e.printStackTrace(); } sqlSessionFactory = new SqlSessionFactoryBuilder ().build(inputStream); } @After public void listByEmployee () { try (SqlSession session = sqlSessionFactory.openSession()) { List<Employee> employees = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee" ); employees.forEach(System.out::println); } } @Test public void selectEmployee () { try (SqlSession session = sqlSessionFactory.openSession()) { EmployeeMapper mapper = session.getMapper(EmployeeMapper.class); Employee employee = mapper.selectEmployee(1 ); System.out.println(employee); } } @Test public void insertEmployee () { try (SqlSession session = sqlSessionFactory.openSession()) { EmployeeMapper mapper = session.getMapper(EmployeeMapper.class); Employee employee = new Employee (); employee.setUsername("Jerry" ); Integer rows = mapper.insertEmployee(employee); session.commit(); System.out.println("Affected rows = " + rows); } } @Test public void updateEmployee () { try (SqlSession session = sqlSessionFactory.openSession()) { EmployeeMapper mapper = session.getMapper(EmployeeMapper.class); Employee employee = new Employee (); employee.setId(5 ); employee.setUsername("Jerry1" ); Integer rows = mapper.updateEmployee(employee); session.commit(); System.out.println("Affected rows = " + rows); } } @Test public void deleteEmployee () { try (SqlSession session = sqlSessionFactory.openSession()) { EmployeeMapper mapper = session.getMapper(EmployeeMapper.class); Integer rows = mapper.deleteEmployee(5 ); session.commit(); System.out.println("Affected rows = " + rows); } } }
4. Mybatis
配置文件
Mybatis
的配置文件分为两大类,第一个是 全局配置文件,第二个是 SQL
映射文件。
这部分的文件在源码分析时全部都会进行加载
4.1. 全局配置文件
如下是 Mybatis
的全局配置文件信息,后面的源码信息会加载如下配置信息
1 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 <?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 > <properties resource ="db.properties" > </properties > <settings > <setting name ="mapUnderscoreToCamelCase" value ="true" /> </settings > <typeAliases > <package name ="com.wickson.bean" /> </typeAliases > <environments default ="development" > <environment id ="development" > <transactionManager type ="JDBC" /> <dataSource type ="POOLED" > <property name ="driver" value ="${driverClassname}" /> <property name ="url" value ="${url}" /> <property name ="username" value ="${username}" /> <property name ="password" value ="${password}" /> </dataSource > </environment > </environments > <databaseIdProvider type ="DB_VENDOR" > <property name ="MySQL" value ="mysql" /> <property name ="SQL Server" value ="sqlserver" /> <property name ="Oracle" value ="orcl" /> </databaseIdProvider > <mappers > <mapper resource ="EmployeeMapper.xml" /> <package name ="com.wickson.mapper" /> </mappers > </configuration >
4.2. SQL
映射文件SQL
映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
cache
– 该命名空间的缓存配置。
cache-ref
– 引用其它命名空间的缓存配置。
resultMap
– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
sql
– 可被其它语句引用的可重用语句块。
insert
– 映射插入语句。
update
– 映射更新语句。
delete
– 映射删除语句。
select
– 映射查询语句。
1 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 <?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" > <mapper namespace ="cn.tulingxueyuan.dao.EmployeeMapper" > <cache type ="org.mybatis.caches.ehcache.EhcacheCache" /> <cache-ref namespace ="com.wickson.mapper.DepartmentMapper" /> <resultMap id ="employeeResultMap" type ="com.wickson.entity.Employee" > <id property ="id" column ="emp_id" /> <result property ="empName" column ="emp_name" /> <result property ="email" column ="emp_email" /> </resultMap > <sql id ="baseColumnList" > emp_id, emp_last_name, emp_email </sql > <insert id ="insertEmployee" useGeneratedKeys ="true" keyProperty ="id" > INSERT INTO employees(${baseColumnList}) VALUES(#{id}, #{lastName}, #{email}) </insert > <update id ="updateEmployee" parameterType ="com.wickson.entity.Employee" > UPDATE employees SET emp_last_name = #{lastName}, emp_email = #{email} WHERE emp_id = #{id} </update > <delete id ="deleteEmployee" parameterType ="int" > DELETE FROM employees WHERE emp_id = #{id} </delete > <select id ="getEmployeeById" resultMap ="employeeResultMap" parameterType ="int" > SELECT ${baseColumnList} FROM employees WHERE emp_id = #{id} </select > </mapper >
5. Mybatis
动态 SQL
5.1. if
根据条件生成 SQL
片段
1 2 3 4 5 6 <select id ="findActiveBlogWithTitleLike" resultType ="Blog" > SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <if test ="title != null" > AND title like #{title} </if > </select >
5.2. choose / when / otherwise
类似于Java中的switch语句,根据条件选择不同的分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="findActiveBlogLike" resultType ="Blog" > SELECT * FROM BLOG WHERE state = ‘ACTIVE’ <choose > <when test ="title != null" > AND title like #{title} </when > <when test ="author != null and author.name != null" > AND author_name like #{author.name} </when > <otherwise > AND featured = 1 </otherwise > </choose > </select >
5.3. foreach
用于循环遍历集合,生成对应的 SQL
片段
如果传入的是单参数且参数类型是一个 List
的时候,collection
属性值为 list
如果传入的是单参数且参数类型是一个array数组的时候,collection
的属性值为 array
如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在 MyBatis
里面也是会把它封装成一个 Map
的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id ="selectPostIn" resultType ="Blog" > SELECT * FROM BLOG <where > <foreach item ="item" index ="index" collection ="list" open ="ID in (" separator ="," close =")" nullable ="true" > #{item} </foreach > </where > </select >
5.4. set
可以用在动态更新的时候
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <update id ="updateAuthorIfNecessary" > update Author <set > <if test ="username != null" > username=#{username}, </if > <if test ="password != null" > password=#{password}, </if > <if test ="email != null" > email=#{email}, </if > <if test ="bio != null" > bio=#{bio} </if > </set > where id=#{id} </update >
5.5. where
可以用在所有的查询条件都是动态的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="findActiveBlogLike" resultType ="Blog" > SELECT * FROM BLOG <where > <if test ="state != null" > state = #{state} </if > <if test ="title != null" > AND title like #{title} </if > <if test ="author != null and author.name != null" > AND author_name like #{author.name} </if > </where > </select >
5.6. bind
用于将表达式绑定为一个变量,以便在后续的 SQL
语句中使用。
1 2 3 4 <if test ="param.filter != null and param.filter!=''" > <bind name ="filterLike" value ="'%' + param.filter + '%'" /> and ( name like #{filterLike,jdbcType=VARCHAR} ) </if >
6. Mybatis
缓存
Mybatis
缓存一般分为两种:
一级缓存:线程级别的缓存,是本地缓存,sqlSession
级别的缓存
二级缓存:全局范围的缓存,不止局限于当前会话
MyBatis
的二级缓存在某些场景下可以提高系统性能,但在大多数情况下不推荐使用,可能导致数据不一致性、内存占用过高、缓存同步问题和对复杂查询结果的管理困难 。
6.1. 一级缓存
基于 PerpetualCach
e 的 HashMap
本地缓存,其存储作用域为 SqlSession
,各个 SqlSession
之间的缓存相互隔离,当 Session flush 或 close 之后,该 SqlSession
中的所有 Cache 就将清空,MyBatis
默认打开一级缓存。
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void cache () { try (SqlSession session = sqlSessionFactory.openSession()) { EmployeeMapper mapper = session.getMapper(EmployeeMapper.class); List<Employee> employees = mapper.listByEmployee(); employees.forEach(System.out::println); System.out.println(" ------------ 默认开启一级缓存 ------------ " ); List<Employee> employeeList = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee" ); employeeList.forEach(System.out::println); } }
1 2 3 4 5 6 7 8 9 10 22:06:38.651 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==> Preparing: SELECT * FROM emp 22:06:38.668 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==> Parameters: 22:06:38.677 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - <== Total: 3 Employee{id=1, username='Tom'} Employee{id=2, username='Jack'} Employee{id=3, username='Tony'} ------------ 默认开启一级缓存 ------------ Employee{id=1, username='Tom'} Employee{id=2, username='Jack'} Employee{id=3, username='Tony'}
失效情况
不同的 SqlSession
会使一级缓存失效。
同一个 SqlSession
,但是查询语句不一样。
同一个 SqlSession
,查询语句一样,期间执行增删改操作。
同一个 SqlSession
,查询语句一样,执行手动清除缓存。
6.2. 二级缓存
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache
,HashMap
存储,不同之处在于其存储作用域为 Mapper(Namespace)
,可以在多个SqlSession
之间共享,并且可自定义存储源,如 Ehcache
。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable
序列化接口(可用来保存对象的状态),可在它的映射文件中配置。
图解(缓存查询的顺序是先查询二级缓存再查询一级缓存)
使用
在全局配置(mybatis-config.xml
)文件开启 二级缓存
1 2 3 4 5 <settings > <setting name ="cacheEnabled" value ="true" /> </settings >
1 2 3 4 5 6 7 8 public class Employee implements Serializable { private Integer id; private String username; }
1 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 public class EmployeeMapperTest { SqlSessionFactory sqlSessionFactory; @Before public void init () { String resource = "mybatis-config.xml" ; InputStream inputStream = null ; try { inputStream = Resources.getResourceAsStream(resource); } catch (IOException e) { e.printStackTrace(); } sqlSessionFactory = new SqlSessionFactoryBuilder ().build(inputStream); } @Test public void cache () { try (SqlSession session = sqlSessionFactory.openSession()) { EmployeeMapper mapper = session.getMapper(EmployeeMapper.class); List<Employee> employees = mapper.listByEmployee(); employees.forEach(System.out::println); System.out.println(" ------------ 开启二级缓存 ------------ " ); List<Employee> employeeList = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee" ); employeeList.forEach(System.out::println); } } @After public void listByEmployee () { try (SqlSession session = sqlSessionFactory.openSession()) { List<Employee> employees = session.selectList("com.wickson.mapper.EmployeeMapper.listByEmployee" ); employees.forEach(System.out::println); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 22:29:12.611 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==> Preparing: SELECT * FROM emp 22:29:12.626 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - ==> Parameters: 22:29:12.635 [main] DEBUG com.wickson.mapper.EmployeeMapper.listByEmployee - <== Total: 3 Employee{id=1, username='Tom'} Employee{id=2, username='Jack'} Employee{id=3, username='Tony'} ------------ 开启二级缓存 ------------ 22:29:12.635 [main] DEBUG com.wickson.mapper.EmployeeMapper - Cache Hit Ratio [com.wickson.mapper.EmployeeMapper]: 0.0 Employee{id=1, username='Tom'} Employee{id=2, username='Jack'} Employee{id=3, username='Tony'} 22:29:12.642 [main] DEBUG com.wickson.mapper.EmployeeMapper - Cache Hit Ratio [com.wickson.mapper.EmployeeMapper]: 0.3333333333333333 Employee{id=1, username='Tom'} Employee{id=2, username='Jack'} Employee{id=3, username='Tony'}
失效
同一个命名空间进行了增删改的操作,会导致二级缓存失效,但是如果不想失效:可以将 SQL
的 flushCache
这是为 false
,但是要慎重设置,因为会造成数据脏读问题,除非你能保证查询的数据永远不会执行增删改
让查询不缓存数据到二级缓存中 useCache="false"
如果希望其他 mapper
映射文件的命名空间执行了增删改清空另外的命名空间就可以设置:
1 <cache-ref namespace ="com.wickson.mapper.DeptMapper" />