1 配置log4j日志输出
在MyBatis执行过程中,如果希望看到SQL语句的执行过程,则可以为MyBatis配置日志输出信息。MyBatis支持不同的日志输出组件,其中,最常用的就是log4j日志组件了。以下演示为MyBatis配置log4j的过程。
(1)修改mybatis主配置文件,设置具体的日志组件。
(2)添加log4j组件依赖
代码语言:javascript复制 <!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
(3)配置log4j组件输出位置和格式(log4j.properties)
代码语言:javascript复制### 设置Logger输出级别和输出目的地 ###
log4j.rootLogger=debug, stdout,logfile
### 把日志信息输出到控制台 ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.err
log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout
### 把日志信息输出到文件:jbit.log ###
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=d:\mybatis-log.txt
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %F %p %m%n
2 动态SQL
MyBatis 的一个强大的特性之一通常是它的动态 SQL 能力。 如果你有使用 JDBC 或其他数据库访问技术的经验,你就明白有条件地串联 SQL 字符串在一起是多么的重要和麻烦,MyBatis的动态SQL就是用来解决这一问题的。
MyBatis的动态 SQL 元素和JSP中的JSTL相似,使用XML元素结合表达式来控制最终生成的SQL内容。MyBatis的动态SQL元素有以下几种:
元素 | 功能 |
---|---|
if | 条件判断 |
choose (when, otherwise) | 相当于java的switch |
where | 简化SQL语句中where的条件判断 |
set | 解决动态update语句 |
trim | 灵活去除多余的关键字 |
foreach | 循环迭代一个集合,常用于SQL的in条件 |
2.1 条件判断<if>
定义如下的MovieMapper接口方法,当cid大于0的时候添加根据CategoryId查询Movie的Where条件,否则查询所有数据。
代码语言:javascript复制List<Movie> getMoviesByCategoryId(@Param("cid") int cid);
这时可以使用<if>来实现,XML配置如下。
代码语言:javascript复制<select id="getMoviesByCategoryId" resultType="Movie" parameterType="int">
select m.* from Movie m
<if test="cid>0">
where m.CategoryId=#{cid}
</if>
</select>
使用log4j日志来查看最终执行的SQL语句,我们发现,如果cid大于0时,日志如下:
而cid==0时,日志如下:
这种最终执行的SQL语句会根据情况动态调整的技术,就称为“动态SQL”。
2.2 动态拼接多个where条件<where>
如果要根据多个参数的条件去动态拼接多个where,就存在一个什么时候该用where、什么时候该用and的问题。单使用if,可能需要写成这样。
Mapper接口方法:
代码语言:javascript复制int fetchMovieRows(@Param("cid") int cid, @Param("title")String title);
XML配置:
代码语言:javascript复制<select id="fetchMovieRows" resultType="int">
select count(m.id) from Movie m where 1=1
<if test="cid>0">
and m.CategoryId=#{cid}
</if>
<if test="title!=null and title!=''">
and m.title like concat('%',#{title},'%')
</if>
</select>
MyBatis提供的<where>表达式可以更优雅的解决这个问题,不会出现where 1=1。<where>会根据情况,在第一个where条件出现的地方加上where,去掉多余的and。使用<where>后,XML配置如下:
代码语言:javascript复制<select id="fetchMovieRows" resultType="int">
select count(m.id) from Movie m
<where>
<if test="cid>0">
and m.CategoryId=#{cid}
</if>
<if test="title!=null and title!=''">
and m.title like concat('%',#{title},'%')
</if>
</where>
</select>
2.3 动态更新对象<set>
一般情况下,你要update对象的一个属性,就需要填入对象的所有属性,因为update语句中set的字段数目是固定的,我们很难为每个属性的变更变现不同的update语句。但MyBatis中的<set>允许我们动态生成update语句,当我们要修改对象的某一属性时,只要填入该属性和ID主键即可。
Mapper接口:
代码语言:javascript复制void update(Movie movie);
XML配置:
代码语言:javascript复制<update id="update" parameterType="Movie">
update Movie
<set>
<if test="movieCode!=null">movieCode=#{movieCode},</if>
<if test="title!=null">title=#{title},</if>
<if test="director!=null">director=#{director},</if>
<if test="dateReleased!=null">dateReleased=#{dateReleased},</if>
<if test="category!=null and category.id>0">categoryId=#{category.id},</if>
</set>
where id=#{id}
</update>
有趣的是,<set>会忽略最后一个字段值后的“,”。
2.4 去除多余部分,增加前后缀<trim>
<trim>表达式用于去除多余的一些部分,并且同时加上前后缀。例如上述的动态update方法,我们也可以把<set>换成<trim>来实现。
代码语言:javascript复制<update id="update" parameterType="Movie">
update Movie
<trim prefix="set" suffixOverrides="," suffix="where id=#{id}" >
<if test="movieCode!=null">movieCode=#{movieCode},</if>
<if test="title!=null">title=#{title},</if>
<if test="director!=null">director=#{director},</if>
<if test="dateReleased!=null">dateReleased=#{dateReleased},</if>
<if test="category!=null and category.id>0">categoryId=#{category.id},</if>
</trim>
</update>
2.5 循环<foreach>
有时候,参数或条件是集合时,我们还可以通过foreach表达式来实现循环输出值得效果,在where条件为in{ … }表达式时尤其有用。
- foreach数组(array)
接口方法:
代码语言:javascript复制void deleteBunch(int... ids);
配置:
代码语言:javascript复制<delete id="deleteBunch">
delete from Movie where Id in
<foreach collection="array" item="movieId" open="(" close=")" separator=",">
#{movieId}
</foreach>
</delete>
(2)foreach列表(list)
接口方法:
代码语言:javascript复制void deleteBunch(List<Integer> idList);
配置:
代码语言:javascript复制<delete id="deleteBunch">
delete from Movie where Id in
<foreach collection="list" item="movieId" open="(" close=")" separator=",">
#{movieId}
</foreach>
</delete>
(3)foreach键值对集合(map)
接口方法:
代码语言:javascript复制void deleteBunch(Map<String,Object> map);
这里的map实际上并不是直接被循环的数据,而是包含被循环的数据作为其中一项键值对。
代码语言:javascript复制Map<String,Object> map = new HashMap<String,Object>();
map.put("ids", new int[]{7,9,11});
mapper.deleteBunch(map);
sess.commit();
配置:
代码语言:javascript复制<delete id="deleteBunch">
delete from Movie where Id in
<foreach collection="ids" item="movieId" open="(" close=")" separator=",">
#{movieId}
</foreach>
</delete>
3 延时加载
当我们使用二次查询的方式配置外键对象映射(resultMap)时,我们还可以选择使用延时加载的方式获取外键对象属性。也就是说,不使用外键对象时,该对象不加载。
使用延时加载,需要注意以下几点:
- 导入动态代理所需要的jar包
直接导入cglib-2.2.2.jar
或使用maven
代码语言:javascript复制<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
(2)在主配置文件mybatis.xml中添加<settings>配置,开启延时加载机制
代码语言:javascript复制<configuration>
<properties resource="jdbc.properties"/>
<settings>
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<typeAliases>…</typeAliases>
<environments default="development">…</environments>
<mappers>…</mappers>
</configuration>
(3)在resultMap映射中,需要使用二次查询的方式。对于延时加载,join的方式没有意义
代码语言:javascript复制<mapper namespace="mycinema.dao.MovieDao">
<resultMap type="Movie" id="movieResultMap">
<association property="category" javaType="Category"
column="CategoryId" select="mycinema.dao.CategoryDao.fetchById" />
</resultMap>
<select id="fetchById" parameterType="int" resultMap="movieResultMap">
select * from Movie where id=#{id}
</select>
</mapper>
经过上述设置,当调用者不使用查询结果的外键属性时,不会触发外键对象查询。
代码语言:javascript复制@Test
public void fetchByIdTest(){
Movie m = target.fetchById(1);
Assert.assertEquals("疯狂的石头",m.getTitle());
System.out.println(m.getTitle());
}
上述执行日志(log4j)如下:
而如果不配置延时加载,同样的测试代码,其执行日志如下:
4 MyBatis的缓存
在实际应用中,我们常常需要反复获取相同的数据(尤其是新近的数据),如果每次都要从数据库查询,会使得系统开销很大,为了提高性能,可以把反复被使用的数据放在内存中保存一段时间,这段时间如果要获取相同的对象,就直接从内存中取出,这样的技术成为“缓存”技术。
和大多数数据持久化框架相似,MyBatis同样提供了一级缓存和二级缓存机制。MyBatis默认使用PerpetualCache作为缓存提供者。
4.1 一级缓存
一级缓存就是保存在SqlSession对象中的缓存,该缓存的生存期与SqlSession对象的生存期相同。SqlSession对象会把它查询过的数据对象放置在一级缓存中,同一个SqlSession再次获取已缓存对象时无需再访问数据库,如果SqlSession对象的clearCache或 close方法被调用,一级缓存就会被清空。当然,如果缓存的数据被修改过,缓存也会被清除。
(1)两次执行相同参数的相同查询,只访问一次数据库。
代码语言:javascript复制String sqlId = "mycinema.dao.CategoryDao.fetchCateById";
SqlSession session = MyBatisUtil.openSession();
Category c1 = (Category)session.selectOne(sqlId, 1);
System.out.println(c1.getName());
Category c2 = (Category)session.selectOne(sqlId, 1);
System.out.println(c2.getName());
session.close();
(2)使用session.clearCache()可以清空一级缓存
代码语言:javascript复制String sqlId = "mycinema.dao.CategoryDao.fetchCateById";
SqlSession session = MyBatisUtil.openSession();
Category c1 = (Category)session.selectOne(sqlId, 1);
System.out.println(c1.getName());
session.clearCache(); //清空一级缓存
Category c2 = (Category)session.selectOne(sqlId, 1);
System.out.println(c2.getName());
session.close();
(3)使用session.close()关闭Session,一级缓存失效。
代码语言:javascript复制String sqlId = "mycinema.dao.CategoryDao.fetchCateById";
SqlSession session = MyBatisUtil.openSession();
Category c1 = (Category)session.selectOne(sqlId, 1);
System.out.println(c1.getName());
session.close(); //关闭会话
session = MyBatisUtil.openSession(); //再次打开新会话
Category c2 = (Category)session.selectOne(sqlId, 1);
System.out.println(c2.getName());
session.close();
(4)当数据放生变更时,相关的一级缓存会被清理。
代码语言:javascript复制String updateId = "mycinema.dao.CategoryDao.update";
String selectId = "mycinema.dao.CategoryDao.fetchCateById";
SqlSession session = MyBatisUtil.openSession();
Category c1 = (Category)session.selectOne(selectId, 1);
System.out.println(c1.getName());
session.update(updateId, c1); //更新
session.commit(); //提交
Category c2 = (Category)session.selectOne(selectId, 1);
System.out.println(c2.getName());
session.close();
4.2 二级缓存
二级缓存是基于SqlSessionFactory级别的缓存,也就是说,该缓存在多个SqlSession对象之间有效,不同的SqlSession对象可以共享相同的缓存。
二级缓存的作用域是基于Namespace / Mapper的,当某一个作用域(Namespaces / Mapper)进行了增删改操作后,默认该作用域下所有 select 中的缓存将被清理。
二级缓存可以使用默认的PerpetualCache提供者,也允许配置其他缓存框架,如Ehcache等。
(1)启用二级缓存
启用二级缓存,需要使用在映射配置文件中加上<cache>元素的配置。
代码语言:javascript复制<mapper namespace="mycinema.dao.MovieDao">
<!-- 开启二级缓存 -->
<cache />
……
此外,被缓存的实体类,应该是可序列化的(即需要实现Serializable接口)
(2)测试二级缓存效果
多个SqlSession对象使用相同的语句和参数作查询,可以从缓存中获取对象。
代码语言:javascript复制String selectId = "mycinema.dao.MovieDao.getMoviesByCate";
SqlSession session1 = MyBatisUtil.openSession();
List<Category> list1 = session1.selectList(selectId, 1);
System.out.println(list1.size());
session1.close();
SqlSession session2 = MyBatisUtil.openSession();
List<Category> list2 = session2.selectList(selectId, 1);
System.out.println(list2.size());
session2.close();
(3)二级缓存的一些注意事项
1)映射语句文件中的所有select语句将会被缓存;
2)映射语句文件中的所有insert,update和delete语句会刷新缓;
3)缓存会使用Least Recently Used(LRU,最近最少使用的)算法来收回;
4)缓存会根据指定的时间间隔来刷新;
5)缓存默认会存储1024个对象。
6)cache标签常用属性:
代码语言:javascript复制<cache
eviction="FIFO" <!--回收策略为先进先出-->
flushInterval="60000" <!--自动刷新时间60s-->
size="512" <!--最多缓存512个引用对象-->
readOnly="true" <!--只读-->
/>