Mybatis 框架课程第四天
1 Mybatis 延迟加载策略
通过前面的学习,我们已经掌握了 Mybatis 中一对一,一对多,多对多关系的配置及实现,可以实现对象的关联查询。实际开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的账户信息。此时就是我们所说的延迟加载.
1.1 何为延迟加载
延迟加载:
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.
好处:
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
坏处:
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
1.2 实现需求
需求:
查询账户(Account)信息并且关联查询用户(User)信息。如果先查询账户(Account)信息即可满足要求,当我们需要查询用户(User)信息时再查询用户(User)信息。把对用户(User)信息的按需去查询就是延迟加载。
mybatis 第三天实现多表操作时,我们使用了 resultMap 来实现一对一,一对多,多对多关系的操作。主要是通过 association、collection 实现一对一及一对多映射。association、collection 具备延迟加载功能。
1.3 使用 assocation 实现延迟加载
需求:
查询账户信息同时查询用户信息。
1.3.1 账户的持久层 DAO 接口
/**
* 账户的持久层接口
*/
public interface IAccountDao {
/**
* 查询所有账户
* @return
*/
List<Account> findAll();
}
1.3.2 账户的持久层映射文件
<?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="com.zjw.dao.IAccountDao">
<resultMap id="accountUserMap" type="Account">
<id property="id" column="id"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- <association property="user" javaType="User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="address" column="address"></result>
<result property="sex" column="sex"></result>
<result property="birthday" column="birthday"></result>
</association>-->
<!--
一对一的关系映射,配置封装User的内容
select属性指定的内容,查询用户的唯一标识
column属性指定的内容,用户根据id查询时,所需参数的值
-->
<association property="user" column="uid" javaType="User" select="com.zjw.dao.IUserDao.findById">
</association>
</resultMap>
<!--查询所有账户-->
<select id="findAll" resultMap="accountUserMap">
select * from account ;
</select>
</mapper>
1.3.3 用户的持久层接口和映射文件
/**
* 用户的持久层接口
*/
public interface IUserDao {
/**
* 根据id查询用户
* @param userId
* @return
*/
User findById(Integer userId);
}
<?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="com.zjw.dao.IUserDao">
<resultMap id="UserAccountMap" type="User">
<id property="id" column="id"></id>
<result property="address" column="address"></result>
<result property="username" column="username"></result>
<result property="sex" column="sex"></result>
<result property="birthday" column="birthday"></result>
</resultMap>
<!--查询用户-->
<select id="findById" parameterType="Integer" resultType="User">
select * from user where id = #{userId} ;
</select>
</mapper>
1.3.4 开启 Mybatis 的延迟加载策略
我们需要在 Mybatis 的配置文件 SqlMapConfig.xml 文件中添加延迟加载的配置。
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
1.3.5 编写测试只查账户信息不查用户信息。
public class AccountTest {
private InputStream in ;
private SqlSessionFactoryBuilder builder;
private SqlSessionFactory factory ;
private SqlSession session ;
private IUserDao userDao ;
private IAccountDao accountDao;
@Before
public void init() throws Exception{
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
builder = new SqlSessionFactoryBuilder();
factory = builder.build(in);
//使用工厂生成SqlSession对象
session = factory.openSession();
//使用SqlSession穿过将Dao接口的代理对象
accountDao = session.getMapper(IAccountDao.class);
}
@After
public void destory() throws Exception{
session.commit();
session.close();
in.close();
}
/**
* 测试查询操作
*/
@Test
public void testFindAll(){
//使用代理对象执行方法
List<Account> accounts = accountDao.findAll();
}
}
测试结果如下:
我们发现,因为本次只是将 Account 对象查询出来放入 List 集合中,并没有涉及到 User 对象,所以就没有发出 SQL 语句查询账户所关联的 User 对象的查询。
1.4 使用 Collection 实现延迟加载
同样我们也可以在一对多关系配置的
结点中配置延迟加载策略。
<collection>结点中也有 select 属性,column 属性。
需求:
完成加载用户对象时,查询该用户所拥有的账户信息。
1.4.1 在 User 实体类中加入 List Account 属性
package com.zjw.domain;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
public class User implements Serializable {
private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;
//建立一对多的关系
private List<Account> accounts;
public List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", address='" + address + '\'' +
", sex='" + sex + '\'' +
", birthday=" + birthday +
'}';
}
}
1.4.2编写用户和账户持久层接口的方法
/**
* 查询所有用户
* @return
*/
List<User> findAll();
/**
* 根据用户id查询用户的所有账户
* @param uid
* @return
*/
List<Account> findAccountByUid(Integer uid);
1.4.3 编写用户持久层映射配置
<?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="com.zjw.dao.IUserDao">
<resultMap id="UserAccountMap" type="User">
<id property="id" column="id"></id>
<result property="address" column="address"></result>
<result property="username" column="username"></result>
<result property="sex" column="sex"></result>
<result property="birthday" column="birthday"></result>
<!-- collection 是用于建立一对多中集合属性的对应关系
ofType 用于指定集合元素的数据类型
select 是用于指定查询账户的唯一标识(账户的 dao 全限定类名加上方法名称)
column 是用于指定使用哪个字段的值作为条件查询
-->
<collection property="accounts" ofType="Account" column="id" select="com.zjw.dao.IAccountDao.findAccountByUid">
</collection>
</resultMap>
<!--查询所有-->
<select id="findAll" resultMap="UserAccountMap">
select * from user;
</select>
</mapper>
1.4.4 编写账户持久层映射配置
<select id="findAccountByUid" resultType="Account" parameterType="Integer">
select ac.* from account ac where ac.uid = #{uid};
</select>
1.4.5 测试只加载用户信息
public class UserTest {
private InputStream in ;
private SqlSessionFactoryBuilder builder;
private SqlSessionFactory factory ;
private SqlSession session ;
private IUserDao userDao ;
@Before
public void init() throws Exception{
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
builder = new SqlSessionFactoryBuilder();
factory = builder.build(in);
//使用工厂生成SqlSession对象
session = factory.openSession();
//使用SqlSession穿过将Dao接口的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After
public void destory() throws Exception{
session.commit();
session.close();
in.close();
}
/**
* 测试查询操作
*/
@Test
public void testFindAll(){
//使用代理对象执行方法
List<User> users = userDao.findAll();
// for (User user : users) {
// System.out.println(user);
// List<Account> accounts = user.getAccounts();
// for (Account account : accounts) {
// System.out.println(account);
// }
// }
}
我们发现并没有加载 Account 账户信息.
2 Mybatis 缓存
像大多数的持久化框架一样,Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。
Mybatis 中缓存分为一级缓存,二级缓存。
2.1 Mybatis 一级缓存
2.1.1 证明一级缓存的存在
一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在。
2.1.2 编写用户持久层Dao接口
/**
* 用户的持久层接口
*/
public interface IUserDao {
/**
* 根据id查询用户
* @param userId
* @return
*/
User findById(Integer userId);
}
2.1.2 编写用户持久层映射文件
<?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="com.zjw.dao.IUserDao">
<!--根据id查询用户 (查询支持cache)
好像开启了cache就不用配置useCache了,默认就是true
-->
<select id="findById" resultType="User" parameterType="Integer">
select * from user u where u.id = #{userId}
</select>
</mapper>
2.1.3 编写测试方法
package com.zjw.test;
import com.zjw.dao.IUserDao;
import com.zjw.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.InputStream;
import java.util.List;
public class UserTest {
private InputStream in ;
private SqlSessionFactoryBuilder builder;
private SqlSessionFactory factory ;
private SqlSession session ;
private IUserDao userDao ;
@Before
public void init() throws Exception{
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
builder = new SqlSessionFactoryBuilder();
factory = builder.build(in);
//使用工厂生成SqlSession对象
session = factory.openSession();
//使用SqlSession穿过将Dao接口的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After
public void destory() throws Exception{
session.commit();
session.close();
in.close();
}
/**
* 测试查询操作
*/
@Test
public void testFindAll(){
//使用代理对象执行方法
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
}
/**
* 测试一级缓存
* 可以发现拿到的是同一个对象
*
* 一级缓存是SqlSession范围的缓存,当调用SqlSession的修改,添加,删除,commit(),close()方法时,一级缓存会给清空
*/
@Test
public void testFindUserById(){
User user1 = userDao.findById(41);
User user2 = userDao.findById(41);
System.out.println(user1);
System.out.println(user2);
System.out.println(user1 == user2);
}
/**
* 测试一级缓存
* 关闭session可以清空一级缓存
*/
@Test
public void testFindUserById2(){
User user1 = userDao.findById(41);
//关闭session
session.close(); //close可以清空缓存
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
// session.clearCache();//clearCache也可以清空缓存
User user2 = userDao.findById(41);
System.out.println(user1);
System.out.println(user2);
System.out.println(user1 == user2);
}
/**
* 测试一级缓存
* clearCache清空一级缓存
*/
@Test
public void testFindUserById3(){
User user1 = userDao.findById(41);
session.clearCache();//clearCache也可以清空缓存
User user2 = userDao.findById(41);
System.out.println(user1);
System.out.println(user2);
System.out.println(user1 == user2);
}
}
testFindUserById()
我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是 Mybatis 提供给我们的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询 id 为 41 的记录时,并没有发出 sql 语句从数据库中查询数据,而是从一级缓存中查询。
一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close(),clearCache()等方法时,就会清空一级缓存。
testFindUserById2()
testFindUserById3()
testFindUserById2(),testFindUserById3()方法分别进行了session关闭和清空缓存,发现一级缓存没有了,会接着查询数据库。
2.1.4 一级缓存失效的四种情况
-
sqlsession变了 缓存失效
-
sqlsession不变,查询条件不同,一级缓存失效
-
sqlsession不变,中间发生了增删改操作,一级缓存失效
-
sqlsession不变,手动清除缓存,一级缓存失效
2.2 Mybatis 二级缓存
二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
2.2.1 二级缓存结构图
首先开启 mybatis 的二级缓存。
sqlSession1 去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中。
如果 SqlSession3 去执行相同 mapper 映射下 sql,执行 commit 提交,将会清空该 mapper 映射下的二级缓存区域的数据。
sqlSession2 去查询与 sqlSession1 相同的用户信息,首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据。
2.2.2 二级缓存的开启与关闭
2.2.2.1 第一步:在 SqlMapConfig.xml 文件开启二级缓存
<!--开启二级缓存,默认是开启的-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。为 true 代表开启二级缓存;为false 代表不开启二级缓存。
2.2.2.2 第二步:配置相关的 Mapper 映射文件
<?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="com.zjw.dao.IUserDao">
<!--开启user支持二级缓存-->
<cache/>
<!--根据id查询用户 (查询支持cache)
好像开启了cache就不用配置useCache了,默认就是true
-->
<select id="findById" resultType="User" parameterType="Integer">
select * from user u where u.id = #{userId}
</select>
</mapper>
2.2.2.3 第三步:配置 statement 上面的 useCache 属性
<select id="findById" resultType="User" parameterType="Integer" useCache="true">
select * from user u where u.id = #{userId}
</select>
将 UserDao.xml 映射文件中的<select>标签中设置 useCache=”true”代表当前这个 statement 要使用二级缓存,如果不使用二级缓存可以设置为 false。
注意:针对每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存。默认为true
2.2.3 二级缓存测试
public class SecondLeveCacheTest {
private InputStream in ;
private SqlSessionFactoryBuilder builder;
private SqlSessionFactory factory ;
private SqlSession session ;
private IUserDao userDao ;
@Before
public void init() throws Exception{
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
builder = new SqlSessionFactoryBuilder();
factory = builder.build(in);
// //使用工厂生成SqlSession对象
// session = factory.openSession();
// //使用SqlSession穿过将Dao接口的代理对象
// userDao = session.getMapper(IUserDao.class);
}
@After
public void destory() throws Exception{
// session.commit();
// session.close();
in.close();
}
/**
* 测试二级缓存
* SqlSessionFactory级别的缓存
* 二级缓存的使用步骤:
* 第一步:让Mybatis框架支持二级缓存(在SqlMapConfig.xml中配置)
* 第二步:让当前的映射文件支持二级缓存(在IUserDao.xml中配置)
* 第三步:让当前的操作支持二级缓存(在select标签中配置)
*/
@Test
public void testFindUserById(){
SqlSession session1 = factory.openSession();
IUserDao userDao1 = session1.getMapper(IUserDao.class);
User user1 = userDao1.findById(41);
System.out.println(user1);
//关闭一级缓存
session1.close();
SqlSession session2 = factory.openSession();
IUserDao userDao2 = session2.getMapper(IUserDao.class);
User user2 = userDao2.findById(41);
System.out.println(user2);
System.out.println(user1 == user2);
}
}
2.2.4 二级缓存注意事项
当我们在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口,这种就可以使用序列化方式来保存对象。
public class User implements Serializable{
private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;
//...
}
如果没有序列化会报错NotSerializableException