MyBatis
Posted antherd
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis相关的知识,希望对你有一定的参考价值。
Session
SqlSession 代表和数据库的一次会话,用完必须关闭
SqlSession 和 connection 一样都是非线程安全的。每次使用都应该去获取新的对象
mapper接口没有实现类,但是mybatis会为这个接口生成一个代理对象
两个重要配置文件:
- mybatis的全局配置文件,包含数据库连接信息,事务管理器信息等。系统运行环境信息
- sql映射文件:保存了每一个sql语句的映射信息,将sql抽取出来
多数据库支持
全局配置文件中配置databaseIdProvider属性
sql映射文件中,为sql语句标签添加databaseId属性
获取自增主键
mysql支持自增主键,自增主键值的获取,mybatis也是利用statement.getGeanreatedKeys() 获取主键值策略
sql映射文件中,为sql语句标签添加useGeneratedKeys="true"属性
keyProperty:指定对应的主键属性,也就是mybatis获取到主键值以后,将这个值封装给javaBean的哪个属性
Oracle不支持自增,使用序列来模拟自增
<!--
keyProperty:查出的主键值封装给javaBean的哪个属性
order=“BEFORE”:当前sql在插入sql之前运行
resultType:查出的数据的返回值类型
BEFORE运行顺序:
先运行selectKey查询id的sql,查出id值封装给javaBean的id属性
再运行插入的sql,就可以取出id属性对应的值
AFTER运行顺序:
...
-->
<selectKey keyProperty="id" order="BEFORE" resultType="Integer">
<!-- 编写查询主键的sql语句 -->
select EMPLOYEES_SQL.nextval from dual
</selectKey>
参数处理
单个参数:mybatis不会做特殊处理
#{参数名},取出参数值(其实参数名可以随便写)
多个参数:mybatis会做特殊处理,多个参数会被封装成一个map
默认
key:param1 … paramN,或者参数的索引(0…n)
value:参数值
直接填写参数名会报异常:org.apache.ibatis.binding.BindingException
可以在方法参数前添加@Param("") 注解
key:@Param指定的值
value:参数值
POJO
如果多个参数正好是业务逻辑的数据模型,我们就可以直接传入pojo
#{属性名}:取出传入的pojo的属性值
Map
如果多个参数不是业务模型中的数据,没有对应的pojo,为了方便,我们也可以传入map
#{key}:取出map中对应的值
TO
如果多个参数不是业务模型中的数据,但是经常要使用,推荐来编写一个TO(Transfer Object)数据传输对象
Page {
int index;
int size;
}
特别注意:如果是Collection(List、Set)类型或者是数组,也会特殊处理。也是把传入的list或者数组封装在map中
key:Collection(collection),如果是List还可以使用key(list),数组(array)
public Employee getEmpById(List<Integer> ids);
取值:取出第一个id的值:#{list[0]}
源码分析
names:{0=id, 1=lastName}:构造器的时候就确定了
确定流程
- 获取每个标了param注解的参数的@Param的值:id,lastName:赋值给name;
- 每次解析一个参数给map中保存信息:(key:参数索引,value:name的值)
name的值:
标注了param注解,注解的值
没有标注:
1. 全局配置:useActuralParamName(jdk1.8):name=参数名
2. name=map.size():相当于当前元素的索引
#{} 和 ${} 区别
区别:
#{}:是以预编译的形式,将参数设置到sql语句中,PreparedStatement;防止sql注入
${}:取出的值直接拼装在sql语句中,会有安全问题
select * from tbl_employee where id = ${id} and last_name = #{lastName}
Preparing:select * from tbl_employee where id = 2 and last_name = ?
大多情况中,我们取参数的值都应该去使用#{};
原生jdbc不支持占位符的地方我们就可以使用${}进行取值
比如分表:按照年份分表拆分
select * from ${year}_salary where xxx;
按字段排序
select * from tbl_employee order by ${f_name} ${order};
#{} 更丰富的用法:
规定参数的一些规则:
javaType,jdbcType,mode(存储过程),numericScale,resultMap,typeHandler,jdbcTypeName,expression
jdbcType通常需要在某些特定的条件下被设置:在我们数据为null的时候,有些数据库可能不能识别mybatis对null的默认处理。比如Oracle(报错):JdbcType OTHER:无效的类型:因为mybatis对所有的null都映射的是原生jdbc的OTHER类型
由于全局配置中:jdbcTypeForNull=OTHER;oracle不支持
- #{email, jdbcType=null}
- jdbcTypeForNull = NULL
select
返回list集合,resultType填写元素的类型
返回一条记录的map:key:列名,value:对应的值。resultType填写map
多条记录封装一个map:Map<Integer,Employee>:key:这条记录的主键,value:记录封装后的javaBean。resultType填写元素的类型,在接口方法上指定封装map的时候key @MapKey("id")
自动映射
- 全局设置
- autoMappingBehavior默认是PARTIAL,开启自动映射的功能。唯一的要求是列名和javaBean属性名一致
- 如果autoMappingBehavior设置为null则会取消自动映射
- 数据库字段命名规范,POJO属性符合驼峰命名法,如 A_COLUMN -> aCloumn,我们可以开启自动驼峰命名规则映射功能,mapUnderscoreToCamelCase=true
- 自定义resultMap,实现高级结果集映射
查询员工的同时查出员工的部门信息
-
联合查询:级联属性封装结果集
<result column="dept_name" property="dept.departmentName" />
-
association指定联合javaBean对象(一对一),property指定那个属性是联合的对象,javaType指定属性的类型
<association property="dept" javaType="..."> <id column="id" property="id" /> ... </association>
使用association进行分步查询:select:表明当前属性是调用select指定的方法查出的结果,column:指定将哪一列的值传给这个方法,并封装给 property
<association property="dept" select="com.antherd.mybatis.dao.DepartmentMapper.getDeptById" column="d_id"> <id column="id" property="id" /> ... </association>
可以使用延迟加载:每次查询Employee对象的时候,都将一起查询起来。部门信息在我们使用的时候再去查询。分段查询的基础之上加上两个全局配置:lazyLoadingEnable = true,aggressiveLazyLoading = false
-
collection定义关联集合类型的属性封装规则(一对多)
<collection property="emps" ofType="..."> <id cloumn="eid" property="id" /> ... </collection>
collection:定义关联集合类型的属性的封装规则
ofType:指定集合里面元素的类型使用collection进行分步查询:
<collection property="emps" select="com.antherd.mybatis.dao.EmployeeMapperPlus.getEmpsByDid" column="id"> </collection>
拓展:多列值传递给关联查询
将多列的值封装map传递
column="{key1=columnName1, key2=columnName2}"fetchType属性:表示使用延迟加载
- lazy:延迟
- eager:立即
-
discriminator鉴别器:mybatis可以使用discriminator判断某列的值,然后根据某列的值改变封装行为
封装Employee:
如果查出的是女生,就把部门信息查询出来,否则不查询
如果是男生,把last_name这一列的值赋值给email<discriminator javaType="string" column="gender"> <case value="0" resultType="com.antherd.mybatis.bean.Employee"> <association ...> </case> <case value="1" resultType="com.antherd.mybatis.bean.Employee"> <id ...> <result ...> </case> </discriminator>
动态sql
-
if:判断
拼装问题解决:- where 后面加 1=1
- 使用where标签把所有的查询条件包裹在内,只会去掉开头的and或or
- 使用trim标签把所有条件包裹在内。
- prefix:trim标签体中是整个字符串拼串后的结果,prefix给拼串后的整个字符串加一个前缀,如prefix = ”where“
- prefixOverrides:前缀覆盖,去掉整个字符串前面多余的字符串
- suffix:后缀
- suffixOverrides:后缀覆盖,去掉整个字符串后面多余的字符,如prefix = ”and“
-
choose:选择
<choose><when></when></choose>
-
trim:去除
-
foreach:循环,open:遍历所有结果拼接一个开始的字符,close:遍历所有结果拼接一个结束的字符,sqparator:元素之间的分隔符,index:遍历list的时候是索引,遍历map的时候表示map的key,item表示map的值
-
set:去除多余”,“
数据库连接属性中添加 allowMultiQueries=true允许一条语句中,使用”;“来分割多条查询
Oracle数据库批量保存:
Oracle不支持values (), (), ()
Oracle支持批量方法
- 多个insert放在begin - end里面
- 利用中间表
insert into employees(id, name, email)
select id, name email from (
select "1" id, "name1" name, "email1" email from dual
union
select "2" id, "name2" name, "email2" email from dual
...
)
两个内置参数
mybatis默认还有两个内置参数:
_parameter:代表整个参数
单个参数:_parameter就是这个参数
多个参数:参数会被封装为一个map:_parameter就是代表这个map
_databaseId:如果配置了databaseIdProvider标签
_databaseId就是代表当前数据库的别名oracle
<select id="getEmpsTestInnerParamter" resultType="com.antherd.mybatis.dao.Employee">
<if test="_databaseId=='mysql'">
select * from tbl_employee
<if test="_parameter !=null">
where last_name = #{_parameter.lastName}
</if>
</if>
<if test="_databaseId=='oracle'">
select * from tbl_employee
</if>
</select>
bind:可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值
<bind name="_lastName" value="'%' + lastName + '%'" />
抽取可重用sql片段,方便后面引用
- sql抽取: 经常将要查询的列名,或者插入用的列名抽取出来方便引用
- include来引用已经抽取的sql
- include还可以自定义一些property,sql标签内部就能使用自定义属性
include-property取值的正确方式${prop}, #{不能使用这种方式}
<sql id="insertColumn">
id, name, email
</sql>
<include refid="insertColumn">
<property name="testColumn" value="1234" />
</include>
缓存
MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率
MyBatis系统中默认定义了两级缓存
一级缓存 和 二级缓存(全局缓存)
- 一级缓存(本地缓存):与数据库同一次会话期间查询到的数据会放在本地缓存中。以后如果需要获取相同的数据,直接从缓存中拿,没必要再去查询数据库
- 二级缓存(全局缓存):工作机制:1. 一个会话,查询一条数据,这个数据就会被放在当前会话的一级缓存中 2. 如果会话关闭,一级缓存中的数据会被保存到二级缓存中,新的会话查询信息,就可以参照二级缓存中的内容 3. sqlSession EmployeeMapper -> Employee DepartmentMapper -> Department 不同namespace查出的数据会放在自己对应的缓存中(map中)效果:数据会从二级缓存中获取,查出的数据都会被默认先放在一级缓存中,只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中
- 默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启。SqlSession级别缓存是一个Map
- 二级缓存需要手动开启和配置,它是基于namespace级别的缓存。基于namespace对应一个二级缓存。
- 为了提高拓展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存
一级缓存失效情况(没有使用到当前一级缓存的情况,效果就是,还需要再向数据库发出查询)
- sqlSession不同
- sqlSession相同,查询条件不同(当前一级缓存中还没有这个数据)
- sqlSession相同,两次查询之间执行了增删改操作(这次增删改可能对当前数据有影响)
- sqlSession相同,手动清除了一级缓存(缓存清空)
二级缓存使用:
- 开启全局二级缓存配置:cacheEnabled=true
- 在mapper.xml的mapper标签中添加cache标签配置二级缓存
cache标签参数 - POJO需要实现序列化接口
- eviction(缓存的回收策略):LRU(默认)、FIFO、SOFT、WEAK
- flushInterval(缓存刷新间隔):缓存多长时间清空一次,默认不清空,设置一个毫秒值
- readOnly(是否只读):true(只读):mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis为了加快获取速度,直接就将数据在缓存中的引用交给用户。不安全,速度快。 false(非只读):mybatis觉得获取的数据可能会被修改。mybatis会利用序列化&反序列化的技术克隆一份新的数据给用户。安全,速度慢
- size(缓存存放多少元素):
- type(指定自定义缓存的全类名):实现Cache接口即可
缓存相关的设置/属性:
- cacheEnable=true/false:关闭缓存(二级缓存关闭)(一级缓存一直可用)
- 每个select标签都有useCache=“true”:false:不使用缓存(一级缓存依然使用,二级缓存不使用)
- 每次增删改标签的:flushCache=“true”:(一级二级都会清除)增删改执行完成后就会清除缓存。一级缓存清空,二级也会被缓存清空
- 查询标签:flushCache=“false”:如果flushCache=true,每次查询之后都会清空缓存;缓存是没有被使用的
- sqlSession.clearCache():只是清除当期session的一级缓存
- localCacheScope:本地缓存作用域(一级缓存SESSION):当前会话的所有数据保存在会话缓存中。STATEMENT:可以禁用一级缓存
Cache接口提供了实现类,方便第三方缓存实现具体缓存细节
第三方缓存整合原理&ehcache
mybatis 提供了与第三方整合的工具包
包含redis-cache、spring、ehcache-cache等
mapper中开启cache
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
ehcache.xml
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="D:\\ehcache" />
<defaultCache
maxElementInMemory="10000"
maxElementsOnDisk="100000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
tiemToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
引用缓存:namespace 指定和哪个名称空间缓存一致
<cache-ref namespace="" />
第三方缓存整合:
1. 导入第三方缓存包即可
2. 导入与第三方缓存整合的适配包:官方有
3. mapper.xml 中使用自定义缓存
运行原理
- 获取sqlSessionFactory对象
解析文件中的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession
注意:【MappedStatement】代表一个增删改查的详细信息 - 获取SqlSession对象
返回一个DefaultSQLSession对象,包含Executor和Configuration
这一步会创建Executor对象 - 获取接口的代理对象(MapperProxy)
getMapper,使用MapperProxyFactory创建一个MapperProxy的代理对象 代理对象里面包含了DefaultSqlSession(Executor) - 执行增删改查方法
总结:
- 根据配置文件(全局,sql映射)初始化出Configuration对象
- 创建一个DefaultSqlSession对象
里面包含Configuration以及Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor) - DefaultSqlSession.getMapper():拿到Mapper接口对应的MapperProxy
- MapperProxy里面有DefaultSqlSession
- 执行增删改查方法
- 调用DefaultSqlSession的增删改查
- 会创建一个StatementHandler对象(同时也会创建出ParameterHandler和ResultSetHandler)
- 调用StatementHandler预编译参数以及设置参数值
使用ParameterHandler来给sql设置参数 - 调用StatementHandler的增删改查方法
- ResultSetHandler封装结果
注意:
四大对象每个创建的时候都有一个interceptorChain.pluginAll流程
获取到所有的Interceptor(拦截器),返回target包装后的对象
插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面向切面)
我们的插件可以为四大对象创建出代理对象
代理对象就可以拦截到四大对象的每一个执行
插件编写:
- 编写Interceptor的实现类
- 使用@Intercepts注解完成插件签名
- 将写好的插件注册到全局配置文件中<plugins / >标签
以上是关于MyBatis的主要内容,如果未能解决你的问题,请参考以下文章
SSM-MyBatis-05:Mybatis中别名,sql片段和模糊查询加getMapper
MYBATIS05_ifwherechoosewhentrimsetforEach标签sql片段