对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。
ORM框架是连接数据库的桥梁,只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。
ORM解决的问题
1、避免了大量重复代码(创建连接、构建语句集、执行、结果集、流关闭)
2、资源统一管理(数据库信息)
3、表与对象的映射、表字段映射问题、表字段类型映射问题
4、SQL 耦合
mybatis配置
全局配置文件 mybatis-config.xml
<?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>
<!-- 定义外部配置,${jdbc.url}引用 -->
<properties resource="db.properties"></properties>
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
<!-- 控制全局缓存(二级缓存),默认 true-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当开启时,任何方法的调用都会加载该对象的所有属性。默认 false,可通过select标签的 fetchType来覆盖-->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- Mybatis 创建具有延迟加载能力的对象所用到的代理工具,默认JAVASSIST -->
<!--<setting name="proxyFactory" value="CGLIB" />-->
<!-- STATEMENT级别的缓存,使一级缓存,只针对当前执行的这一statement有效 -->
<!--
<setting name="localCacheScope" value="STATEMENT"/>
-->
<setting name="localCacheScope" value="SESSION"/>
</settings>
<!-- 简化拼写,将type的对象用alias代替:parameterType="blog" -->
<typeAliases>
<typeAlias alias="blog" type="com.mybatis.domain.Blog" />
</typeAliases>
<!-- <typeHandlers>
<typeHandler handler="com.mybatis.type.MyTypeHandler"></typeHandler>
</typeHandlers>-->
<!-- 对象工厂 -->
<!-- <objectFactory type="com.mybatis.objectfactory.ObjectFactory">
<property name="objName" value="666"/>
</objectFactory>-->
<!-- <plugins>
<plugin interceptor="com.mybatis.interceptor.SQLInterceptor">
<property name="objName" value="betterme" />
</plugin>
<plugin interceptor="com.mybatis.interceptor.MyPageInterceptor">
</plugin>
</plugins>-->
<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>
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
</configuration>
properties
可通过properties属性来实现引用配置文件
如果两个配置文件有同一个字段,优先使用外部配置文件的,${}引用
<properties resource="db.properties" />
<environments default="development">
<environment id="development">
<!-- 单独使用时配置成MANAGED没有事务 -->
<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>
可以直接引入外部配置文件,properties子元素中可以增加一些属性配置
<properties resource="db.properties">
<property name="" value="" />
<property name="" value="" />
</properties>
settings 全局参数设置
<setting name="logImpl" value="STDOUT_LOGGING" />
<!--执行器改成批量执行(攒够条数后,一次性执行)-->
<setting name="defaultExecutorType" value="BATCH"/>
typeAliases
将type的对象用alias代替,实际应用时:parameterType=“blog”
<typeAliases>
<typeAlias alias="blog" type="com.mybatis.domain.Blog" />
</typeAliases>
指定一个包名,将包中bean的首字母小写的非限定类名作为bean的别名
<typeAliases>
<package name="com.mybatis.domain"/>
</typeAliases>
typeHandlers 类型处理器
TypeHandlerRegistry中预先设置字段类型转换:
this.register((Class)String.class, (TypeHandler)(new StringTypeHandler()));
this.register((Class)String.class, JdbcType.CHAR, (TypeHandler)(new StringTypeHandler()));
this.register((Class)String.class, JdbcType.CLOB, (TypeHandler)(new ClobTypeHandler()));
this.register((Class)String.class, JdbcType.VARCHAR, (TypeHandler)(new StringTypeHandler()));
this.register((Class)String.class, JdbcType.LONGVARCHAR, (TypeHandler)(new ClobTypeHandler()));
this.register((Class)String.class, JdbcType.NVARCHAR, (TypeHandler)(new NStringTypeHandler()));
this.register((Class)String.class, JdbcType.NCHAR, (TypeHandler)(new NStringTypeHandler()));
this.register((Class)String.class, JdbcType.NCLOB, (TypeHandler)(new NClobTypeHandler()));
objectFactory 对象工厂
DefaultObjectFactory类基于反射,获取对象实例
<objectFactory type="com.mybatis.objectfactory.ObjectFactory">
<property name="" value=""/>
</objectFactory>
plugins 插件
<plugins>
<plugin interceptor="com.mybaits.interceptor.SQLInterceptor">
<property name="" value="" />
</plugin>
<plugin interceptor="com.mybaits.interceptor.MyPageInterceptor">
</plugin>
</plugins>
在映射语句执行之前进行拦截
Environments 环境配置
用来管理数据库环境,nvironments可以包含多个,来适应多个环境的数据库
<environments default="development">
<environment id="development">
<!-- 单独使用时配置成MANAGED没有事务 -->
<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>
transactionManager 事务管理
JDBC:用JDBC管理事务
MANAGED:基于集成的容器来管理事务
dataSource 数据源
默认是带有连接池的数据源,同type=“POOLED”。type=“UNPOOLED”,不带有连接池,type=“JNDI”,采用服务器提供的JNDI技术实现。
注意:如果spring中也有定义,则覆盖mybatis。所以一般直接在spring中定义,除非单独使用mybatis。
Mappers 映射器
<mappers>
<mapper resource="com/ferao/mapper/UserMapper.xml"/>
</mappers>
<!--该方式绑定注册时,接口和它的Mapper配置文件必须同名,且它的Mapper配置文件必须在同一个包下-->
<mappers>
<mapper class="com.ferao.mapper.UserMapper" />
</mappers>
<!--该方式注入绑定时,接口和它的Mapper配置文件必须同名,且它的Mapper配置文件必须在同一个包下-->
<mappers>
<package name="com.ferao.mapper"></package>
</mappers>
mybatis应用
mapper中参数传递
parameterType(输入类型)
resultType(输出类型)
resultMap(映射实体类)
顺序传参法
#{}里面的数字代表传入参数的顺序
这种方法sql表达不直观,一旦顺序调整容易出错,不建议使用
public User selectUser(String name, int deptId);
<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{0} and dept_id = #{1}
</select>
@Param注解传参法
#{}里的名称对应注解@Param里的名称。
这种方法在参数不多的情况还是比较直观的,推荐使用
public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);
<select id="selectUser" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
Map传参法
#{}里的名称对应Map里的key
这种方法适合传递多个参数,且参数易变,需要灵活传递的情况,推荐使用
public User selectUser(Map<String, Object> params);
<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
Java Bean传参法
#{}里的名称对应User类的成员属性
这种方法直观,代码可读性强,业务逻辑处理方便,推荐使用
public User selectUser(User user);
<select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
useGeneratedKeys 插入后自动获取id
<insert id="insertSelective" parameterType="……" useGeneratedKeys="true" keyProperty="id">
resultType 输出类型
一般数据类型(单条)
<select id="getStuNameById" resultType="string">
select username from t_student where id = #{id}
</select>
JavaBean 类型(单条)
public Student getStuById(Integer id);
<select id="getStuById" resultType="student">
select * from t_student where id = #{id}
</select>
List类型(多条)
public List<Student> getAllStus();
<!-- 注意这里的 resultType 返回值类型是集合内存储数据的类型,不是 'list' -->
<select id="getAllStus" resultType="student">
select * from t_student
</select>
Map类型(单条key+value)
public Map<String, Object> getStuAsMapById(Integer id);
<select id="getStuAsMapById" resultType="map">
select * from t_student where id = #{id}
</select>
Map类型(单条key+obj)
<!-- 注意:id作为key -->
@MapKey("id")
Map<Integer, Student> getAllStusAsMap();
<!-- 注意:返回的是 student对象 -->
<select id="getAllStusAsMap" resultType="student">
select * from t_student where id = #{id}
</select>
Map类型(多条key+obj)
List<Map<String, Object>> getAllStuAsMapById(Integer id);
<select id="getAllStuAsMapById" parameterType="int" resultType="map">
select * from t_student
</select>
resultMap
<select id="getOrder" parameterType="int" resultType="com.jourwon.pojo.Order">
select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
</select>
select * from orders where order_id=#{id}
<resultMap type="com.jourwon.pojo.Order" id="orderResultMap">
<!–用id属性来映射主键字段–>
<id property="id" column="order_id">
<!–用result属性来映射非主键字段,property为实体类属性名,column为数据库表中的属性–>
<result property ="orderno" column ="order_no"/>
<result property="price" column="order_price" />
</reslutMap>
association 嵌套结果集
<resultMap id="" type="">
<!--设置主键时使用,使用此标签配置映射关系(可能不止一个) -->
<id column="" jdbcType="" property="" />
<result column="" jdbcType="" property=""/>
<!-- 集合中的property须为oftype定义的pojo对象的属性-->
<collection property="pojo的集合属性" ofType="集合中的pojo对象">
<id column="表主键字段" jdbcType="字段类型" property="pojo对象的主键属性" />
<result column="表字段" jdbcType="字段类型" property="pojo对象的属性" />
</collection>
</resultMap>
collection 嵌套结果集
<select id="findSportsInfoByEmpId" resultMap="empmap">
select e.*, s.*
from employee as e,sport as s
where e.eid=s.eid
and e.eid=#{eid}
</select>
<resultMap type="Employee" id="empmap">
<id property="eid" column="eid"/>
<result property="ename" column="ename"/>
<result property="epwd" column="epwd"/>
<result property="address" column="address"/>
<result property="tel" column="tel"/>
<!-- collection描述一对多的关系,ofType是集合所包含的类型,可以写完整Java类名或别名 -->
<collection property="sports" ofType="Sport">
<id property="sportId" column="sportid"/>
<result property="sportName" column="sportname"/>
<result property="sportScore" column="sportscore"/>
</collection>
</resultMap>
association 嵌套查询
<resultMap id="BlogWithAuthorQueryMap" type="com.mybatis.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.mybatis.domain.Author"
column="author_id" select="selectAuthor"/>
</resultMap>
<select id="selectAuthor" parameterType="int" resultType="com.mybatis.domain.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
动态SQL
if
<if test="subscribeOrderSource != null">
choose、when、otherwise
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<otherwise>
</otherwise>
</choose>
trim、where、set
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="bid != null">
bid,
</if>
<if test="name != null">
name,
</if>
</trim>
foreach
<foreach collection="list" item="blogs" index="index" separator=",">
( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
</foreach>
批量操作
批量操作时,如果数据量过大,发送的数据包太大,mybatis会报错。
Show VARIABLES like ‘%max_allowed_packet%’ 查看,默认4MB多的大小。
动态SQL批量操作
批量插入
<insert id="insertBlogList" parameterType="java.util.List">
insert into blog (bid, name, author_id)
values
<foreach collection="list" item="blogs" index="index" separator=",">
( #{blogs.bid},#{blogs.name},#{blogs.authorId} )
</foreach>
</insert>
批量修改
<update id="updateBlogList">
update blog set
name =
<foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
when #{blogs.bid} then #{blogs.name}
</foreach>
,author_id =
<foreach collection="list" item="blogs" index="index" separator=" " open="case bid" close="end">
when #{blogs.bid} then #{blogs.authorId}
</foreach>
where bid in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item.bid,jdbcType=INTEGER}
</foreach>
</update>
修改执行器类型
修改执行器类型,设置批量执行
<setting name="defaultExecutorType" value="BATCH"/>
JDBC批量操作的方式
public void testJdbcBatch() throws IOException {
Connection conn = null;
PreparedStatement ps = null;
try {
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mybatis?useUnicode=true&……, "root", "123456"); // 打开连接
ps = conn.prepareStatement("INSERT into blog values (?, ?, ?)");
for (int i = 1000; i < 101000; i++) {
Blog blog = new Blog();
ps.setInt(1, i);
ps.setString(2, String.valueOf(i)+"");
ps.setInt(3, 1001);
ps.addBatch(); // 放入客户端缓存
}
ps.executeBatch(); // 批量执行
conn.commit();
} catch (Exception se) {
se.printStackTrace();
} finally {
ps.close();
conn.close();
……
}
}
#、$
$方式不会对符号转义,不能防止SQL注入
$方式没有预编译,不会缓存
PrepareStatement(ps)、Statement
1.都是接口,ps继承statement。
2.Statement执行静态SQL,ps可以执行带入参的SQL
3.Ps的addBatch() 实现批量操作
4.Ps对于相似的SQL只编译一次(语句相同,入参不同),减少编译的次数。
5.Ps可以防止SQL注入。
模糊查询
like %
直接用%有SQL注入的风险。
like concat()
order_num like concat("%", #{orderNum}, "%")
bind标签
<select id="id" resultType="……">
<!-- bind:可以将OGNL表达式的值绑定到一个变量中,方便后来引用这个变量的值 -->
<bind name="bindName" value="'%'+eName+'%'"/>
SELECT * FROM emp
<if test="param!=null">
where ename like #{bindName}
</if>
/select>
自定义TypeHandler
声明MyTypeHandler类
<typeHandlers>
<typeHandler handler="com.mybatis.type.MyTypeHandler" />
</typeHandlers>
实现MyTypeHandler类
public class MyTypeHandler extends BaseTypeHandler<String> {
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
// 设置 String 类型的参数的时候调用,Java类型到JDBC类型
// 注意只有在字段上添加typeHandler属性才会生效
// insertBlog name字段
System.out.println("---------------setNonNullParameter1:"+parameter);
ps.setString(i, parameter);
}
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 根据列名获取 String 类型的参数的时候调用,JDBC类型到java类型
// 注意只有在字段上添加typeHandler属性才会生效
System.out.println("---------------getNullableResult1:"+columnName);
return rs.getString(columnName);
}
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
// 根据下标获取 String 类型的参数的时候调用
System.out.println("---------------getNullableResult2:"+columnIndex);
return rs.getString(columnIndex);
}
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
System.out.println("---------------getNullableResult3:");
return cs.getString(columnIndex);
}
}
xml中引用MyTypeHandler
#{name,jdbcType=VARCHAR,typeHandler=com.mybatis.type.MyTypeHandler}
自定义对象工厂
声明ObjectFactory
<objectFactory type="com.mybatis.objectfactory.ObjectFactory">
<property name="objname" value="666"/>
</objectFactory>
实现ObjectFactory
public class ObjectFactory extends DefaultObjectFactory {
@Override
public Object create(Class type) {
System.out.println("创建对象方法:" + type);
if (type.equals(Blog.class)) {
Blog blog = (Blog) super.create(type);
blog.setName("object factory");
blog.setBid(1111);
blog.setAuthorId(2222);
return blog;
}
Object result = super.create(type);
return result;
}
}
Mapper映射器的继承
public interface BlogMapperExt extends BlogMapper
<mapper namespace="com.mybatis.mapper.BlogMapperExt"
extends = 'com.mybatis.mapper.BlogMapper'>
mybatis体系结构与工作原理
工作流程
解析配置文件
Mybatis启动时,解析全局配置文件、映射器文件,解析成configuration对象。
提供操作接口
mybatis在应用程序和数据库中间。与数据库之间的一次连接(会话),就是一个SqlSession对象。
SqlSession对象由SqlSessionFactory会话工厂创建。
Builder负责创建SqlSessionFactory。
Mybatis是对JDBC的封装,其底层一定会有JDBC的核心对象,比如执行SQL的Statement,结果集ResultSet。
SQL执行
SqlSession持有一个Executor对象,用来封装对数据库的操作。
在执行器Executor执行query或者update操作的时候我们创建一系列的对象,来处理参数、执行SQL、处理结果集,这里我们把它简化成一个对象: StatementHandler,可以把它理解为对Statement的封装,在阅读源码的时候我们再去了解还有什么其他的对象。
包结构(21个包)
架构模型
接口层
接口层是我们打交道最多的,核心对象是sqlsession,它是上层应用于mybatis之间的桥梁。Sqlsession定义了很多对数据库的操作方法。
接口层负责接收请求,并调用核心层的响应模块来完成具体的数据库操作。
核心层
与数据库操作相关的动作都在这一层完成。
1.将接口入参解析、映射成JDBC类型。
2.解析XML文件中的SQL语句(插入参数、生产动态SQL)
3.执行SQL语句。
4.处理结果集,映射成Java对象。
基础层
mybatis缓存
ORM框架一般都会提供缓存,从而提高查询效率,减少数据库压力。
Mybatis和Hibernate一样,也有一级缓存、二级缓存,并且预留了集成第三方缓存的接口。
Cache
Mybatis的缓存类都在cache包里,一个Cache接口,PrepetualCache实现类。
PrepetualCache
PrepetualCache是用HashMap实现的。
PrepetualCache是由SqlSession的Executor(执行器)维护的。
BaseExecutor
DefaultSqlSession
装饰器类
PrepetualCache是装饰器模式中的基础实现类,又叫基础缓存。
Decorators包内都是缓存装饰类,可以提供很多额外的功能,比如:回收策略、日志记录、定时刷新等等。
实际场景中,基础缓存会被装饰四五层。
缓存分类
可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。
一级缓存(本地缓存)
Mybatis的一级缓存是在会话(sqlsession)层面进行的,由Executor执行。
一级缓存的开关
默认开启
<!-- 关闭一级缓存 -->
<setting name="localCacheScope" value="STATEMENT"/>
STATEMENT:将一级缓存的作用域限于单次执行的单个SQL,等价于关闭。
一级缓存适用范围
同一个会话中,多次执行相同的SQL,会直接从内存中取缓存结果,不访问数据库。
不同的会话中,即使SQL相同,也不会使用一级缓存。
一级缓存验证
首先必须关闭二级缓存:localCacheScope 设置为SESSION
<setting name="localCacheScope" value="SESSION"/>
@Test
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper0 = session1.getMapper(BlogMapper.class);
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper0.selectBlogById(1));
System.out.println("第二次查询,相同会话,获取到缓存了吗?");
System.out.println(mapper1.selectBlogById(1));
System.out.println("第三次查询,不同会话,获取到缓存了吗?");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
结论:
1.“mapper0.selectBlogById(1)”进入数据库查询,mapper1 和mapper0 源于同一个session1,所以“mapper1.selectBlogById(1)”直接从缓存中取值。
2.mapper2 源于session2所以哪怕SQL一样,“mapper2.selectBlogById(1)”不会从缓存取值。
源码分析
get
put
update(delete)
query方法中(见上 get),flushCache = true 也会清空缓存。
Mapper文件中,的属性flushCache 默认true,所以会清空缓存。但是默认false,所以不请客缓存。
二级缓存
一级缓存不能跨会话共享,不同的会话之间对于相同数据肯能有不一样的缓存,以至于在多个会话、分布式场景下,会存在直接返回缓存中过时数据的问题,所以就需要工作范围更广的二级缓存了。
二级缓存的范围是namespace级别(每个mapper有自己的namespace),可以被多个SqlSession共享,生命周期和应用同步。
二级缓存取不到时,才去一级缓存取。
CachingExecutor
二级缓存由CachingExecutor维护,CachingExecutor是Executor的装饰器类。
二级缓存的开启方式
mybatis-config.xml(默认开启)
<!-- 控制全局缓存(二级缓存),默认 true,false可以关闭-->
<setting name="cacheEnabled" value="true"/>
Mapper.xml文件中配置标签
<cache
type="org.apache.ibatis.cache.impl.PerpetualCache"
size="1024" <!--最多缓存对象个数,默认1024-->
eviction="LRU" <!--回收策略-->
flushInterval="120000" <!--自动刷新时间(ms),默认调用时刷新-->
readOnly="false"/> <!--改为true,可读写(对象必须支持序列化),默认false(安全)-->
注意:cache标签如果没有的话,虽然会进入CachingExecutor类,但在判断if (ms.isUseCache() && resultHandler == null) 时,false,从而导致二级缓存不生效。
useCache=”false”关闭缓存
<select id="" resuleMap="" userCache="false">
二级缓存验证
二级缓存绑定事务
@Test
public void testCache() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session1 = sqlSessionFactory.openSession();
SqlSession session2 = sqlSessionFactory.openSession();
try {
BlogMapper mapper1 = session1.getMapper(BlogMapper.class);
System.out.println(mapper1.selectBlogById(1));
// 事务不提交的情况下,二级缓存会写入吗?
session1.commit();
System.out.println("第二次查询");
BlogMapper mapper2 = session2.getMapper(BlogMapper.class);
System.out.println(mapper2.selectBlogById(1));
} finally {
session1.close();
}
}
注意:如果没有session1.commit();“mapper2.selectBlogById(1)”不会从二级缓存取数据。事务提交之后,才会把结果写入二级缓存。
多个namespace共享一个二级缓存
<cache-ref namespace="com.mybatis.mapper.BlogMapperExt"/>
依赖Redis二级缓存
将type替换成第三方的引用
redis会自动的将Sql+条件+Hash等当做key值,而将查询结果作为value。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<cache type="org.mybatis.caches.redis.RedisCache"
eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
适用场景
1.查询为主,比如历史交易、历史订单的查询。(所有的增删改都会刷新二级缓存,导致二级缓存失效)。
2.多个namespace针对同一个表操作的场景,如果一个namespace刷新的缓存,其他的没有,会出现读到脏数据的情况,所以只适用于一个Mapper操作单表的情况。
源码分析
mybatis插件
自定义插件
实现Interceptor接口
(1)实现Interceptor接口,
(2)重写intercept、plugin、setProperties方法,其中intercept是最关键的,包含拦截的逻辑、需要增强的功能
(3)拦截器上添加注解。指定需要拦截的对象、方法、参数。
@Intercepts({ @Signature(
type = StatementHandler.class,
method = "query",
args = { Statement.class, ResultHandler.class}) })
public class SQLInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = statementHandler.getBoundSql();
String sql = boundSql.getSql();
System.out.println("获取到SQL语句:"+sql);
try {
return invocation.proceed();
}finally {
long endTime = System.currentTimeMillis();
System.out.println("SQL执行耗时:" + (endTime-startTime) +"ms");
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
String columnName= properties.getProperty("columnName");
// System.out.println("获取到的参数:"+ columnName);
}
}
插件注册
Mybatis-config.xml文件中注册插件,配置属性
解析注册
Mybatis启动时,扫描标签,把所有插件注册到configuration对象的InterceptorChain
XMLConfigBuilder解析
InterceptorChain 保存
架构体系
注意:多个插件时,插件的配置顺序()与执行顺序相反。
关键对象-作用
Interceptor接口 自定义拦截
InterceptorChain 存放插件的容器
Plugin 对象,提供创建代理类的方法
Invocation 对被代理对象的封装
源码分析
先二级缓存装饰?还是先插件代理?
Executor对象可以被二级缓存装饰,再插件中有需要被代理,那么这两者的先后关系是怎样的?
DefaultSqlSessionFactory.openSessionFromDataSource()
构建executor Configuration.newExecutor()
executor在构建的过程中,先创建基本类型,再二级缓存装饰,最后插件拦截CachingExecutor。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) { // 创建基本类型
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor); // 二级缓存装饰
}
executor = (Executor) interceptorChain.pluginAll(executor);// 插件拦截,创建代理类
return executor;
}
创建代理类 InterceptorChain.pluginAll
pluginAll方法中,通过遍历interceptors (插件集),基于interceptor.plugin(target)实现对target的代理(递归)
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); // 创建代理对象
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
interceptor.plugin
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance( // 代理
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
// 调用自己定义的插件类,实现自己的拦截逻辑
}
return method.invoke(target, args); // 实现已有的逻辑
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
分页插件 PageInterceptor
针对不同数据库,有不同的实现
PageHelper.startPage
PageHelper是PageMethod子类
PageMethod.startPage()中setLocalPage(page);
PageMethod.setLocalPage(page)
在当前线程中创建LOCAL_PAGE对象,并将page信息存放在LOCAL_PAGE中
PageHelper.getLocalPage()
dialect.getPageSql中,通过getLocalPage()从LOCAL_PAGE中获取page对象。
插件适用场景
spring、mybatis整合
步骤
Pom依赖
<!--mybatis 和Spring整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.4</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
applicatioContext.xml配置sqlsessionFactorybean
<!-- 在Spring启动时创建 sqlSessionFactory -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation"
value="classpath:mybatis-config.xml"></property>
<property name="mapperLocations"
value="classpath:mapper/*.xml"></property>
<property name="dataSource" ref="dataSource"/>
</bean>
配置扫描Mapper接口的路径
<mybatis-spring:scan #base-package="com.mybatis.crud.dao"/>
或
<bean id="mapperScanner"
class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.mybatis.crud.dao"/>
</bean>
或
@MapperScan("com.t3.ts.maintain.biz.center.mappers")
创建会话工厂 sqlSessionFactory
InitializingBean接口
实现InitializingBean接口,重写afterPropertiesSet()方法,创建sqlSessionFactory 实例。
@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
FactoryBean接口
实现FactoryBean接口,重新getObject()方法,用户自定义实例化Bean的逻辑。
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
ApplicationListener接口
实现ApplicationListener接口,让sqlSessionFactory有能力监控应用发出的事件通知。
监听ContextRefreshedEvent(上下文刷新事件),会在spring容器加载完之后执行。
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (failFast && event instanceof ContextRefreshedEvent) {
// fail-fast -> check all statements are completed
this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
}
}
创建会话
DefaultSqlSession不能直接拿来用,非线程安全。
SqlSessionTemplate
线程安全,可以在所有DAO层共享一个实例(默认单例)。
基于代理对象sqlSessionProxy 实现调用
SqlSessionTemplate
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance( // 反射
SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class },
new SqlSessionInterceptor());
}
SqlSessionInterceptor
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy,
Method method, Object[] args) throws Throwable {
/* 每次获取一个新的sqlSession ,保证线程安全 */
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, // 执行器类型
SqlSessionTemplate.this.exceptionTranslator); // 异常解析器
try {
Object result = method.invoke(sqlSession, args);// 基于sqlSession 调用增删改查
if (!isSqlSessionTransactional(sqlSession,
SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
SqlSessionTemplate的实例获取方式
继承SqlSessionDaoSupport类,getSqlSession()获取SqlSessionTemplate的实例。
SqlSessionDaoSupport.class
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
public SqlSession getSqlSession() {
return this.sqlSession;
}
/**
* {@inheritDoc}
*/
@Override
protected void checkDaoConfig() {
notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}
应用
public class BaseDao extends SqlSessionDaoSupport {
public Object selectOne(String statement) {
return getSqlSession().selectOne(statement);
}
Mapper接口扫描注册
然而在实际应用中,只声明了一个Mapper接口,并将其注入到DAO层,那么很显然,当Mapper接口在spring容器中注册时,必然做了一些其他的操作。
ClassPathMapperScanner.class
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 扫出所有的mapper class文件
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
……
// beanClass 设置为MapperBeanClass.class
definition.setBeanClass(this.mapperFactoryBean.getClass());
……
}
MapperFactoryBean.class
public class MapperFactoryBean<T>
extends SqlSessionDaoSupport implements FactoryBean<T> {
……
@Override
public T getObject() throws Exception { // 代理类
//获取SqlSessionTemplate实例
return this.getSqlSession().getMapper(this.mapperInterface);
}
@Override
public Class<T> getObjectType() {
return this.mapperInterface; // 获取Mapper接口:比如com.t3.ts.maintain.biz.center.mappers.MaintenanceAccidentPersonInjuredMapper
}
}
public class MapperRegistry {
……
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
}
MapperFactoryBean继承了SqlSessionDaoSupport接口。也就是说,所有的Mapper接口在容器中都被注册成一个支持泛型的MapperFactoryBean。
getObject()最终调用了
Configuration.getMapper(Class type, SqlSession sqlSession)
mapperProxy.invoke()
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[] { mapperInterface },
mapperProxy);
总结
1.SqlSessionTemplate作为SqlSession接口的实现类,其本质是个代理类。内部类SqlSessionInterceptor实现InvocationHandler接口,构建属性SqlSession sqlSessionProxy。
2.SqlSessionDaoSupport获取SqlSessionTemplate的实例。
3.MapperFactoryBean继承SqlSessionDaoSupport,并实现FactoryBean,在Mapper注入的时候,通过调用SqlSessionTemplate的getMapper()方法,获取JDK代理对象并注入。
4.执行Mapper接口的任意方法,会触发管理类MapperProxy,进入SQL处理的流程。