zl程序教程

您现在的位置是:首页 >  后端

当前栏目

MyBatis动态SQL日志和缓存

2023-06-13 09:15:10 时间

1 配置log4j日志输出

在MyBatis执行过程中,如果希望看到SQL语句的执行过程,则可以为MyBatis配置日志输出信息。MyBatis支持不同的日志输出组件,其中,最常用的就是log4j日志组件了。以下演示为MyBatis配置log4j的过程。

(1)修改mybatis主配置文件,设置具体的日志组件。

(2)添加log4j组件依赖

         <!-- log4j -->
		<dependency>
			<groupId>log4j</groupId>
			<artifactId>log4j</artifactId>
			<version>1.2.12</version>
		</dependency>

(3)配置log4j组件输出位置和格式(log4j.properties)

### 设置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条件,否则查询所有数据。

List<Movie> getMoviesByCategoryId(@Param("cid") int cid);

这时可以使用<if>来实现,XML配置如下。

<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接口方法:

int fetchMovieRows(@Param("cid") int cid, @Param("title")String title);

XML配置:

<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配置如下:

<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接口:

void update(Movie movie);

     XML配置:

<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>来实现。

<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{ … }表达式时尤其有用。

  1. foreach数组(array)

接口方法:

void deleteBunch(int... ids);

配置:

<delete id="deleteBunch">
		delete from Movie where Id in 
		<foreach collection="array" item="movieId" open="(" close=")" separator=",">
			#{movieId}
		</foreach>
	</delete>

(2)foreach列表(list)

        接口方法:

void deleteBunch(List<Integer> idList);

配置:

<delete id="deleteBunch">
		delete from Movie where Id in 
		<foreach collection="list" item="movieId" open="(" close=")" separator=",">
			#{movieId}
		</foreach>
	</delete>

(3)foreach键值对集合(map)

        接口方法:

void deleteBunch(Map<String,Object> map);

这里的map实际上并不是直接被循环的数据,而是包含被循环的数据作为其中一项键值对。

Map<String,Object> map = new HashMap<String,Object>();
map.put("ids", new int[]{7,9,11});	
mapper.deleteBunch(map);
	sess.commit();

配置:

<delete id="deleteBunch">
		delete from Movie where Id in 
		<foreach collection="ids" item="movieId" open="(" close=")" separator=",">
			#{movieId}
		</foreach>
	</delete>

3 延时加载

当我们使用二次查询的方式配置外键对象映射(resultMap)时,我们还可以选择使用延时加载的方式获取外键对象属性。也就是说,不使用外键对象时,该对象不加载。

        使用延时加载,需要注意以下几点:

  1. 导入动态代理所需要的jar包

直接导入cglib-2.2.2.jar

        或使用maven

<dependency>
	<groupId>cglib</groupId>
	<artifactId>cglib</artifactId>
	<version>2.2.2</version>
</dependency>

(2)在主配置文件mybatis.xml中添加<settings>配置,开启延时加载机制

<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的方式没有意义

<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>

经过上述设置,当调用者不使用查询结果的外键属性时,不会触发外键对象查询。

@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)两次执行相同参数的相同查询,只访问一次数据库。

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()可以清空一级缓存

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,一级缓存失效。

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)当数据放生变更时,相关的一级缓存会被清理。

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>元素的配置。

<mapper namespace="mycinema.dao.MovieDao">
	<!-- 开启二级缓存 -->
	<cache />
……

此外,被缓存的实体类,应该是可序列化的(即需要实现Serializable接口)

(2)测试二级缓存效果

多个SqlSession对象使用相同的语句和参数作查询,可以从缓存中获取对象。

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标签常用属性:

<cache 
eviction="FIFO"  		<!--回收策略为先进先出-->
flushInterval="60000" 	<!--自动刷新时间60s-->
size="512"				<!--最多缓存512个引用对象-->
readOnly="true"			<!--只读-->
/>