MyBatis: LAZY LOAD

张天宇 on 2020-04-06

MyBatis中的延迟加载。

延迟加载和立即加载

延迟加载:真正使用数据时候才发起查询,不用的时候不查询,按需加载(懒加载)。

立即加载:不管用不用,只要一调用方法,马上发起查询。

在对应的四种表关系中,一对多、多对多使用 延迟加载。多对一、一对一采用立即加载。

即看屁股,屁股大的延迟加载。

实现

一对一实现延迟加载

需求:当查询账户信息时使用延迟加载。也就是说,如果不需要使用用户信息的话,那么只查询账户信息;只有当需要使用用户信息时,才去关联查询。

  1. 在MyBatis的配置文件中开启延迟加载

    1
    2
    3
    4
    5
     <!-- 开启延迟加载 -->
    <settings>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="aggressiveLazyLoading" value="false"/>
    </settings>
  2. 修改上一篇笔记编写好的账户映射文件 AccountMapper.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <mapper namespace="top.tyzhang.dao.IAccountDao">
    <resultMap id="accountUserMap" type="top.tyzhang.domain.Account">
    <id property="id" column="id"></id>
    <result property="uid" column="uid"></result>
    <result property="money" column="money"></result>
    <association property="user" column="uid" javaType="top.tyzhang.domain.User" select="top.tyzhang.dao.IUserDao.findById"></association>
    </resultMap>
    <select id="findAll" resultMap="accountUserMap">
    SELECT * from account
    </select>
    </mapper>
  • 标签中的 select 属性表示我们要调用的映射语句的 ID,它会column 属性指定的列中检索数据,作为参数传递给目标 select 语句
  • column 属性指定传递给我们要调用的映射语句的参数

一对多实现延迟加载

  1. IUserDao映射文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <mapper namespace="top.tyzhang.dao.IUserDao">
    <resultMap id="userAccountMap" type="top.tyzhang.domain.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>
    <collection property="accounts" ofType="top.tyzhang.domain.Account" select="top.tyzhang.dao.IAccountDao.findAccountByUid" column="uid"></collection>
    </resultMap>
    <select id="findAll" resultMap="userAccountMap">
    select * from userm
    </select>
    </mapper>
  2. 在账户实体类添加对应方法

    1
    2
    3
    4
    public interface IAccountDao {
    List<Account> findAll();
    List<Account> findAccountByUid(Integer uid);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <mapper namespace="top.tyzhang.dao.IAccountDao">
    <resultMap id="accountUserMap" type="top.tyzhang.domain.Account">
    <id property="id" column="id"></id>
    <result property="uid" column="uid"></result>
    <result property="money" column="money"></result>
    <association property="user" column="uid" javaType="top.tyzhang.domain.User" select="top.tyzhang.dao.IUserDao.findById"></association>
    </resultMap>
    <select id="findAccountByUid" resultType="top.tyzhang.domain.Account">
    SELECT * from account where uid = #{uid}
    </select>
    </mapper>
  3. 测试类

    1
    2
    3
    4
    5
    6
    7
    public void findAll() {
    List<Account> accounts = accountDao.findAll();
    for (Account account : accounts) {
    System.out.println(account);
    System.out.println(account.getUser());
    }
    }

    当只执行findAll时候,日志中只调用了SELECT * from account ,如果打印语句执行,在打印循环体中还会逐个执行select * from userm where id = ?

缓存

WHAT:存在于内存的临时数据。

WHY:为了减少和数据库交互的次数,提高执行效率

HOW:适用:经常查询并且不经常改变的数据。数据的正确与否对最终结果影响不大的。不适用:经常改变的数据。数据的正确与否对最终结果影响很大的。例如:商品的库存、银行的汇率、股市的牌价等。

一级缓存

指的是MyBatis中SqlSession对象的缓存,当我们执行查询后,查询的结果会同时存入SqlSession为我们提供的一块区域中。该区域的结构是一个Map,当我们再次查询同样的数据,MyBatis会先去SqlSession中查询是否有,有的话直接拿过来用。当SqlSession对象消失时,MyBatis的一级缓存也就消失了。

1
2
3
4
User user1 = userDao.findById(41);
User user2 = userDao.findById(41);
System.out.println(user1 == user2);
//输出 true
1
2
3
4
5
6
7
8
9
User user1 = userDao.findById(41);
//1
sqlSession.close();
sqlSession = factory.openSession();
//2 sqlSession.clearCache(); 也可以清空缓存
userDao = sqlSession.getMapper(IUserDao.class);
User user2 = userDao.findById(41);
System.out.println(user1 == user2);
//输出 false

当调用SqlSession的修改、添加、删除、commit()、close()等方法时,就会清空一级缓存。例如以上例子如果在user2前update user1,输出false。

二级缓存

指的是MyBatis中SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享缓存。

使用步骤
  1. 让MyBatis框架支持二级缓存,在SqlMapConfig.xml中配置。

    1
    2
    3
    4
    <settings>
    <!-- 开启缓存, 可以忽略,默认为true -->
    <setting name="cacheEnabled" value="true"/>
    </settings>
  2. 让当前的映射文件支持二级缓存,在IUserDao.xml中配置。

    1
    <cache/>
  3. 让当前的操作支持二级缓存,在select标签中配置。

    1
    2
    3
    <select id="findAll" resultMap="userAccountsMap" useCache="true">
    SELECT * FROM user
    </select>
    • 当我们使用二级缓存的时候,所缓存的类一定要实现 java.io.Serializable 接口,这样才可以使用序列化的方式来保存对象。
    • 由于是序列化保存对象,所以二级缓存中存放的是数据,而不是整个对象。
1
2
3
4
5
6
7
8
9
10
11
12
SqlSession sqlSession1 = factory.openSession();
IUserDao dao1 = sqlSession1.getMapper(IUserDao.class);
User user1 = dao1.findById(41);
sqlSession.close();

SqlSession sqlSession2 = factory.openSession();
IUserDao dao2 = sqlSession2.getMapper(IUserDao.class);
User user2 = dao2.findById(41);
sqlSession.close();

System.out.println(user1 == user2);
//输出:false,但通过日志发现,只执行了一次sql语句。不开启二级缓存,执行两次。

基于注解开发

常用注解

注解 作用
@Intsert 实现新增
@Update 实现更新
@Delete 实现删除
@Select 实现查询
@Results 实现结果集封装
@ResultMap 实现引用 @Results 定义的封装
@One 实现一对一结果集封装
@Many 实现一对多结果集封装
@SelectProvider 实现动态 SQL 映射
@CacheNamespace 实现注解二级缓存的使用

环境搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbcConfig.properties"></properties>
<!-- 配置别名 -->
<typeAliases>
<package name="top.tyzhang.domain"></package>
</typeAliases>
<!-- 配置环境 -->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<!-- 指定带有注解的Dao接口所在位置 -->
<mappers>
<package name="top.tyzhang.dao"></package>
</mappers>
</configuration>

CRUD

接口实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface IUserDao {
@Select("select * from userm")
List<User> findAll();

@Insert("insert into userm(username, address, sex, birthday) values (#{username}, #{address}, #{sex}, #{birthday})")
void saveUser(User user);

@Update("update userm set username=#{username}, sex=#{sex}, birthday=#{birthday}, address=#{address} where id = #{id}")
void updateUser(User user);

@Delete("delete from userm where id =#{id}")
void deleteUser(Integer id);

@Select("select * from userm where id =#{id}")
User findById(Integer id);

@Select("select * from userm where username like #{username}")
List<User> findUserByName(String username);

@Select("select * from userm where username like '%${value}%'")
List<User> findUserByName1(String username);

@Select("select count(*) from userm")
int findTotal();
}
测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class annocrud {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init() throws IOException {
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}
@After
public void destroy() throws IOException {
session.commit();
session.close();
in.close();
}
@Test
public void testSave(){
User user = new User();
user.setUsername("张天宇");
user.setAddress("山东省临沂市");
userDao.saveUser(user);
}

@Test
public void testUpdate(){
User user = new User();
user.setId(49);
user.setUsername("张天宇");
user.setAddress("浙江省杭州市");
user.setSex("男");
userDao.updateUser(user);
}

@Test
public void testDelete(){
userDao.deleteUser(49);
}

@Test
public void testFindOne(){
System.out.println(userDao.findById(48));
}

@Test
public void testFindByName(){
List<User> users = userDao.findUserByName("%王%");
for (User user:users)
System.out.println(user);
}

@Test
public void testFindByName1(){
List<User> users = userDao.findUserByName1("王");
for (User user:users)
System.out.println(user);
}

@Test
public void testFindTotal(){
System.out.println(userDao.findTotal());
}
}
注意

当实体类的属性与数据库表列名不一致,应该使用@Results、@Result、@ResultMap 等注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IUserDao {
@Select("SELECT * FROM user")
@Results(id = "UserMap",value = {
@Result(id = true, property = "userId",column = "id"),
@Result(property = "userName",column = "username"),
@Result(property = "userBirthday",column = "birthday"),
@Result(property = "userSex",column = "sex"),
@Result(property = "userAddress",column = "address"),
})
List<User> listAllUsers();

@Insert("INSERT INTO user(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address})")
@ResultMap("UserMap")
int saveUser(User user);
}
  • @Results 注解用于定义映射结果集,相当于标签
    其中,id 属性为唯一标识。value 属性用于接收 @Result[] 注解类型的数组。
  • @Result 注解用于定义映射关系,相当于标签
    其中,id 属性指定主键。property 属性指定实体类的属性名,column 属性指定数据库表中对应的列。
  • @ResultMap 注解用于引用 @Results 定义的映射结果集,避免了重复定义映射结果集。

一对一立即加载

1
2
3
4
5
6
7
8
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
//多对一,mybatis称之为一对一,一个账户只能属于一个用户
private User user;
//...
}
1
2
3
4
5
6
7
8
9
10
public interface IAccountDao {
@Select("select * from account")
@Results(id="accountMap", value={
@Result(id=true, column = "id", property = "id"),
@Result(column = "uid", property = "uid"),
@Result(column = "money", property = "money"),
@Result(property = "user", column = "uid", one=@One(select="top.tyzhang.dao.IUserDao.findById", fetchType= FetchType.EAGER))
})
List<Account> findAll();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class accounttest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IAccountDao accountDao;

@Before
public void init() throws IOException {
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
accountDao = session.getMapper(IAccountDao.class);
}
@After
public void destroy() throws IOException {
session.commit();
session.close();
in.close();
}
@Test
public void testFindAll(){
List<Account> accounts = accountDao.findAll();
for (Account account:accounts) {
System.out.println(account);
System.out.println(account.getUser());
}
}
}
  • @One 注解相当于标签 ,是多表查询的关键,在注解中用来指定子查询返回单一对象。其中,select 属性指定用于查询的接口方法,fetchType 属性用于指定立即加载或延迟加载,分别对应 FetchType.EAGER 和 FetchType.LAZY。
  • 在包含 @one 注解的 @Result 中,column 属性用于指定将要作为参数进行查询的数据库表列。

一对多延迟加载

1
2
3
4
5
6
7
8
9
10
public class User implements Serializable {
private Integer id;
private String username;
private String address;
private String sex;
private Date birthday;
//一对多关系映射,一个用户对应多个账户
private List<Account> accounts;
//...
}
1
2
3
4
5
6
7
8
public interface IUserDao {
@Select("select * from userm")
@Results(value = {@Result(id=true, column = "id", property = "id"),
@Result(property = "accounts", column = "id",
many = @Many(select = "top.tyzhang.dao.IAccountDao.findAccountByUid", fetchType = FetchType.LAZY))}
)
List<User> findAll();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface IAccountDao {

@Select("select * from account")
@Results(id="accountMap", value={
@Result(id=true, column = "id", property = "id"),
@Result(column = "uid", property = "uid"),
@Result(column = "money", property = "money"),
@Result(property = "user", column = "uid", one=@One(select="top.tyzhang.dao.IUserDao.findById", fetchType= FetchType.EAGER))
})
List<Account> findAll();
@Select("select * from account where uid = #{uid}")
List<Account> findAccountByUid(Integer uid);
}

开启二级缓存

1
2
3
4
<settings>
<!-- 开启缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>

在持久层配置

1
2
3
4
@CacheNamespace(blocking = true)
public interface IUserDao {
// .....
}