Mybatis相关
目录
- 前言
- 常见的持久化层技术
- Mybatis 起步
- Mybatis全局配置文件
- Mybatis映射文件
- 动态Sql
- Mybatis缓存机制
- SSM整合
- Mybatis Generator
- Mybatis运行原理
- Mybatis插件开发
- Mybatis批量处理Sql
- Mybatis 类型处理器
- Mybatis后记
前言
计划花费一些时间学习一下当下非常流行的持久层框架Mybatis
.
视频参考:尚硅谷雷丰阳Mybatis教程
其它资料参考:Mybatis3官方中文文档
另外, 文章凡引用大佬文章的地方, 均贴出了链接, 感谢!
斜体文字均摘自MyBatis3官方文档
笔记仅供参考, 如有差错, 欢迎指出!
常见的持久化层技术
-
JDBC
原生持久化层技术, 更是一种规范.
使用
JDBC
写过项目的都知道,DAO层
需要写一系列接口和实现类.并且每个方法除了需要关注核心的sql
语句外,还需要频繁地创建
Connection
,PreparedStatement
,ResultSet
等非核心操作. 重复性工作很多-
针对JDBC的第三方工具包: DBUtil(Apache) , JDBCTemplate(Spring)
没使用过, 个人不做评价.
-
-
Hibernate (没使用过, 不做评价)
-
全自动: 我们只需要告诉
Hibernate
,JavaBean
与数据库中记录的对应关系, 相应的sql语句
由框架自动生成.这也就意味着很难进行sql优化(可以使用
Hibernate
提供的HQL
进行相关优化, 增加了学习成本) -
全映射: 意味着每次查询出来的都是表的全部字段, 效率较低. 而进行部分映射又需要借助
HQL
ORM:Object Relational Mapping 对象关系映射
将数据库表和实体类、实体类的属性对应起来,让我们可以实现操作实体类即操作数据表
-
-
Mybatis(半自动框架)
Mybatis
将编写SQL, 这一核心工作交给程序员来完成, 并且采用xml
形式解耦, 而非像JDBC
一样,将sql
硬编码在Java
代码之中
Mybatis 起步
第一个Mybatis Demo
下面就参照Mybatis官方文档-起步一节, 以Mybatis的方式, 进行一次查询并封装相应结果, 开始我们对Mybatis的学习.
-
首先引入
Mybatis
的依赖`<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.7</version> </dependency>
-
接下来配置
Mybatis
的全局配置文件全局配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(ransactionManager) 详细内容后面会在探讨
<?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="dbconfig.properties"/> <!-- 以properties 文件的形式, 给出数据库的相关配置信息 --> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <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> <mappers> <!--包含了一组映射器(mapper),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息 --> <mapper resource="studentMapper.xml"/> </mappers> </configuration>
#血的教训!!! Driver一定要大写, 不然会一直出ClassNotFoundException的 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/qdu?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8 jdbc.username=root jdbc.password=root
-
下面给出映射文件
studentMapper.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"> <mapper namespace="com.shy.mybatis.studentMapper"> <!-- namespace:调用已映射的sql语句用 命名空间.id --> <!-- id: 该ql语句在此namespace下的唯一标识 --> <!-- resultType: 此sql语句期望封装的返回值类型 --> <select id="selectStudentById" resultType="com.shy.entity.Student"> SELECT * FROM student WHERE id = #{id} <!-- #{id} 为动态输入的参数 --> </select> </mapper>
student
表: -
配置文件准备就绪, 接下来开始创建
SqlSessionFactory
,public class MybatisUtil { private static SqlSessionFactory sqlSessionFactory; public static synchronized SqlSessionFactory getSqlSessionFactory() throws IOException { // 获取mybatis全局配置文件的输入流 // Mybatis提供了 Resources 工具类, 方便获取配置文件的输入流 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 采用单例模式(官方文档推荐这样做)创建SqlSessionFactory if(null == sqlSessionFactory){ // SqlSessionFactoryBuilder唯一的用处就是创建SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } return sqlSessionFactory; } }
-
SqlSessionFactory
顾名思义就是来创建SqlSession
的工厂, 所以接下来创建SqlSession
, 并进行查询封装输出测试@Test public void test01() throws IOException { SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory(); // 利用SqlSessionFactory获取SqlSession, SqlSession相当与数据库的一次会话 SqlSession sqlSession = sqlSessionFactory.openSession(); try { // 调用mapper中的SQL语句, 并传入SQL语句所需参数 Student student = sqlSession.selectOne("com.shy.mybatis.studentMapper.selectStudentById",1); System.out.println(student.toString()); }finally { // 使用完sqlSession后一定要关闭 sqlSession.close(); } } //Student{id=1, name='Lando', gender='male', age=20}
Q: 为什么使用完sqlSession后一定要手动关闭?
A: 参考
使用Mapper接口
Student student = sqlSession.selectOne("com.shy.mybatis.studentMapper.selectStudentById",1);
在第一个demo
中, 我们使用了 "com.shy.mybatis.studentMapper.selectStudentById
来调用sql
语句:
<mapper namespace="com.shy.mybatis.studentMapper">
<select id="selectStudentById" resultType="com.shy.entity.Student">
SELECT * FROM student WHERE id = #{id}
</select>
</mapper>
不过, 现在更简洁,更流行的方式是使用与指定sql语句的参数和返回值相匹配的接口
演示:
-
首先编写相应的接口
StudentMapper.java
public interface StudentMapper { // 返回值与resultType相同 // 方法名与id相同 // 参数列表与Sql语句的参数保持一致 public Student selectStudentById(int id); }
-
将接口与mapper的配置文件进行绑定
即将
studentMapper.xml
中mapper
的namespace
改为接口的全限定名<mapper namespace="com.shy.dao.StudentMapper"> <!-- 动态绑定 --> <select id="selectStudentById" resultType="com.shy.entity.Student"> SELECT * FROM student WHERE id = #{id} </select> </mapper>
-
准备就绪后, 创建
SqlSession
进行测试@Test public void test03() throws IOException { SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); try{ // 调用getMapper(), Mybatis底层会通过AOP的方式, 创建出StudentMapper接口相应的实现类(代理设计模式) StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = studentMapper.selectStudentById(1); System.out.println(student.toString()); }finally { sqlSession.close(); } } // Student{id=1, name='Lando', gender='male', age=20}
使用注解配置Sql语句
public interface StudentMapper {
@Select("SELECT * FROM Student WHERE id = #{id}")
public Student selectStudentById(int id);
}
使用注解配置sql
语句, 就不需要studentMapper.xml
映射文件了.
注册mapper
时, 使用接口的全限定名. 因为没有xml
映射文件了
<mappers>
<mapper class="com.shy.dao.StudentMapper"/>
</mappers>
测试:
@Testpublic void test03() throws IOException { SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); try{ // 调用getMapper(), Mybatis底层会通过AOP的方式, 创建出StudentMapper接口相应的实现类(代理设计模式) StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = studentMapper.selectStudentById(1); System.out.println(student.toString()); }finally { sqlSession.close(); }}// Student{id=1, name='Lando', gender='male', age=20}
Mybatis全局配置文件
该配置文件决定了Mybatis
的工作方式
结构
Mybatis
全局配置文件主要包括以下几种属性
properties(属性)
<properties>
主要用来配置一些属性
最常见的方式是引入外部的.properties
文件(解耦合), 如dbconfig.properties
即数据库的相关信息
<properties resource="dbconfig.properties"/>
<!-- resource 类路径 url 网络路径/磁盘路径 -->
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=root
此外还可以在<properties>
标签内编写属性
<properties>
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
...
</properties>
或者作为方法参数传入
public SqlSessionFactory build(InputStream inputStream, Properties properties) {
return build(inputStream, null, properties);
}
如果不只一个地方进行了配置,Mybatis 将按照下面的顺序来加载:
通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性(高优先级覆盖低优先级)
settings(设置)
<settings>
用来配置一些可以影响Mybatis
工作方式的属性.
-
mapUnderscoreToCamelCase "驼峰式"策略映射
Mybatis
默认将表的字段值映射到Java Bean
中与字段名相同的属性上.student表 id字段 → \rightarrow →Student实体的 id属性上
而在实战当中, 数据库字段多写成
student_id
的形式,Java Bean
的属性多遵循驼峰式写成studentId
的形式.在这种情况下:
student表 student_id字段 ↛ \nrightarrow ↛Student实体的 studentId属性上
此时可以设置
mapUnderscoreToCamelCase
,开启驼峰命名的自动映射<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>
更多属性详情请参照官方文档
-
autoMappingBehavior 自动映射
-
lazyLoadingEnabled 延迟加载
-
aggressiveLazyLoading
typeAliases(类型别名)
类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写(但最好还是写全限定名,语义明确)
<typeAliases>
<!-- 别名默认使用 Bean 的首字母小写的非限定类名 student
<!--alias指定一个别名-->
<typeAlias type="com.shy.entity.Student" alias="student"/>
<package name="com.shy"/> <!-- 为此包下的每个类都起一个别名 -->
</typeAliases>
还可以使用@Alias
注解
@Alias("student")
public class Student{}
此外, Mybatis
还未Java常见的内置的类型提供了默认别名, 具体参看文档
typeHandlers(类型处理器)
typeHandlers
用来将JDBC
类型(如varchar
)和Java
类型(如String
)相映射, 完成类型的转换,从而将字段值封装到JavaBean
中
MyBatis
默认为我们提供了一系列内置的typeHandlers
详细内容见[Mybatis 类型处理器](#Mybatis 类型处理器)
*objectFactory(对象工厂)
每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作
默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。
如果想覆盖对象工厂的默认行为,可以通过继承
DefaultObjectFactory
创建自己的对象工厂来实现
plugins(插件)
MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用 (AOP)
environments(环境)
environment
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景
<environments default="development"> <!-- 指定默认的environment -->
<environment id="development"> <!-- 环境的唯一标识 -->
<transactionManager type="JDBC"/> <!-- 事务管理器 -->
<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>
<environment id="test">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
...
</dataSource>
</environment>
</environments>
在创建SqlSessionFactory
时可以指定environment
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"), "test");
transactionManager
对事务的理解不是太深, 直接搬上官方文档了
在Spring整合Mybatis时, 会使用Spring的相关功能管理事务
datasource
用来配置JDBC
连接对象的资源.
三大内置数据源类型(具体内容参考文档)
UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形
POOLED– 这种数据源的实现利用池的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求
JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
另外, 继承UnpooledDataSourceFactory
可以自定义数据源
databaseIdProvider
MyBatis
可以根据不同的数据库厂商执行不同的语句.
一定要设置属性别名, 不然识别不出来
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/> <!-- name 属性不能随意指定 -->
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
sqlSession.getConnection().getMetaData().getDatabaseProductName(); // 可以获取当前数据库的name属性
<!-- databaseId="mysql" 该查询只会在MySQL环境下被执行 -->
<select id="selectStudentById" resultType="com.shy.entity.Student" databaseId="mysql">
SELECT * FROM mybatis.student WHERE id = #{id}
</select>
如果配置了
databaseIdProvider
, 则内置参数_databaseProvider
代表当前数据库的名称
mappers
mapper
用来告诉Mybatis
,Sql映射文件
或对应的接口的位置.
<mappers>
<!-- resource 适用于 StudentMapper.xml 与 StudentMapper接口不在同一路径下的情况 -->
<mapper resource="StudentMapper.xml"/> <!-- 根据我三四天的经验来看, 这个在IDEA下更常用一些 -->
<mapper class="com.shy.dao.StudentMapper"/>
<package name="com.shy.dao"/>
</mappers>
以上三种配置方式, 适用于不同情况, 具体参考大佬博客
Mybatis映射文件
MyBatis 的真正强大在于它的语句映射,这是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 致力于减少使用成本,让用户能更专注于 SQL 代码。
结构
Mybatis映射文件是其最为核心的部分, 这一部分的学习主要参考尚硅谷雷丰阳Mybatis教程, 顺序与上图结构略有不同
insert/update/delete
这三个元素分别用来映射插入/更新/删除语句
<!-- public int insertStudent(Student student);-->
<!-- 一般 parameterType 省略即可, Mybatis可以自动识别传入参数的类型 -
<insert id="insertStudent" parameterType="com.shy.entity.student">
INSERT INTO student(name, gender, age)
VALUES (#{name}, #{gender}, #{age})
</insert>
<!-- public int updateStudentById(Student student);-->
<update id="updateStudentById">
UPDATE student
SET name = #{name},
gender = #{gender},
age = #{age}
WHERE id = #{id}
</update>
<!-- public int deleteStudentById(int id);-->
<delete id="deleteStudentById">
DELETE
FROM student
WHERE id = #{id}
</delete>
属性总览
注意事项
-
通过传入
Java Bean
传递多个参数在上述插入和更新方法中, 我们直接传入
Student
对象(封装多个参数), 这样在Sql
语句中就可以使用#{bean的属性}
来传递值. -
增删改要及时提交
commit
如果不调用
SQLSession.commit()
,Sql
语句虽然会执行成功, 但数据库中不会有相应修改这是因为如果不
commit
, 在sqlSession.close()
, 会自动回滚rollback
// 相关源码 @Override public void close(boolean forceRollback) { try { if (forceRollback) { // 不commit, forceRollback = true tcm.rollback(); } else { tcm.commit(); } } finally { delegate.close(forceRollback); } }
try { StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); Student student = new Student("Norris","male",25); int rows = studentMapper.insertStudent(student); System.out.println(rows); sqlSession.commit(); // 一定要commit }finally { sqlSession.close(); }
当然也可在创建
SqLSession
时设置autoCommit
SqlSession sqlSession = sqlSessionFactory.openSession(true);
-
增删改的返回值可以是
void/Integer(int)/Long/Boolean
只需在接口对应的方法上指定返回值类型即可
public int/Boolean updateStudentById(Student student);
// 相关源码 private Object rowCountResult(int rowCount) { final Object result; if (method.returnsVoid()) { result = null; } else if (Integer.class.equals(method.getReturnType()) || Integer.TYPE.equals(method.getReturnType())) { result = rowCount; } else if (Long.class.equals(method.getReturnType()) || Long.TYPE.equals(method.getReturnType())) { result = (long) rowCount; } else if (Boolean.class.equals(method.getReturnType()) || Boolean.TYPE.equals(method.getReturnType())) { result = rowCount > 0; } else { throw new BindingException("Mapper method '" + command.getName() + "' has an unsupported return type: " + method.getReturnType()); } return result; }
-
获取自增主键(将自增主键赋值给Java Bean的属性)
<!-- keyProperty指定自增主键赋值给Java Bean的哪个属性 --> <insert id="insertStudent" useGeneratedKeys="true" keyProperty="id"> INSERT INTO student(name, gender, age) VALUES (#{name}, #{gender}, #{age}) </insert>
此方式只适用于支持主键自增的数据库(如
MySql
,SqlServer
)对于不支持主键自增的数据库,使用selectKey
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kBNOdtA0-1631105867439)(D:/Typora%20Img/image-20210820215214427.png)]
Sql语句中的参数传递
源码分析: Mybatis参数传递封装成Map的过程
在编写XXXMapper.xml
文件中的Sql
语句时, 往往会涉及到#{xx}
的参数传递的问题,下面就简要探讨一下
-
单个参数
#{key}
中key
可以是任意形式, 但一般与入参名称保持一致<!-- public Student selectStudentById(int id);--> <select id="selectStudentById" resultType="com.shy.entity.Student"> SELECT *FROM student WHERE id = #{id} </select>
-
多个参数
-
默认情况下
#{param1/args0}
对应第一个参数,#{param2/args1}
对应第二个参数…<!-- public Student selectStudentByNameAndGender(String Name, String Gender);--> <select id="selectStudentByNameAndGender" resultType="com.shy.entity.Student"> # SELECT * FROM student WHERE name = #{arg0} AND gender = #{arg1} # SELECT * FROM student WHERE name = #{param1} AND gender = #{param2} </select>
由于这种参数不够语义化, 所以不推荐使用
-
接口参数使用
@Param
注解#{key}
中的值可以是@Param("")
中的值<!-- public Student selectStudentByNameAndGender(@Param("name") String Name, @Param("gender") String Gender);--> <select id="selectStudentByNameAndGender" resultType="com.shy.entity.Student"> SELECT * FROM student WHERE name = #{name} AND gender = #{gender} </select>
-
另外接口的参数可能有多种类型
-
接口参数为
POJO
POJO: plain ordinary java object 简单Java Bean
#{key}
中的值对应POJO
的某一项属性public class Student { private int id; private String name; private String gender; private int age; ... }
<!-- public int updateStudentById(Student student);--> <update id="updateStudentById"> UPDATE student SET name = #{name},gender = #{gender},age = #{age}WHERE id = #{id} </update>
对于用
@Param("student")
标注的参数, 可以用#{student.name}
来进行映射 -
接口参数封装成
Map
假如我们要传递的参数某一entity中的数据, 且不经常使用, 我们可以将其封装成Map进行传递.
#{key}
中的key
对应在Map
中封装的key
<!-- public Student selectStudentByMap(Map<String,Object> map);--> <select id="selectStudentByMap" resultType="com.shy.entity.Student"> SELECT * FROM student WHERE name = #{name} AND age = #{age} </select>
对于用
@Param("map")
标注的参数, 可以用#{map.name}
来进行映射// 测试 Map<String, Object> map = new HashMap<>(); map.put("name","Fati"); map.put("age",18); Student student = studentMapper.selectStudentByMap(map); System.out.println(student.toString()); // Student{id=2, name='Fati', gender='male', age=18}
假如经常使用,我们可以将其封装成一个TO(数据传输对象)来进行传递(如分页类Page)
-
接口参数类型为Collection(List/Set) / Array
参数类型为
List
, 则用#{list[0]}/#{collection[0]}
指代其中的第一个元素参数类型为
set
,则用#{collection[0]}
指代其中的第一个元素参数类型为
array
则用#{array[0]}
指代其中的第一个元素当然如果用
@Param()
标注后, 就可以无视以上规则// #{actorIdList[0]} public List<Actor> selectActorListByActorIdList(@Param("actorIdList") List<Integer> actorIdList);
// 相关源码 public static Object wrapToMapIfCollection(Object object, String actualParamName) { if (object instanceof Collection) { ParamMap<Object> map = new ParamMap<>(); map.put("collection", object); if (object instanceof List) { map.put("list", object); } Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object)); return map; } else if (object != null && object.getClass().isArray()) { ParamMap<Object> map = new ParamMap<>(); map.put("array", object); Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object)); return map; } return object; }
#{}与${}
#{}
类似于PreparedStatement
中的?
占位符, 能够防止sql
注入, 但是表名,字段名和关键字(如排序方式)无法使用#{}
当sql语句中需要动态替换这些字段时, 就可以使用${}
.
${}
实质上是字符串拼接.
<!-- public List<Actor> selectActorListByColumnAndOrder(String column, String order);-->
<select id="selectActorListByColumnAndOrder" resultType="com.shy.entity.Actor">
SELECT * FROM actor ORDER BY ${column} ${order}
</select>
#{}还支持设置一些其他的属性
处理null值
Mybatis
默认将Java
中的null
值映射为JDBC
中的Other
类型, 而对于一些数据库(如Oracle
)无法识别.
这时可以设置将null
值映射为JDBC
中的NULL
类型
#{name, jdbcType=NULL}
或者修改全局默认配置
<settings>
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
select返回List/Map
<!-- public List<Actor> selectActorList();-->
<select id="selectActorList" resultType="com.shy.entity.Actor"> <!-- resultType为List包含的类型 -->
SELECT * FROM actor;
</select>
此外, Mybatis
还支持将返回值List
封装成Map
,只需指定Map
的Key
@MapKey("ActorId")
public Map<Integer,Actor> selectActorMap();
<select id="selectActorMap" resultType="com.shy.entity.Actor">
SELECT * FROM actor
</select>
resultMap
resultMap
是Mybatis
中最重要最强大的元素, 它可以让你自定义数据库字段与JavaBean属性的映射规则
当自动映射的规则不能满足需求时, 就可以使用resultMap
自定义映射规则
id/result
id 用来指定主键列
result 用来指定普通字段
<!-- 自定义映射规则. type: 返回值类型 -->
<resultMap id="actorResultMap" type="com.shy.entity.Actor">
<!-- property: JavaBean的某一属性 column:表的某一字段 -->
<id property="ActorId" column="aid"/> <!-- id 用来指定主键列 -->
<result property="ActorChineseName" column="aChineseName"/> <!-- result 用来指定普通字段 -->
<!-- 其他名称相同字段与属性, 便会按照规则自动映射. 当然我们也可以指定 -->
<result property="ActorOriginName" column="ActorOriginName" />
<result property="ActorInfo" column="ActorInfo" />
</resultMap>
<select id="selectActorList" resultMap="actorResultMap">
SELECT * FROM actor
</select>
1:1关联查询
假设有Student
实体
public class Student {
private int id;
private String name;
private String gender;
private int age;
private School school; // 一名学生只在一所学校学习
}
public class School {
private int schoolId;
private String schoolName;
}
数据库中有对应表student
,school
现在想要查出id=1
的学生信息, 则需要进行多表查询
SELECT * FROM student, school WHERE student.schoolId = school.schoolId AND id = 1;
并且将查出的信息借助Mybatis
封装到Student
对象中
按之前所学,我们可能会这样写:
<select id="selectStudentById" resultType="com.shy.entity.Student">
SELECT * FROM student, school WHERE student.schoolId = school.schoolId AND id = #{id}
</select>
但测试之后发现school
并不能正确封装
Student{id=1, name='Pedri', gender='male', age=18, school=null}
这时, 我们便需要使用resultType
自定义映射规则
-
级联属性封装查询结果
<resultMap id="studentResultMap" type="com.shy.entity.Student"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="gender" column="gender"/> <result property="age" column="age"/> <!-- school实体的属性 --> <result property="school.schoolId" column="schoolId"/> <result property="school.schoolName" column="schoolName"/> </resultMap>
-
使用
association
封装查询结果-
嵌套结果映射
<resultMap id="studentResultMap" type="com.shy.entity.Student"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="gender" column="gender"/> <result property="age" column="age"/> <association property="school" javaType="com.shy.entity.School"> <id property="schoolId" column="schoolId"/> <result property="schoolName" column="schoolName"/> </association> </resultMap>
<!-- 也可以定义在外部, 以便复用 --> <resultMap id="schoolResultMap" type="com.shy.entity.School"> <id property="schoolId" column="schoolId"/> <result property="schoolName" column="schoolName"/> </resultMap> <resultMap id="studentResultMap" type="com.shy.entity.Student"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="gender" column="gender"/> <result property="age" column="age"/> <association property="school" resultMap="schoolResultMap"/> <!-- 引用 schoolResultMap --> </resultMap>
-
嵌套
Select
查询简单来说, 就是把多表查询分解成了多个简单的查询
# 原有的多表查询 SELECT * FROM student, school WHERE student.schoolId = school.schoolId AND id = #{id}; # 分解后的查询 1.SELECT * FROM student WHERE id = #{id} 2.SELECT * FROM school WHERE schoolId = #{schoolId}
<!-- studentMapper.xml--> <select id="selectStudentById" resultMap="studentResultMap"> SELECT * FROM student WHERE id = #{id} </select> <resultMap id="studentResultMap" type="com.shy.entity.Student"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="gender" column="gender"/> <result property="age" column="age"/> <!--将selectStudentById查出的schoolId传递给com.shy.dao.SchoolMapper.selectSchoolBySchoolId进行查询--> <association property="school" select="com.shy.dao.SchoolMapper.selectSchoolBySchoolId" column="schoolId"/> </resultMap> <!-- schoolMapper.xml--> <select id="selectSchoolBySchoolId" resultType="com.shy.entity.School"> SELECT * FROM school WHERE schoolId = #{schoolId} </select>
# 延迟加载简单来讲, 就是只有在用到school的相关属性时, 才会执行第2条sql. 从而提高效率 1.SELECT * FROM student WHERE id = #{id} 2.SELECT * FROM school WHERE schoolId = #{schoolId}
<association ... fetchType="lazy"/> <!-- 开启延迟加载 --> <setting name="lazyLoadingEnabled" value="TRUE"/> <!-- 或通过全局配置开启延迟加载 --->
另外, 有时嵌套的子查询可能需要多个参数, 这是我们可以借助
column={属性1=字段1,属性2-字段2}
来传递.
-
1:n关联查询
假设有FootballClub
实体
public class FootballClub {
private int id;
private String name;
private List<Player> players; // 一支球队有多名球员
}
public class Player {
private int id;
private String name;
}
数据库中有对应表 football_club
,player
现在我们想要查询出id=1
的FootballClub
的信息并使用Mybatis
将结果封装到对象中
SELECT * FROM football_club, player WHERE football_club.fc_id = player.fc_id AND football_club.fc_id = 1
这时, 我们需要借助collection
将相关信息封装到List
集合中, 同样可以采用嵌套结果映射和嵌套SELECT映射两种方式
-
嵌套结果映射
<select id="selectFootballClubByFootBallClubId" resultMap="FootballClubResultMap"> SELECT * FROM football_club, player WHERE football_club.fc_id = player.fc_id AND football_club.fc_id = #{id} </select> <resultMap id="FootballClubResultMap" type="com.shy.entity.FootballClub"> <id property="id" column="fc_id"/> <result property="name" column="fc_name"/> <!-- ofType 指定集合内封装的类型(必须指定!)--> <collection property="players" ofType="com.shy.entity.Player"> <id property="id" column="player_id"/> <result property="name" column="player_name"/> </collection> </resultMap>
-
嵌套SELECT映射
<!-- FootballClub.xml --> <resultMap id="FootballClubResultMap" type="com.shy.entity.FootballClub"> <id property="id" column="fc_id"/> <result property="name" column="fc_name"/> <!-- 将 fc_id 传递给selectPlayerListByFootballClubId 查出列表封装给players --> <collection property="players" select="com.shy.dao.PlayerMapper.selectPlayerListByFootballClubId" column="fc_id" ofType="com.shy.entity.Player"/> </resultMap> <select id="selectFootballClubByFootBallClubId" resultMap="FootballClubResultMap"> SELECT * FROM football_club WHERE fc_id = #{fc_id} </select> <!-- PlayerMapper.xml--> <resultMap id="PlayerResultMap" type="com.shy.entity.Player"> <id property="id" column="player_id"/> <result property="name" column="player_name"/> </resultMap> <select id="selectPlayerListByFootballClubId" resultMap="PlayerResultMap"> SELECT * FROM player WHERE fc_id = #{fc_id} </select>
discriminator(鉴别器)
discriminator
可以根据情况关联不同的resultMap
sql 可重用代码段
<sql>
标签中可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用
<include>
标签用来引用已经应以的<sql>
标签, 并且在其中可以定义不同的参数值
<sql id="studentColumns"> <!-- 将查询字段抽取出来 -->
${table}.id,${table}.name,${table}.gender,${table}.age,${table}.schoolId
</sql>
<select id="selectStudentById" resultMap="studentResultMap">
SELECT
<include refid="studentColumns"> <!-- 引用 <sql>片段 -->
<property name="table" value="student"/> <!-- 设置参数 -->
</include>
FROM student WHERE id = #{id}
</select>
动态Sql
Mybatis
提供了动态Sql
技术, 实现在不同的情况拼接Sql语句, 从而执行不同的Sql语句的功能.
if
if
最常见的应用场景就是根据条件包含where
子句的一部分
public List<Student> selectStudentListByIfConditions
(@Param("name")String name, @Param("gender")String gender, @Param("age") Integer age);
<select id="selectStudentListByIfConditions" resultType="com.shy.entity.Student">
SELECT * FROM student WHERE
<if test="name != null">name = #{name}</if>
<if test="gender != null">AND gender = #{gender}</if>
<if test="age != null">AND age = #{age}</if>
</select>
// 测试
List<Student> studentList = studentMapper.selectStudentListByIfConditions("messi", "male", null);
// SELECT * FROM student WHERE name = "messi" AND gender = "male"
// age 为空没有作为筛选条件
test
中的遵循OGNL的语法
又或者update
动态更新一部分数据
public int updateStudentInfoByIfConditions
(@Param("name") String name, @Param("gender")String gender, @Param("age") Integer age, @Param("id") Integer id);
<update id="updateStudentInfoByIfConditions">
UPDATE student SET
<if test="name != null">name = #{name},</if>
<if test="gender != null">gender = #{gender},</if>
<if test="age != null">age = #{age}</if>
WHERE id = #{id}
</update>
// 测试
int rows = studentMapper.updateStudentInfoByIfConditions("Leo Messi",null,18,7);
// UPDATE student SET name = "Leo Messi", age = 18 WHERE id = 7;
// gender 为空没有更新
where/set/trim
-
where
where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除 (只会去除开头多余的AND OR)
<select id="selectStudentListByIfConditions" resultType="com.shy.entity.Student"> SELECT * FROM student WHERE <if test="name != null">name = #{name}</if> <if test="gender != null">AND gender = #{gender}</if> <if test="age != null">AND age = #{age}</if> </select> 考虑这个SELECT, 假如测试条件中只有一个gender="male", 那么将要被执行的sql语句为: SELECT * FROM student WHERE AND gender = "male" 显然AND是多余的. 这是我们就要借助 <where> 标签, 其会自动去掉开头的 AND. <select id="selectStudentListByIfConditions" resultType="com.shy.entity.Student"> SELECT * FROM student <where> <if test="name != null">name = #{name}</if> <if test="gender != null">AND gender = #{gender}</if> <if test="age != null">AND age = #{age}</if> </where> </select> 此时执行的sql语句为: SELECT * FROM student WHERE gender = "male"
-
set
set元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号 (只会去掉句末的逗号)
<update id="updateStudentInfoByIfConditions"> UPDATE student SET <if test="name != null">name = #{name},</if> <if test="gender != null">gender = #{gender},</if> <if test="age != null">age = #{age}</if> WHERE id = #{id} </update> 考虑这个UPDATE, 假如测试条件中只有一个name="梅西", 那么将要被执行的sql语句为: UPDATE student SET name = "梅西", WHERE id = 7; 显然 , 是多余的, 这时我们就要借助<set>标签, 其会自动去掉末尾多余的 , <update id="updateStudentInfoByIfConditions"> UPDATE student <set> <if test="name != null">name = #{name},</if> <if test="gender != null">gender = #{gender},</if> <if test="age != null">age = #{age}</if> </set> WHERE id = #{id} <!-- 一定要放在set标签外, 不然 ,无法成为末尾 --> </update> 此时执行的sql语句为: UPDATE student SET name = "梅西" WHERE id = 7;
-
trim
有时
<where>
和<set>
可能无法满足我们的需求.我们便可以使用
<trim>
来自定义 前后缀以及开头末尾要去掉的字符
<!-- 等效于 <update> -->
<trim prefix="SET" suffixOverrides=",">
<!-- 等效于 <where> -->
<trim prefix="WHERE" prefixOverrides="AND">
<trim prefix="WHERE" prefixOverrides="OR">
choose
从多个条件中选择一个使用, 类似于switch
<select id="selectStudentListByChooseCondition" resultType="com.shy.entity.Student">
SELECT * FROM student
<trim prefix="WHERE" prefixOverrides="AND">
<choose>
<when test="name != null">name = #{name}</when>
<when test="gender != null">gender = #{gender}</when>
<otherwise>age = #{age}</otherwise>
</choose>
</trim>
</select>
foreach
-
使用
in
进行查询<!-- public int selectStudentByIdList(@Param("idList") List<Integer> IdList);--> <select id="selectStudentByIdList" resultType="com.shy.entity.Student"> SELECT * FROM student WHERE id IN <foreach collection="idList" item="id" open="(" close=")" separator=","> #{id} </foreach> </select> SQL: SELECT * FROM SELECT * FROM student WHERE id IN (1, 2, 3)
-
批量插入
<!-- public int insertStudentList(@Param("studentList")List<Student> studentList);--> <insert id="insertStudentList"> INSERT INTO student(name, gender, age) VALUES <foreach collection="studentList" item="student" separator=","> (#{student.name},#{student.gender},#{student.age}) </foreach> </insert> SQL: INSERT INTO student(name, gender, age) VALUES ("Messi","male",34),("Xavi","male",40); <!-- public int insertStudentList2(@Param("studentList")List<Student> studentList);--> <insert id="insertStudentList2"> <foreach collection="studentList" item="student" separator=";"> INSERT INTO student(name, gender, age) VALUES (#{student.name}, #{student.gender}, #{student.age}) </foreach> </insert> SQL: INSERT INTO student(name, gender, age) VALUES ("Messi","male",34); INSERT INTO student(name, gender, age) VALUES ("Xavi","male",40), <!-- 此方式需要设置MySQL的全局属性 allowMultiQueries=true(设置此属性,需将其附加在jdbc.url后) -->
bind
bind
允许你使用OGNL表达式创建一个变量, 以便在需要时引用这个变量
<!-- 例子直接搬官网了 -->
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
_parameter
是Mybatis
内置的参数, 在没有方法参数使用@Param
注解前提下:若只有一个参数, 则
_parameter
代表这个参数若有多个参数, 则
_parameter
代表这些个参数封装的map
.
Mybatis缓存机制
考虑下面一个应用场景:
有时我们在做查询时, 可能执行两次相同的查询拿到同样的数据, 如果第二次查询可以直接引用之前的结果, 那么查询效率就会大大增加.
而MyBatis
内置了一个强大的事务性查询缓存机制, 可以有效提高查询效率
MyBatis
缓存机制包括一级缓存和二级缓存, 同时支持实现Cache
接口自定义二级缓存
一级缓存
-
一级缓存(Local Cache), 默认作用域是
SqlSession
-
同一会话期间查询到的数据会放到一级缓存中, 之后如果需要获取相同的数据, 会直接从一级缓存中得到
studentMapper.selectStudentById(1); studentMapper.selectStudentById(1); 执行上述两次相同的查询, Mybatis只向数据库发送了一次查询请求
-
一级缓存可以通过全局配置的
localCacheScope
改变作用域如果设置 localCacheScope = STATEMENT, 即使两次相同的查询, 也需要向数据库请求两次, 一级缓存失效.
-
-
一级缓存不可以被关闭, 但是可以手动清除
-
sqlSession.clearCache()
手动清除一级缓存 -
CRUD
标签的flushCache
属性手动清除一级/二级缓存CRUD即 增加(Create)、检索(Retrieve)、更新(Update)和删除(Delete)
<!-- SELECT 默认不刷新Cache --> <select id="selectStudentByName" resultType="com.shy.entity.Student" flushCache="false"> SELECT * FROM student WHERE name = #{name} </select> <!-- INSERT/UPDATE/DELETE 默认刷新Cache --> <insert id="insertStudent" useGeneratedKeys="true" keyProperty="id" flushCache="true"> INSERT INTO student(name, gender, age) VALUES (#{student.name, jdbcType=NULL}, #{student.gender}, #{student.age}) </insert>
-
-
一级缓存失效的四种情况
二级缓存
-
二级缓存的作用域是基于
namespace
的, 即一个Mapper
接口对应一个二级缓存 -
二级缓存需要手动配置
-
在全局配置文件中, 通过
cacheEnabled = true
开启二级缓存虽然默认值为true, 不过还是推荐显式地配置该属性, 即使与默认值相同
-
在需要使用二级缓存的
mapper.xml
文件中添加<cache/>
标签配置二级缓存-
<cache>
标签的属性
-
-
对应的POJO要实现
Serializable
接口
-
-
二级缓存的工作流程
- 二级缓存中的数据全部来自一级缓存: 当SqlSession关闭后,该会话对应的一级缓存中的数据才会被保存到对应的二级缓存中
- 在
Mybatis
执行CRUD
操作时, 会依次到二级缓存/一级缓存中查找缓存, 若没有相应缓存, 则执行SQL语句
// 在开启二级缓存后, 每执行一条CRUD操作. 控制台中都会打印出缓存命中率(缓存命中/查找次数) [main] [com.shy.dao.StudentMapper]-[DEBUG] Cache Hit Ratio [com.shy.dao.StudentMapper]: 0.5
-
二级缓存支持手动请除
-
CRUD
标签的flushCache
属性手动清除一级/二级缓存 -
SELECT
标签的useCache
属性可以配置该查询是否使用二级缓存 - 当进行了insert/update/delete操作后, 二级缓存将被刷新
-
自定义缓存
MyBatis
支持通过实现Cache
接口来自定义缓存
public class MyCache implements Cache {
@Override
public String getId() {
return null;
}
@Override
public void putObject(Object key, Object value) {}
@Override
public Object getObject(Object key) {
return null;
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {}
@Override
public int getSize() {
return 0;
}
}
不过在实战中, 我们往往选用更优秀的第三方缓存整合到Mybatis中
在Mybatis的github仓库中可以找到许多整合方案
缓存机制流程
SSM整合
由于Spring MVC还没有学习, 此章节稍后补充
Mybatis Generator
更多详细信息参考 Mybatis Generator 官方文档 (只有英文)
-
引入依赖
<dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.4.0</version> </dependency>
-
编写
Mybatis Generator
配置文件<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- 从类路径导入相关JAR包 不过我们使用maven管理JAR包--> <!-- <classPathEntry location="/Program Files/IBM/SQLLIB/java/db2java.zip" />--> <!-- 读取外部.properties文件--> <properties resource="dbconfig.properties"/> <!-- context中用于配置MBG的一些属性 targetRuntime 指定MBG生成代码的风格(具体参考文档)--> <context id="MysqlTables" targetRuntime="MyBatis3"> <!--指定如何连接到目标数据库--> <jdbcConnection driverClass="${jdbc.driver}" connectionURL="${jdbc.url}" userId="${jdbc.username}" password="${jdbc.password}"> </jdbcConnection> <!-- 指定MBG如何 JDBC类型和JAVA类型相互转换时的策略 这里我们使用默认配置,详细信息参考文档--> <javaTypeResolver > <property name="forceBigDecimals" value="false" /> </javaTypeResolver> <!-- 指定JavaBean生成的包和工程的位置, 以及JavaBean时的策略(默认配置, 详细信息参考文档)--> <!-- 路径一定要回, 不然什么东西都不会生成 <javaModelGenerator targetPackage="com.shy.model" targetProject="src/main/java"> <property name="enableSubPackages" value="true" /> <property name="trimStrings" value="true" /> </javaModelGenerator> <!-- 指定mapper映射文件生成的包和工程的位置, 以及生成时的策略(默认配置, 详细信息参考文档)--> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"> <property name="enableSubPackages" value="true" /> </sqlMapGenerator> <!-- 指定Mapper接口生成的类型, 包和工程的位置, 以及生成时的策略(默认配置, 详细信息参考文档)--> <javaClientGenerator type="XMLMAPPER" targetPackage="com.shy.mapper" targetProject="src/main/java"> <property name="enableSubPackages" value="true" /> </javaClientGenerator> <!-- table用于指定根据那些表创建JavaBean以及Mapper--> <!-- schema 数据库中的哪个模式 tableName 哪张表 domainObjectName 生成的Bean的名称 --> <table schema="mybatis" tableName="student" domainObjectName="Student" > <!-- 一些生成策略 详细信息参考文档--> <!-- <property name="useActualColumnNames" value="true"/>--> <!-- <generatedKey column="ID" sqlStatement="DB2" identity="true" />--> <!-- <columnOverride column="DATE_FIELD" property="startDate" />--> <!-- <ignoreColumn column="FRED" />--> <!-- <columnOverride column="LONG_VARCHAR_FIELD" jdbcType="VARCHAR" />--> </table> <table tableName="actor" domainObjectName="Actor"/> </context> </generatorConfiguration>
-
运行
Mybatis Generator
运行
MBG
有多种方式, 这里我们选择Running MBG from Java with an XML Configuration File
// 生成代码照搬官网 @Test public void test() throws Exception { List<String> warnings = new ArrayList<String>(); boolean overwrite = true; File configFile = new File("src/main/resources/MBGConfig.xml"); // 加载MBG配置文件 ConfigurationParser cp = new ConfigurationParser(warnings); Configuration config = cp.parseConfiguration(configFile); DefaultShellCallback callback = new DefaultShellCallback(overwrite); MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings); myBatisGenerator.generate(null); }
-
生成的
entity
,mapper接口
,mapper映射文件
其中我们可以通过生成的
XXXexample类
构造任何查询条件
配合生成的mapper
接口进行CRUD
操作
从mapper.xml
可以看出, MBG
通过动态sql
构造条件
<sql id="Example_Where_Clause">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
This element was generated on Sat Aug 28 08:43:33 CST 2021.
-->
<where>
<foreach collection="oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
- 测试
@Test
public void test16() throws IOException {
SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
StudentExample studentExample = new StudentExample();
// Criteria 用来封装查询条件, 内部用and连接
StudentExample.Criteria studentExampleCriteria = studentExample.createCriteria();
studentExampleCriteria.andGenderEqualTo("male");
studentExampleCriteria.andAgeEqualTo(18);
StudentExample.Criteria studentExampleCriteria1 = studentExample.createCriteria();
studentExampleCriteria1.andNameLike("%e%");
// 不同的Criteria之间可以用or连接
studentExample.or(studentExampleCriteria1);
studentMapper.selectByExample(studentExample);
}finally {
sqlSession.close();
}
}
// Mybatis执行的sql:
//[com.shy.mapper.StudentMapper.selectByExample]-[DEBUG] ==> Preparing: select id, name, gender, age, schoolId from student WHERE ( gender = ? and age = ? ) or( name like ? )
//[main] [com.shy.mapper.StudentMapper.selectByExample]-[DEBUG] ==> Parameters: male(String), 18(Integer), %e%(String)
Mybatis运行原理
此部分建议参考 Mybatis源码分析视频
/**
* DEBUG模式简单学习Mybatis运行原理
*/
@Test
public void test17() throws IOException {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 1.获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 2.获取sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.获取Mapper接口的代理对象
ActorMapper actorMapper = sqlSession.getMapper(ActorMapper.class);
// 4.执行CRUD操作
List<Actor> actorList = actorMapper.selectActorList();
}
在使用Mybatis
进行CRUD
操作时, 往往需要进行以上4步. 下面结合上述视频中的时序图, 在debug
模式下简单学习一下Mybatis
运行原理
-
获取
sqlSessionFactory
对象Mybatis通过相关解析器解析全局配置文件中的信息, 以及各个
mapper.xml
中的信息(包括封装了sql语句的MappedStatement对象, 以及封装了创建各个mapper接口代理对象的工厂的MappedRegisty),
将这些信息封装保存到Configuration对象中. 并通过build()方法, 创建了包含该Configuration对象的DefaultSqlSessionFactory对象
-
获取
sqlSession
对象DefaultSqlSessionFactory创建了DefaultSqlSession对象, 其包含Configuration对象和Executor对象等
其中, Executor对象是用来执行
CRUD
操作的 -
获取Mapper接口的代理对象
DefaultSqlSession对象通过getMapper()方法创建出对应mapper接口的动态代理对象 mapperProxy,
其包含了SqlSession(其中有executor, 从而能进行增删改查)对象
-
执行CRUD操作
Mybatis插件开发
开发第一个插件
实现Interceptor
接口
→
\rightarrow
→ 编写@Intercepts
注解指定拦截哪些对象的哪些方法
→
\rightarrow
→ 注册该Interceptor
package com.shy.plugin;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.javassist.tools.reflect.Metaobject;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Statement;
import java.util.Properties;
// 指定拦截哪些对象(Executor/ParameterHandler/ResultSetHandler/StatementHandler)的哪些方法
@Intercepts({
@Signature(type = StatementHandler.class, method = "parameterize", args = {Statement.class})
})
public class MyPlugin implements Interceptor {
/**
* 只会拦截目标对象的目标方法
* @param invocation 包含目标对象,方法的一些信息
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// System.out.println("在原方法执行之前执行代码");
System.out.println("invocation.getArgs() = " + invocation.getArgs());
System.out.println("invocation.getMethod() = " + invocation.getMethod());
System.out.println("invocation.getTarget() = " + invocation.getTarget());
System.out.println("invocation.getClass() = " + invocation.getClass());
// SystemMetaObject.forObject获取的metaObject封装了一个originalObject,本例中即StatementHandler的实现类
MetaObject metaObject = SystemMetaObject.forObject(invocation.getTarget());
// metaObject.getValue获取属性
Object boundSql = metaObject.getValue("boundSql");
System.out.println("boundSql.sql = " + boundSql);
// metaObject.setValue("key","value"); 可以设置属性, 前提是有set方法
Object result = invocation.proceed(); // 执行原对象的原方法
// System.out.println("在原方法执行之后执行代码");
return result;
}
/**
* 为target创建代理对象
* @param target 目标对象
* @return 如果target是要拦截的对象, 则返回target的代理对象. 否则直接返回target
*/
@Override
public Object plugin(Object target) {
Object wrap = Plugin.wrap(target, this); // wrap就是来为target创建动态代理对象
return wrap;
}
/**
* 将<plugin>标签中的属性设置进来
*/
@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
<configuration>
<plugins>
<plugin interceptor="com.shy.plugin.MyPlugin">
<property name="username" value="root"/>
<property name="password" value="root"/>
</plugin>
</plugins>
</configuration>
编写插件之前一定要大致了解
Mybatis
的运行原理, 否则可能会破坏其运行流程尤其是要熟悉四大对象
Executor/ParameterHandler/ResultSetHandler/StatementHandler
的创建时间及一些方法的大致作用
编写的插件会在
SqlSessionFactory
创建时, 通过configuration.addInterceptor(interceptorInstance);
封装到Configuration
对象中.另外,
Executor/ParameterHandler/ResultSetHandler/StatementHandler
创建之后都会调用interceptorChain.pluginAll()
方法, 根据已经注册的Interceptor
决定为哪些对象创建代理
假如有多个插件拦截了同一个对象, 则会在创建代理的代理对象, 所以在执行方法时, 先执行
SecondPlugin
,再执行FirstPlugin
插入的代码
PageHelper 第三方分页插件
简单学习一下
PageHelper
的使用, 详细信息请参考: MyBatis 分页插件 PageHelper 官方文档
-
引入依赖
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.1</version> </dependency>
-
注册插件
PageHelper
本质上是一个Interceptor
, 利用动态代理的方式添加分页逻辑<configuration> <plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin> </plugins> </configuration>
-
简单使用
/** * 测试PageHelper */ @Test public void test() throws IOException { SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(); try { ActorMapper actorMapper = sqlSession.getMapper(ActorMapper.class); // 查询第一页, 一页5条数据 PageHelper.startPage(1,5); List<Actor> actorList = actorMapper.selectActorList(); for (Actor actor : actorList) { System.out.println(actor.toString()); } // PageInfo类封装了有关分页的信息 PageInfo pageInfo = new PageInfo(actorList,5); // 导航栏应该展示的页码数(如 3 4 5 6 7 ) int[] navigatepageNums = pageInfo.getNavigatepageNums(); }finally { sqlSession.close(); } } /** Actor{ActorId=16, ActorChineseName='李雪健', ActorOriginName='Xuejian Li', ActorGender='男'} Actor{ActorId=17, ActorChineseName='陈道明', ActorOriginName='Daoming Chen', ActorGender='男'} Actor{ActorId=18, ActorChineseName='杰森.弗莱明', ActorOriginName='Jason Flemyng', ActorGender='男'} Actor{ActorId=19, ActorChineseName='马修.麦康纳', ActorOriginName='Matthew McConaughey', ActorGender='男'} Actor{ActorId=20, ActorChineseName='查理.汉纳姆', ActorOriginName='Charlie Hunnam', ActorGender='男'} **/
Mybatis批量处理Sql
在学习动态sql:foreach时, 曾介绍过利用拼接Sql
的方式批量处理Sql语句,但是数据库有时并不支持过长的语句.
其实, 我们可以在创建SQLSession时, 指定ExecutorType=Batch
来批量处理Sql
-
批处理方式
@Test public void test20() throws IOException { SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory(); SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH); long start = System.currentTimeMillis(); try { StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); for (int i = 0; i < 100; i++) { studentMapper.insertStudent(new Student("测试","male",18)); } sqlSession.commit(); long end = System.currentTimeMillis(); System.out.println("执行时间: " + (end - start) + "ms"); }finally { sqlSession.close(); } }
此方式在commit之后才执行sql, 之前一直在准备参数 [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Preparing: INSERT INTO student(name, gender, age) VALUES (?, ?, ?) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer) 执行时间: 3642ms
-
原始方式
[main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Preparing: INSERT INTO student(name, gender, age) VALUES (?, ?, ?) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] <== Updates: 1 [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Preparing: INSERT INTO student(name, gender, age) VALUES (?, ?, ?) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] ==> Parameters: 测试(String), male(String), 18(Integer) [main] [com.shy.dao.StudentMapper.insertStudent]-[DEBUG] <== Updates: 1 执行时间: 7153ms
Mybatis 类型处理器
类型处理器, 即TypeHandler
,用来将JDBC
类型(如varchar
)和Java
类型(如String
)相映射, 完成类型的转换,从而将字段值封装到JavaBean
中.
即可以自定义数据库字段与Java属性映射的方式
枚举类型的TypeHandler
Enum
默认使用的TypeHandler是EnumTypeHandler
// EnumTypeHandler源码
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
//...
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
// 在数据库中保存枚举的名称
if (jdbcType == null) {
ps.setString(i, parameter.name());
} else {
ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE);
}
//...
}
此外, 我们也可以将Enum的TypeHandler更改为EnumOrdinalTypeHandler
, 将在数据库中保存枚举的序号(ordinal)
<typeHandlers>
<!-- javaType 告知Handler要处理的java类型 com.shy.entity.Status为自定义的枚举类-->
<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="com.shy.entity.Status"/>
</typeHandlers>
// com.shy.entity.Status
public enum Status {
LOGIN, LOGOUT
}
// EnumOrdinalTypeHandler源码
public class EnumOrdinalTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
//...
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
// 在数据库中保存枚举的序号
ps.setInt(i, parameter.ordinal());
}
//...
}
自定义TypeHandler
// 假如我们更改Status枚举如下所示, 现在想要在数据库中保存状态码code, 而不是名称/序号.
// 并且希望通过数据库中的code,返回对应的枚举对象
// 我们就需要借助自定义TypeHandler
public enum Status {
LOGOUT(404,"登出"), LOGIN(200,"登录");
private Integer code;
private String msg;
Status(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
// 根据状态码返回枚举对象
public static Status getStatusByCode(Integer code){
if(code == 404) {
return LOGOUT;
}else{
return LOGIN;
}
}
}
自定义TypeHandler
, 可以实现TypeHandler接口
, 或者继承BaseTypeHandler
public class StatusEnumTypeHandler implements TypeHandler {
/**
* 定义Java类型如何保存进数据库
*/
@Override
public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
Status status = (Status) parameter;
ps.setString(i,status.getCode().toString()); // 保存状态码, i为index
}
/**
* 定义数据库字段如何映射成Java类型
*/
@Override
public Object getResult(ResultSet rs, String columnName) throws SQLException {
int code = Integer.parseInt(rs.getString(columnName));
// 根据状态码返回Status对象
Status status = Status.getStatusByCode(code);
return status;
}
@Override
public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
int code = Integer.parseInt(rs.getString(columnIndex));
Status status = Status.getStatusByCode(code);
return status;
}
@Override
public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = Integer.parseInt(cs.getString(columnIndex));
Status status = Status.getStatusByCode(code);
return status;
}
}
注册该TypeHandler
<typeHandlers>
<typeHandler handler="com.shy.typeHandler.StatusEnumTypeHandler" javaType="com.shy.entity.Status" />
</typeHandlers>
测试插入
@Test
public void test22() throws IOException {
SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
try(SqlSession sqlSession = sqlSessionFactory.openSession()){
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = new Student("Russell","male",22,Status.LOGIN);
studentMapper.insertStudent(student);
sqlSession.commit();
}
}
测试查询
@Test
public void test22() throws IOException {
SqlSessionFactory sqlSessionFactory = MybatisUtil.getSqlSessionFactory();
try(SqlSession sqlSession = sqlSessionFactory.openSession()){
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student Russell = studentMapper.selectStudentById(20);
System.out.println(Russell.toString());
}
}
// Student{id=20, name='Russell', gender='male', age=22, status=LOGIN}
Mybatis后记
此部分是在系统性学习Mybatis之后, 在实战当中遇到的问题,收获的知识
try-with-resource关闭会话
之前关闭会话时,使用try-finally
关闭会话, 但是此方式有可能会在finally
中出现异常
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
// .......
}finally {
sqlSession.close(); // 有可能会出现异常
}
在JDK7
之后, 引入了try-with-resource
语法糖, Mybatis
官方更推荐使用此方式关闭会话
try(SqlSession sqlSession = sqlSessionFactory.openSession()){ // 底层自动调用close关闭
// .......
}