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

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

一.高级查询

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

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集合中。


二 .延迟加载

关于延迟加载,百度搜了好多,但是都乱七八糟的信息.延迟加载解决的是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为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。

时间: 2024-08-06 09:34:34

Mybatis学习记录(四)--高级查询和缓存的相关文章

mybatis学习笔记(10)-一对一查询

mybatis学习笔记(10)-一对一查询 mybatis学习笔记10-一对一查询 resultType实现 resultMap实现 resultType和resultMap实现一对一查询小结 本文使用两种方式(resultType和resultMap)实现一对一查询,查询订单信息,关联查询创建订单的用户信息 resultType实现 sql语句 确定查询的主表:订单表 确定查询的关联表:用户表 关联查询使用内连接?还是外连接? 因为orders表中有一个外键(user_id),通过外键关联查询

MyBatis:学习笔记(3)——关联查询

MyBatis:学习笔记(3)--关联查询 关联查询 理解联结 SQL最强大的功能之一在于我们可以在数据查询的执行中可以使用联结,来将多个表中的数据作为整体进行筛选. 模拟一个简单的在线商品购物系统,如果我们将用户信息和订单信息都保存在user表中,这样就不存在联结关系,因为我们仅仅操作一张表就好. 但是这是非常不明智的选择,举例来说,一个用户可以拥有多个订单,如果保存在一个表中,势必会导致用户信息的多次出现,因为每个订单绑定的用户信息都是相同的. 所以我们尽量要将不同的信息存储与不同的表中,但

Linux System Programming 学习笔记(四) 高级I/O

1. Scatter/Gather I/O a single system call  to  read or write data between single data stream and multiple buffers This type of I/O is so named because the data is scattered into or gathered from the given vector of buffers Scatter/Gather I/O 相比于 C标准

MyBatis 学习记录5 MyBatis的二级缓存

主题 之前学习了一下MyBatis的一级缓存,主要涉及到BaseExecutor这个类. 现在准备学习记录下MyBatis二级缓存. 配置二级缓存与初始化发生的事情 首先二级缓存默认是不开启的,需要自己配置开启. 如上图,需要在configuration里去开启. 其次在需要用到二级缓存的Mapper的配置里做一些操作,如下图,增加一个cache节点 至此就可以在UserMapper上开启二级缓存了. 当MaBatis初始化的时候,需要解析各种XML配置来生成SQLSessionFactory,

Mybatis学习记录(三)--Mybatis配置文件详解

关于mybatis的配置文件,主要有两个,一个是SqlMapperConfig.xml文件一个是mapper.xml文件,分别对这两个进行深入全面学习. 一.SqlMapperConfig.xml文件 1.标签概况 在SqlMapperConfig.xml中主要有以下标签,其中环境集合environments和spring整合后废除不用.objectFactory和plugins不经常使用. properties(属性) settings(全局配置参数) typeAliases(类型别名) ty

mybatis学习记录

转自:http://www.yihaomen.com/article/java/302.htm (读者注:其实这个应该叫做很基础的入门一下下,如果你看过Hibernate了那这个就非常的简单) (再加一条,其实大家可以看官方的教程更好些:http://mybatis.github.io/mybatis-3/,而且如果英文不是很好的那就看中文的:http://mybatis.github.io/mybatis-3/zh/sqlmap-xml.html) 写在这个系列前面的话: 以前曾经用过ibat

Hibernate学习---第四节:一级缓存

一.Hibernate 一级缓存的概念: 一级缓存生命周期很短与 session 生命周期一致,所以一级缓存也叫 session 级缓存或事务级缓存.位于缓存中的对象处于持久化状态,它和表中的相关记录对应,session 能够在某些时间点,按照缓存中持久化对象的属性变化来同步数据库中表的记录,这一过程称为清理缓存. (1).一级缓存实现原理.session 缓存是由它的实现类 sessionImpl 中定义的一些集合属性构成的,原理是保证有一个引用在关联着某个持久化对象,保持它的生命周期不会结束

转MYSQL学习(四) 查询

MySQL中select的基本语法形式: select 属性列表 from 表名和视图列表 [where 条件表达式] [group by 属性名[having 条件表达式]] [order by 属性名[asc|desc]] [limit <offset>,row count] 说明: where子句:按照“条件表达式”指定的条件进行查询. group by子句:按照“属性名”指定的字段进行分组. having子句:有group by才能having子句,只有满足“条件表达式”中指定的条件的

MyBatis(7)高级查询

高级查询: 对于整体的工程是时候增加一点文件了: 具体用到那个类再去说明类的内容 一对一查询: 1.resultType进行实现: 执行的sql语句: 查询的主表:订单表 查询的关联表:用户表 orders表有一个外键 select orders.*,user.username,user.sex,user.address  from orders ,user where orders.user_id = user.id; ordersCustomer.java public class Orde