MyBatis学习笔记(二)

接笔记一继续,还剩下一些知识点,本篇包含缓存、高级查询、延迟加载(lazy)、插件的使用
当然,仅靠这两篇也是不全的(注解就没说),只能说个大概,后面做 SSM 的时候还会再进行补充吧….

动态SQL

MyBatis 的强大特性之一便是它的动态 SQL。
如果你有使用 JDBC 或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
MyBatis 采用功能强大的基于 OGNL 的表达式来消除其他元素。

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

下面就来详细说说到底怎么用,还是那句话,官方有很详细的文档,详细了解还是去官方比较好,飞机:http://www.mybatis.org/mybatis-3/zh/dynamic-sql.html
这里就是演示最基本的使用,首先是 if,最常用的就是根据传入的参数来决定 sql :

1
2
3
4
5
6
7
8
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null and title != ''">
AND title like #{title}
</if>
</select>

熟悉 OGNL 的就不用多说了,都看得懂,忽然发现我对 OGNL 并不是多了解o( ̄▽ ̄)ゞ)) ̄▽ ̄)o,抽时间单独搞一篇!;
需要提一下的是,OGNL 中可以直接使用 Java 中的方法
比如 <if test="title != null and ''.equals(title.trim())"> 是合法的,我以前有说过么??大概


然后继续向下看,第二个 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>

如果都没匹配到就会执行 otherwise 标签的内容,如果没有定义这个标签就默认什么也不做,当匹配到第一个后就会 break,即使后面的也匹配但是会按照第一个来确定最终的语句;


第三个是 trim (where, set) ,有些时候 WHERE 后面没有默认的语句,就是说条件是通过动态 SQL 确定的,但是如果都不匹配那么 WHERE 语句后面就是空了;比较业余的做法就是加 1=1 这种恒等条件;
MyBatis 有一个简单的处理,这在 90% 的情况下都会有用

1
2
3
4
5
6
7
8
9
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="title != null">
AND title like #{title}
</if>
</where>
</select>

总结一下:
如果 where 标签内部有 sql 语句那么就会在前面自动添加 where 关键字,如果标签内是空那么就不做处理;
更 NB 的是,如果内部的语句是 “AND”或“OR”开头的,会自动处理它们…

再来看 set,也能猜到了,是用在 update 语句中的 set 关键字哪里,就是处理逗号的问题了

1
2
3
4
5
6
7
8
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>

set 元素会动态前置 SET 关键字,同时也会消除无关的逗号,嗯~很赞

然后,trim 标签其实可以代替它们两个,可以用来做他们两个做不到的一些逻辑:

1
2
3
4
5
6
7
8
<!-- 和 where 标签等价 -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<!-- 和 set 标签等价 -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>

prefixOverrides 属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。
它带来的结果就是所有在 prefixOverrides 属性中指定的内容将被移除,并且插入 prefix 属性中指定的内容。
通俗说就是:如果判断 trim 里面有东西就在前面加 prefix 的值(还可以指定一个 suffix 值,就是在后面输出了),否则不输出;prefixOverrides 的意思就是判断标签里面的内容的最前面有没有设定的值,如果有就去除;suffixOverrides 就是判断最后有没有了,有就去除。


这样还剩最后一个 foreach ,从名字很明显看出这是用来遍历的,一般用在批量删除、更新等,也就是构建 IN 条件语句的时候

1
2
3
4
5
6
7
8
9
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
WHERE ID in
<foreach item="item" index="index" collection="list"
open="(" separator="," close=")">
#{item}
</foreach>
</select>

然后它贴心的提供了几个属性,比如在开始前加入 ( ,结束后加入 ),指定分隔符为 ,;index 是当前迭代的次数;
另外不要忘了使用 @Param 注解指定参数名哦,要不然 collection 可能会报 “404” 错误

一级缓存

对于缓存和 Hibernate 中的是差不多的,所以这一部分应该是相对比较轻松的;
MyBatis 中的一级缓存作用域也是 Session 级别的,如果执行的是相同的 SQL (相同的语句和参数)那么就会从缓存中获取;
同样的,一级缓存默认是开启的,并且你是关不掉的…..但是可以使用 sqlSession 的 sqlSession.clearCache() 方法清除缓存;
在同一个 Session 中,如果两句相同的 SQL 之间进行了 insert、update、delete 时,会刷新缓存,下次获取还是会重新从数据库中进行获取。

二级缓存

二级缓存的作用域是一个 mapper 的 namespace ;同一个 namespace 中查询 sql 是可以从缓存中命中的;
二级缓存是跨 session 的;
要使用二级缓存首先要开启,开启非常简单,在映射文件 mapper 中加入 <cache/> 标签即可,还有就是别忘了把实体进行序列化,要不然缓存的时候会出错

在全局配置文件中,settings 标签中有一个 cacheEnabled 的属性,该配置影响的所有映射器中配置的缓存的全局开关,默认是 true;所以默认情况下只需要配置下 <cache/> 标签就可以使用了;
但是如果全局配置文件中的 cacheEnabled 为 false,那么二级缓存依然是不可用的状态。

当然 cache 标签还可以设置很多属性,详细的说明还是看官方文档:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache
下面是一个比较全的栗子:

1
2
3
4
5
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新(默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新),存数结果对象或列表的 512 个引用(默认值是 1024),而且返回的对象被认为是只读的(默认 false),因此在不同线程中的调用者之间修改它们会 导致冲突。
可用的收回策略有:

  • LRU – 最近最少使用的:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
  • WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

默认的是 LRU。一般这个就够用了

使用第三方缓存

比如,都很熟悉的 EHCache、或者 Memcache(高性能 KV 缓存框架,目前被 Redis 所替代);
下面说 MyBatis 如何集成 EHcache ,处理依赖就不用说了,然后是编写配置文件: ehcache.xml ,或者选择集成在 MyBatis 中的配置文件中。
Ehcache 默认使用 CLASSPATH 根目录下的 ehcache.xml 作为配置文件,如果没找到,则使用 Jar 包下的 ehcache-failsafe.xml 作为配置文件,该配置文件提供了默认的简单配置;
关于它的配置就不说了,现在的功力还达不到…….不知道怎么配,然后就是在 MyBatis 中的映射文件中使用了:

1
2
3
4
5
6
7
8
9
10
11
<cache type="org.mybatis.caches.ehcache.EhcacheCache">
<property name="timeToIdleSeconds" value="3600"/>
<property name="timeToLiveSeconds" value="3600"/>
<property name="maxEntriesLocalHeap" value="1000"/>
<property name="maxEntriesLocalDisk" value="100000"/>
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>

<!-- 如果你的配置文件写在了外面,可以直接使用下面的其中一种 -->
<cache type="org.mybatis.caches.ehcache.LoggingEhcache"/>
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

使用外部配置文件需要注意的是文件名一定不要写错,ehcache.xml !!还有就是两种 type 的区别,第一种可以输出日志,第二种不可以。

一对一查询

使用 MyBatis 的好处是性能好,使用也相对简单,毕竟是一个比较轻量级的框架,当然是与 hibernate 相比的,但是相应的也给我带来了个问题,就是要写 SQL 语句了(如果你喜欢写,那就….)
首先来看第一种方案:
SQL 语句是必须的吗,就不说了,对于一对一可以使用 左连接的方式将其关联的表给查出来,但是这样一来返回的字段肯定比实体中的字段多,并且没有一个实体与其对应,所以可以创建一个新类,继承自原来的实体(第一个一的一方),然后扩展你所需要的其他属性。


第二种方案:
这种方式就和 hibernate 有点相似了,采用 OO 的思想,使用组合的方式,在原来的实体中引用另一个实体,这样一来,自动映射就不行了,需要手动指定了,一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
<resultMap type="Order" id="orderResultMap" autoMapping="true">
<id column="id" property="id"/>
<!--
association: 用于映射java对象
property:Order对象中的属性名(User对象)
javaType:属性的java类型(可以使用别名)
-->
<association property="user" javaType="User" autoMapping="true">
<!-- id:User对象的id -->
<id column="user_id" property="id"/>
</association>
</resultMap>

因为使用了 association(定义实体引用的对象类型),所以默认 autoMapping 是关闭的,需要手动开启,这样指定实体的 resultMap 就可以了

一对多查询

一对多的实现只有一种那就是 OO 的组合思想,继承是办不到的,和上面类似,需要在实体里加一个集合,比如 List ,然后使用泛型来约束多的一方的类型,就是 hibernate 中的一对多的写法啊;
然后来看看映射文件怎么写:

1
2
3
4
5
6
7
8
9
10
<resultMap type="Order" id="orderAndUserAndOrderDetailResultMap" autoMapping="true">
<id column="id" property="id"/>
<!--
javaType: 属性的 java 类型
ofType:集合中的对象类型
-->
<collection property="orderdetails" javaType="List" ofType="Orderdetail" autoMapping="true">
<id column="detail_id" property="id"/>
</collection>
</resultMap>

是的,无论什么时候,既然用 resultMap,那就起码得写个 id 吧,然后用 collection 定义集合,写好后下面就可以用了,这种情况下不用忘了 autoMapping ,默认是 false 的

多对多查询

本质上还是写映射文件,展示的就是在集合中还能够再套属性,就是说:

1
2
3
4
5
6
7
8
9
10
11
12
13
<resultMap type="Order" id="orderAndUserAndOrderDetailAndItemResultMap" autoMapping="true" 
extends="orderResultMap">
<!--
javaType: 属性的javae类型
ofType:集合中的对象类型
-->
<collection property="orderdetails" javaType="List" ofType="Orderdetail" autoMapping="true">
<id column="detail_id" property="id"/>
<association property="item" javaType="Item" autoMapping="true">
<id column="iid" property="id"/>
</association>
</collection>
</resultMap>

从上面的配置中,还可以得出一个结论,就是 resultMap 是可以继承的,支持这个特性就可以做到代码复用了;还是不要忘记 autoMapping 。
下面写一个示例的 sql 语句,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<select id="query...ByOrderNumber" resultMap="orderAndUser...ResultMap">
SELECT
o.*,
u.user_name,
u.name,
od.item_id,
od.total_price,
od.id detail_id,
i.id iid,
i.item_name,
i.item_price,
i.item_detail
FROM
tb_order o
LEFT JOIN tb_user u ON o.user_id = u.id
LEFT JOIN tb_orderdetail od ON od.order_id = o.id
LEFT JOIN tb_item i ON od.item_id = i.id
WHERE
o.order_number = #{orderNumber}
</select>

如果这样都能看得懂,那应该就没啥问题了,emmmm;
关于多对多我还是有点蒙的,订单和商品应该是多对多,但是这个体现方式我还需要消化消化

批量查询

在 JDBC 中,批量操作使用 addBatch 的方式就可以实现,但是在 MyBatis 中就只能….
想要做到批量查询在 MyBatis 中就只能写相应的 SQL 语句了,或者说拼相应的 SQL 语句;不同的数据库的 SQL 写法会有区别,比如在 MySQL 中:insert into tabName(a,b) values('A','B'),('D','E')
所以在 ParameterType 中直接接收一个 List 就行了,然后用 foreach 遍历出来就行了,注意它们直接有 , 所以要用上 foreach 的特性 separator 属性。

延迟加载

就是所谓的 lazy 懒加载,就是说,数据用的时候再加载;
要开启延迟加载,需要在主配置文件中的 settings 标签中加入两个开关:

1
2
3
4
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

lazyLoadingEnabled:延迟加载的全局开关, 特定关联关系中可通过设置fetchType属性来覆盖该项的开关状态。默认为 false;
aggressiveLazyLoading:当开启时,如果有多个延迟加载项调用其中一个都会加载,false 则按需加载,默认为 false (true in ≤3.4.1)

注意:懒加载需要使用动态代理技术(get 的时候需要执行查询逻辑),但是实体应该都是没有接口的,所以记得加入 Cglib 相关的依赖~

使用懒加载,Java 代码不需要进行更改,改的还是映射文件,栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<resultMap type="Order" id="lazyOrderResultMap" autoMapping="true">
<id column="id" property="id"/>
<!--
select:延迟加载时执行SQL的StatementId
column:指定关联的字段(传入的参数)
-->
<association property="user" javaType="User" select="queryUserById" column="user_id"/>
</resultMap>

<select id="queryUserById" parameterType="Long" resultType="User">
SELECT * FROM tb_user WHERE id = #{id}
</select>

<select id="lazyQueryOrderAndUserByOrderNumber" resultMap="lazyOrderResultMap">
SELECT * FROM tb_order WHERE order_number = #{orderNumber}
</select>

因为全局配置中已经开启了懒加载,所以默认 association 就会进行延迟加载,事前就要配置好

第三方插件(实现分页)

在 MyBatis 主配置文件里有个 插件(plugins)的说明,插件说白了其实就是拦截器,就是在原有的处理流程上加入自己的逻辑(相当于修改源代码的效果)。然后就以分页来作为栗子吧,分页其实就是使用 limit 来实现的;
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

从上到下可以理解为:拦截执行器的方法、拦截参数的处理、拦截结果集的处理、拦截Sql语法构建的处理;
关于自定义插件的步骤先挖坑吧,暂时可参考官方的文档嘛…..


然后来实现分页,使用果然开发的 PageHelper 就可以了,不重复造轮子(我也造不出来);因为是国人写的所以有详细的中文文档,使用也很简单,就不多说了,给个地址好了:
https://github.com/pagehelper/Mybatis-PageHelper
下面说个最简单的使用栗子,首先要保证在主配置文件中已经配置好了:

1
2
3
4
5
6
7
8
9
10
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectAll();
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
//测试PageInfo全部属性
//PageInfo包含了非常全面的分页属性
assertEquals(183, page.getTotal()); // 数据总数
assertEquals(19, page.getPages()); // 总页数
assertEquals(1, page.getFirstPage());

主配置文件的配置,这里按最简单的最基本的设置来:

1
2
3
4
5
6
7
8
9
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<!-- 设置方言 -->
<property name="helperDialect" value="mysql"/>
<!-- 查询总页数 -->
<property name="rowBoundsWithCount" value="true"/>
</plugin>
</plugins>

嘛,这样就差不多了,基本使用的话

自定义插件

使用自定义插件(拦截器)来实现一个批量分页功能,最终就是只需要定义普通的 SQL 语句,id 命名符合一定规范,传入相应的分页参数(对象形式)就可以返回相应的分页数据。
所使用的就是上面提到的 StatementHandler — 拦截Sql语法构建的处理。
首先来分析一下,除 SQL 语句和配置参数(如何分页,每页几条这样的)不同,其他大部分都是相同的,JSP 页面的重复代码可以使用自定义标签来实现复用,然后一行代码就搞定了;然后解决的就是拿到执行的 SQL 语句然后利用子查询拼成一个分页查询的 SQL 语句再替换回去,SQL 语句在配置文件中,由 MyBatis 进行处理,想要得到要么改源码,要么就是使用 拦截器(插件)了!
那么关键点就是:

  1. 要拦截住(拦截什么样的对象、对象的什么行为、什么时候拦截)
  2. 拦截下来做什么
  3. 做完后要交回主权

要实现在执行前完成 SQL 的替换(创建 statement 的时候),需要了解 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
55
56
57
58
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
public class PageInterceptor implements Interceptor {

private String test;

// 3. 处理拦截后要干什么,参数就是被拦截的对象
// 只有拦截成功的才会执行这个方法,相当于动态代理中的那个 InvocationHandler
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 通过 MetaObject 能达到使用反射获取属性的效果
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
// 获取配置文件中 SQL 语句的 ID,判断是不是要分页功能,通过 id 命名结尾带有 ByPage
String id = mappedStatement.getId();
if(id.matches(".+ByPage$")) {
BoundSql boundSql = statementHandler.getBoundSql();
// 原始的SQL语句
String sql = boundSql.getSql();
// 查询总条数的SQL语句
String countSql = "select count(*) from (" + sql + ")a";
Connection connection = (Connection)invocation.getArgs()[0];
PreparedStatement countStatement = connection.prepareStatement(countSql);
// 获取并填充 SQL 中的参数
ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler");
parameterHandler.setParameters(countStatement); // 填充
ResultSet rs = countStatement.executeQuery();

// 获取在 service 层调用时传入的参数,也就是配置文件里的 parameterType
Map<?,?> parameter = (Map<?,?>)boundSql.getParameterObject();
Page page = (Page)parameter.get("page");
if(rs.next()) {
page.setTotalNumber(rs.getInt(1));
}
// 改造后带分页查询的SQL语句
String pageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
// 将改造后的 SQL 塞回 statementHandler,这样就偷梁换日了
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
// 返回主权,也就是继续执行 prepare 方法,这样会执行我们换的 statementHandler
return invocation.proceed();
}

// 2. 负责拦截对象(请求)
// 参数为被拦截的对象,如果判断成功返回的是代理类,否则是拦截对象本身
// 这里相当于是拦截所有的要创建 statement 的对象
@Override
public Object plugin(Object target) {
System.out.println(this.test);
return Plugin.wrap(target, this);
}

// 1. 可以把注册时配置的参数加载进来
@Override
public void setProperties(Properties properties) {
this.test = properties.getProperty("test");
}
}

首先要实现 Interceptor 接口,也就是那三个方法,setProperties 用于注入配置的参数,plugin 负责拦截对象,intercept 负责拦截后的进一步处理(还需要判断下拦下的这个“人”是不是要执行某个“动作”);
注解确定了要拦截那个对象的那个方法,通过这个方法有什么参数来确定是那个重载,达到唯一的目的。
MyBatis 获取 statement 是在 StatementHandler 这个类(接口)中定义的,其中的 prepare 方法返回的就是 statement 对象;所以知道注解怎么写了吧。
Plugin.wrap() 方法中会根据注解来判断是不是需要拦截的对象,如果是就返回代理类,如果不是就返回拦截对象本身(放行);因为这个方法第二个参数传入了 this ,所以其实当调用代理类的时候,执行的就是在 Interceptor 中定义的 intercept 方法!
MetaObject 对象提供了一个 getValue 的方法可以让我们很方便的访问被包装对象的属性(即使是保护权限的),StatementHandler 有两个实现类,我们拦截到的首先是 RoutingStatementHandler 然后通过里面的一个 delegate 属性拿到另一个实现类 BaseStatementHandler ,从它里面的 mappedStatement 属性拿到 MappedStatement ,也就是映射配置文件的信息。还可以拿到 ParameterHandler 对象,其中保存了填充 SQL 的数据,可以用它来填充一个 statement ,然后这个 statement 就可以执行了。
获取处理 SQL 是在 prepare 方法中进行的,所以我们拦截这个方法(在处理这个方法之前会被拦截),更准的说获取 SQL 其实是 instantiateStatement 方法,这个方法是在 prepare 方法中调用的。
然后最后的注册插件就不多说了 plugins 标签,上面也是写过的。

其他

写 SQL 的时候尽量不用用 select * 这种,就是说不要用星,直接手写上就行了,避免给数据库带来不必要的压力,虽然这样写我们倒是很省事;


在 Mybatis 中映射文件的各种标签是可以写多条 SQL 语句的,比如 INSERT 里可以批量插入几张表的数据,多条语句以分号分割;但是有一个前提,就是在连接数据库的 URL 中要加个参数开启这个功能(默认是关闭的)
对于 MySQL:在 URL 的后面再追加 &allowMultiQueries=true
并且写在同一标签的多条 SQL 是在同一个事务下的,只要你不 try ,就能回滚

还有事务的具体操作没有提到,待补充….

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~