您的位置:首页 > 其它

Mybatis学习记录(四)--高级查询和缓存

2016-04-18 14:31 501 查看
这些都是连贯的学习笔记,所以有的地方因为之前都说过,所以也就没怎么写详细了,看不太明白的可以看看之前的笔记.

一.高级查询

高级查询主要是一对一查询,一对多查询,多对多查询

1.一对一查询

有用户和订单两个表,用户对订单是1对1查询.也就是订单中有一个外键是指向用户的.

先创建实体类:

User.java

public class User {
private int id;
private String username;
private String password;
private String nickname;
private int status;
//省略get和set方法
}


Orders.java

public class Orders {
private int id;
private Date buy_date;
private Date pay_date;
private Date confirm_date;
private int status;
private int user_id;//外键,指向用户
//省略get和set方法
}


1.使用resultType

这种方式映射的话,我们需要一个pojo的包装类,在包装类里面增加我们要关联的属性,这里增加用户名和昵称,把要关联的属性聚集在一起.具体如下,

OrdersCustorm.java

public class OrdersCustorm extends Orders {
private String username;
private String nickname;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getNickname() {
return nickname;
}

public void setNickname(String nickname) {
this.nickname = nickname;
}
}


接下来SQL语句就可以使用内连接查询.不过返回的类型是写好的pojo包装类,这样的方法使用起来省事

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="orders">
<select id="findOrderAndUser" parameterType="int" resultType="com.aust.model.OrdersCustorm">
SELECT t_orders.*,user.username,user.nickname
FROM t_orders,user
WHERE user_id = user.id AND user_id=#{id}
</select>

</mapper>


junit测试

@Before
public void init(){
InputStream is = null;
try {
is = Resources.getResourceAsStream("SqlMapperConfig.xml");
} catch (IOException e) {
e.printStackTrace();
}
factory = new SqlSessionFactoryBuilder().build(is);
}

//测试取出单个
@Test
public void findOrderAndUserTest(){
//获取sqlsession
SqlSession session = factory.openSession();
OrdersCustorm custorm = session.selectOne("orders.findOrderAndUser",18);
session.close();
System.out.println(custorm.toString());
}


测试结果



2.使用resultMap

使用resultMap的话,就需要在Orders里面定义一个User属性,用于关联查询,具体如下:

Orders.java

public class Orders {
private int id;
private Date buy_date;
private Date pay_date;
private Date confirm_date;
private int status;
private int user_id;//外键,指向用户
private User user;//用于关联查询
}


然后定义resultMap

autoMapping=”true”这个是打开自动映射,不然只会映射你配置的那些属性

association property=”user” javaType=”com.aust.model.User”这句话就是关联到属性user,也就是在Orders里面新增加的关联变量,映射类型为com.aust.model.User这个类.

<resultMap id="OrderAndUserMap" type="com.aust.model.Orders" autoMapping="true">
<id column="id" property="id"/>
<association property="user" javaType="com.aust.model.User">
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="nickname" property="nickname"/>
</association>
</resultMap>


junit测试

@Before
public void init(){
InputStream is = null;
try {
is = Resources.getResourceAsStream("SqlMapperConfig.xml");
} catch (IOException e) {
e.printStackTrace();
}
factory = new SqlSessionFactoryBuilder().build(is);
}

//测试取出单个
@Test
public void findOrderAndUserTest(){
//获取sqlsession
SqlSession session = factory.openSession();
Orders orders = session.selectOne("orders.findOrderAndUserMap",18);
session.close();
System.out.println(orders.toString());
}




2.一对多查询

现在的需求是查询用户和地址,一个用户对应多个地址.一对多查询只能使用resultMap了,不然会出现很多重复数据.使用前,需要修改User实体类,增加一个集合存储多条地址信息

User.java

public class User {
private int id;
private String username;
private String password;
private String nickname;
private int status;
private List<Adress> adresses;//用于存储用户的多个地址信息
}


然后定义resultMap

collection property=”adresses” ofType=”com.aust.model.Adress”:

collection标签用于映射到一个集合的信息,property要映射的属性,也就是user里面的List adresses,ofType要映射到集合里面的pojo类型,这里是com.aust.model.Adress

<resultMap id="userMap" type="com.aust.model.User" autoMapping="true">
<id column="userid" property="id"/>
<collection property="adresses" ofType="com.aust.model.Adress" autoMapping="true">
<id column="id" property="id"/>
</collection>
</resultMap>


接着写sql语句,仍然使用内连接

<select id="findUserAndAddress" parameterType="int" resultMap="userMap">
SELECT user.id userid,user.username,user.nickname,t_address.*
from user,t_address
WHERE t_address.user_id = user.id AND user.id=#{id};
</select>


junit测试

@Before
public void init(){
InputStream is = null;
try {
is = Resources.getResourceAsStream("SqlMapperConfig.xml");
} catch (IOException e) {
e.printStackTrace();
}
factory = new SqlSessionFactoryBuilder().build(is);
}

@Test
public void findAddressAndUserTest(){
//获取sqlsession
SqlSession session = factory.openSession();
User user = session.selectOne("UserMapper.findUserAndAddress",18);
session.close();
System.out.println(user.toString());
}


测试结果,成功取出多条地址信息



3.多对多查询

手头上没有很好的例子,所以也就直接说说思路.通过上面的1对1和1对n两个可以看出,n对n无非就是collection,association的嵌套使用,每一个collection,association实际上就相当于一个局部的resultMap,只要明白这一点的话,多对多实现是也就很简单了.

4.总结

resultType:

作用:

将查询结果按照sql列名pojo属性名一致性映射到pojo中。

场合:

常见一些明细记录的展示,比如用户购买商品明细,将关联查询信息全部展示在页面时,此时可直接使用resultType将每一条记录映射到pojo中,在前端页面遍历list(list中是pojo)即可。

resultMap:

使用association和collection完成一对一和一对多高级映射(对结果有特殊的映射要求)。

association:

作用:

将关联查询信息映射到一个pojo对象中。

场合:

为了方便查询关联信息可以使用association将关联订单信息映射为用户对象的pojo属性中,比如:查询订单及关联用户信息。

使用resultType无法将查询结果映射到pojo对象的pojo属性中,根据对结果集查询遍历的需要选择使用resultType还是resultMap。

collection:

作用:

将关联查询信息映射到一个list集合中。

场合:

为了方便查询遍历关联信息可以使用collection将关联信息映射到list集合中,比如:查询用户权限范围模块及模块下的菜单,可使用collection将模块映射到模块list中,将菜单列表映射到模块对象的菜单list属性中,这样的作的目的也是方便对查询结果集进行遍历查询。

如果使用resultType无法将查询结果映射到list集合中。

5.补充例子(javaType和ofType)

最近做到一个联合查询,用户登录后要把其完成的题目一起查询出来,只需要查询题目的id,也就是映射出
private List<Integer> pro_ac;
这样的形式,

对应的映射就如下,利用javaType来映射,而不是ofType

<resultMap id="userMap" type="com.aust.model.CumUser" autoMapping="true">
<id column="id" property="id"/>
<collection property="pro_ac" javaType="java.util.List"  ofType="java.lang.Integer" autoMapping="true">
<id column="pro_id" javaType="java.lang.Integer"/>
</collection>
</resultMap>


ofType 是对象的所属类型 javaType :collection 的类型

如:

<collection property="questions" ofType="map" javaType="list">


对应的java 形态为 :
List<Map<String,Object>>


二 .延迟加载

关于延迟加载,百度搜了好多,但是都乱七八糟的信息.延迟加载解决的是N+1问题,所谓N+1问题举个例子,

mybatis不推荐使用嵌套的select查询,如下面所述,

select * from teacher此时可查询出多条(记为N)教师记录。为了进一步查询出教师指导的学生的信息,需要针对每一条教师记录,生成一条SQL语句

select * from student where supervisor_id=?

以上SQL语句中的“?”就代表了每个教师的id。显而易见,这样的语句被生成了N条(“N+1问题”中的N)。这样在整个过程中,就总共执行了N+1条SQL语句,即N+1次数据库查询。而数据库查询通常是应用程序性能的瓶颈,一般应尽量减少数据库查询的次数,那么这种方式就会大大降低系统的性能。

解决方案:
第一种方法是使用一条SQL语句,把教师及其指导的学生的信息一次性地查询出来。
第二种方法是使用MyBatis的延迟加载机制.


1.延迟加载的配置

在SqlMapConfig.xml中配置

//开启热部署
<setting name="lazyLoadingEnabled" value="true"/>
//关闭积极加载,也就是设置为按需要加载
<setting name="aggressiveLazyLoading" value="false"/>


2.写sql查询

还用的是用户和地址之间的查询

//根据用户id查询
<select id="findUser" resultMap="userMap">
SELECT * FROM user;
</select>
//根据用户id查询订单
<select id="findAddress" parameterType="int" resultType="com.aust.model.Adress">
SELECT * FROM t_address WHERE t_address.user_id=#{id}
</select>
//resultMap映射
<resultMap id="userMap" type="com.aust.model.User" autoMapping="true">
<id column="id" property="id"/>
//这里可以看到多了两个属性select表示要调用的那个statement的id
//column表示要传入的参数
<collection property="adresses" ofType="com.aust.model.Adress" autoMapping="true" select="findAddress" column="id">
</collection>
</resultMap>


上面sql意思是,加入我们要取出全部用户,使用findUserById,然后当我们调用用户的user.getAdresses()取出地址的时候,mybatis就会把该用户的id传入findAddress作为输入参数,然后执行查询,也就是说假设我们没取出地址,则不会执行这个查询

junit测试

@Before
public void init(){
InputStream is = null;
try {
is = Resources.getResourceAsStream("SqlMapperConfig.xml");
} catch (IOException e) {
e.printStackTrace();
}
factory = new SqlSessionFactoryBuilder().build(is);
}

@Test
public void findAddressAndUserTest(){
//获取sqlsession
SqlSession session = factory.openSession();
List<User> users = session.selectList("UserMapper.findUser");
//循环取出地址.这个时候mybatis就会自动调用findAddress取出地址
for (User user:users) {
System.out.println(user.getAdresses().toString());//在这里打个断点测试
}
session.close();
}


测试如下,可以看出,取出全部用户后如果遍历则会一条一条的执行取出地址的sql语句.

所以这里如果你使用延迟加载后,遍历一个有很多记录的表的话,反而会影响性能,因为每遍历一次就会执行一条sql,最终得不偿失.

那么延迟加载在什么时候用呢?我认为在很多记录中,你已经知道了要具体取出的用户的时候用,这个时候就只需要执行取出你指定用户的地址,就一条sql



三.查询缓存

缓存就是指把数据库取出的结果暂时存储起来,这个可以存储在内存或者硬盘再或者就是服务器,然后再次执行相同的sql语句的时候,就会先去缓存里面找,找到的话就避免了再次从数据库中取出,因为从数据库取出花费往往是巨大的.

1.一级缓存

原理图如下,一级缓存是SqlSession级别的缓存,也就是说,SqlSession一旦关闭则一级缓存就会自动清空了.一级缓存是mybatis自动启用的,无需配置.



一级缓存区域是根据SqlSession为单位划分的。

每次查询会先从缓存区域找,如果找不到从数据库查询,查询到数据将数据写入缓存。

Mybatis内部存储缓存使用一个HashMap,key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象

sqlSession执行insert、update、delete等操作commit提交后会清空缓存区域。

junit测试一级缓存:

//前面init代码省略
@Test
public void findUserByIdTest(){
//获取sqlsession
SqlSession session = factory.openSession();
//查询18号
User user1 = session.selectOne("UserMapper.findUserById",18);
//再次查询18号
User user2 = session.selectOne("UserMapper.findUserById",18);
session.close();
}


从测试可以看出两次查询实际上只发出了一条sql语句.说明第二次查询是从缓存中找的,当然也可以跟踪代码来看



2.二级缓存

原理图如下



二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域,如果使用mapper代理方法每个mapper的namespace都不同,此时可以理解为二级缓存区域是根据mapper划分。

每次查询会先从缓存区域找,如果找不到从数据库查询,查询到数据将数据写入缓存。

Mybatis内部存储缓存使用一个HashMap,key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象

sqlSession执行insert、update、delete等操作commit提交后会清空缓存区域。

1.开启二级缓存

二级缓存的开启,不但要在SqlMapConfig.xml中配置,还需要在相应的Mapper.xml中配置

<!--开启二级缓存,默认也是开启状态的-->
<setting name="cacheEnabled" value="true"/>


在Mapper.xml中配置如下:

<!--设置该mapper使用二级缓存-->
<cache/>


除此之外二级缓存需要查询结果映射的pojo对象实现java.io.Serializable接口实现序列化和反序列化操作,注意如果存在父类、成员pojo都需要实现序列化接口。

public class Orders implements Serializable
public class User implements Serializable
....


2.二级缓存测试

@Test
public void findUserByIdTest(){
//获取sqlsession1
SqlSession session1 = factory.openSession();
//使用session1查询
User user1 = session1.selectOne("UserMapper.findUserById",18);
session1.close();
//获取session2
SqlSession session2 = factory.openSession();
//使用session2查询
User user = session2.selectOne("UserMapper.findUserById",18);
session2.close();
}


从测试结果可以看出来,两次查询是不同的session,实际上只执行了一次sql语句,缓存命中率,第一次为0,因为缓存为空,第二次为0.5,因为在缓存中找了两次,找到了这个数据.



3.禁用二级缓存

在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。

<select id="findOrderListResultMap" resultMap="ordersUserMap" useCache="false">


4.刷新缓存

在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。

设置statement配置中的flushCache=”true” 属性,默认情况下为true即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。

如下:

<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User" flushCache="true">


5.mybatis二级缓存参数

不过一般都是整合第三方缓存框架来用

flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。

size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。

readOnly(只读)属性可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。

如下例子:

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

1.LRU – 最近最少使用的:移除最长时间不被使用的对象。

2.FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

3.SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。

4.WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

3.整合ehcache

mybatis对于缓存管理不是很好,一般都是用第三方缓存代替,这里使用ehcache,主要掌握整合缓存的方法.

mybatis提供二级缓存Cache接口

package org.apache.ibatis.cache;

import java.util.concurrent.locks.ReadWriteLock;

public interface Cache {
String getId();

void putObject(Object var1, Object var2);

Object getObject(Object var1);

Object removeObject(Object var1);

void clear();

int getSize();

ReadWriteLock getReadWriteLock();
}


想要实现其他缓存的话,需要继承这个接口,当然第三方框架都帮我们写好了,我们只需要拿来使用即可

首先导入包,第一个是核心包,第二个是整合包,这里面有实现了Cache接口的实现类,下面两个是日志包,ehcache依赖这个日志包



接下来在classpath下配置ehcache.xml

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
>
<diskStore path="F:\develop\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>


属性说明:

1. diskStore:指定数据在磁盘中的存储位置。

2. defaultCache:当借助CacheManager.add(“demoCache”)创建Cache时,EhCache便会采用指定的的管理策略

以下属性是必须的:

3. maxElementsInMemory - 在内存中缓存的element的最大数目

4. maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大

5. eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断

6. overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上

以下属性是可选的:

7. timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大

8. timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大

diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.

9. diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。

10. diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作

11. memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)

最后只需要在mapper.xml里面设置缓存类

<!--设置该mapper使用ehcache二级缓存-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>


junit测试

@Test
public void findUserByIdTest(){
//获取sqlsession
SqlSession session1 = factory.openSession();
User user1 = session1.selectOne("UserMapper.findUserById",18);
session1.close();
SqlSession session2 = factory.openSession();
User user = session2.selectOne("UserMapper.findUserById",18);
session2.close();
}




4.缓存应用场景

对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。

实现方法如下:通过设置刷新间隔时间,由mybatis每隔一段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为30分钟、60分钟、24小时等,根据需求而定。

5.缓存局限性

mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: