11.2 注解的使用

@select@insert@update@delete可以完成常见的CRUD(增删改査)SQL语句映射。

示例:测试 select,insert,update和delete操作

项目搭建

展开/折叠
G:\workspace_web2\MyADMLTest
├─src\
│ ├─db.properties
│ ├─domain\
│ │ └─User.java
│ ├─fractory\
│ │ └─SqlSessionFactoryTools.java
│ ├─log4j.xml
│ ├─mapper\
│ │ └─UserMapper.java
│ ├─mybatis-config.xml
│ ├─tb_user.sql
│ └─test\
│   ├─DeleteTest.java
│   ├─InsertUserTest.java
│   ├─SelectAllUser.java
│   ├─SelectUserByIdTest.java
│   └─UpdateUserTest.java
└─WebContent\
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    └─lib\
      ├─ant-1.9.6.jar
      ├─ant-launcher-1.9.6.jar
      ├─asm-5.2.jar
      ├─cglib-3.2.5.jar
      ├─commons-logging-1.2.jar
      ├─javassist-3.22.0-CR2.jar
      ├─log4j-1.2.17.jar
      ├─log4j-api-2.3.jar
      ├─log4j-core-2.3.jar
      ├─mybatis-3.4.5.jar
      ├─mysql-connector-java-5.1.44-bin.jar
      ├─ognl-3.1.15.jar
      ├─slf4j-api-1.7.25.jar
      └─slf4j-log4j12-1.7.25.jar

数据库表

/MyADMLTest/src/tb_user.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SET FOREIGN_KEY_CHECKS = 0;
-- 创建数据库表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`sex` char(2) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入数据
INSERT INTO `tb_user` VALUES ('1', '小明', '男', '21');
INSERT INTO `tb_user` VALUES ('2', '小王', '男', '22');
INSERT INTO `tb_user` VALUES ('3', '小丽', '女', '18');
INSERT INTO `tb_user` VALUES ('4', '小芳', '女', '18');
INSERT INTO `tb_user` VALUES ('5', '小王', '男', '22');
SET FOREIGN_KEY_CHECKS = 1;

持久化对象

/MyADMLTest/src/domain/User.java
1
2
3
4
5
6
7
8
9
10
package domain;
public class User {
private Integer id;
private String name;
private String sex;
private Integer age;
// 此处省略无参构造器User()
// 此处省略getter和setter方法,请自己补上
// 此处省略toString()方法.
}

mapper接口

/MyADMLTest/src/mapper/UserMapper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package mapper;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import domain.User;
public interface UserMapper {
@Select("select * from tb_user where id=#{id}")
@Results({
@Result(column = "id", property = "id", id = true),
@Result(column = "name", property = "name"),
@Result(column = "sex", property = "sex"),
@Result(column = "age", property = "age")
})
User selectUserById(Integer id);
//如果数据表的列名和持久化对象的属性名完全一致,则可以省略@Results注解,Mybatis可以自动映射.
@Select("select * from tb_user where id=#{id}")
User selectUserById2(Integer id);
}

mybatis-cofig.xml

/MyADMLTest/src/mybatis-config.xml
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
<?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">
<!-- 该配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
<!-- 引入数据库信息配置文件 -->
<properties resource="db.properties"/>
<!-- 设置日志实现 -->
<settings>
<setting
name="logImpl"
value="log4j"/>
</settings>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="pooled">
<property
name="driver"
value="${driver}"/>
<property
name="url"
value="${url}"/>
<property
name="username"
value="${username}"/>
<property
name="password"
value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- mapper如果引入的是接口,则使用class属性,class属性的值设为接口的完全限定类名 -->
<!-- mapper如果引入的是XML文件,则使用resource属性,resource属性的值设为xml相对于src的路径 -->
<mapper class="mapper.UserMapper"/>
</mappers>
</configuration>

数据库信息配置文件db.properties

/MyADMLTest/src/db.properties
1
2
3
4
5
6
# 保存为db.properties文件,然后在mybatis-config.xml中通过下面标签引入:
# <properties resource="db.properties"/>
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=root

log4j.xml

/MyADMLTest/src/log4j.xml
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration
PUBLIC "-//LOG4J//DTD LOG4J//EN"
"https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd" >
<!-- 请在mybatis-config.xml中配置如下设置 -->
<!-- <settings> -->
<!-- <setting -->
<!-- name="logImpl" -->
<!-- value="log4j"/> -->
<!-- </settings> -->
<log4j:configuration>
<appender
name="STDOUT"
class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param
name="ConversionPattern"
value="%5p [%t] %m%n"/>
</layout>
</appender>
<logger name="domain.User">
<level value="DEBUG"/>
</logger>
<root>
<level value="ERROR"/>
<appender-ref ref="STDOUT"/>
</root>
</log4j:configuration>

工具类

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
package fractory;

import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class SqlSessionFactoryTools {
private static SqlSessionFactory sqlSessionFactory = null;
static
{
try
{
InputStream mybatisConfigXML = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfigXML);
} catch (IOException e)
{
e.printStackTrace();
}
}
public static SqlSession getSqlSession()
{
return sqlSessionFactory.openSession();
}
}

测试select功能

SelectUserByIdTest.java

/MyADMLTest/src/test/SelectUserByIdTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFactoryTools;
import mapper.UserMapper;
public class SelectUserByIdTest {
public static void main(String[] args)
{
SqlSession sqlSession = null;
sqlSession = SqlSessionFactoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectUserById(1);
System.out.println("-----------------------------------------");
System.out.println(user);
sqlSession.close();
}
}

运行结果:

1
2
3
4
5
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-----------------------------------------
User [id=1, name=小明, sex=男, age=21]

调用selectUserById方法,会执行@Select注解中的SQL语句。

数据表列名和持久化对象属性名相同的情况

@Result注解用于列和属性之间的结果映射,如果列和属性名称相同,则可以省略@Result注解,MyBatis会自动进行映射。
我的持久化类的属性名和数据表的列名完全一致,则上面的查询方法可以写成如下形式:

G:\workspace_web2\MyADMLTest\src\mapper\UserMapper.java
1
2
@Select("select * from tb_user where id=#{id}")
User selectUserById2(Integer id);

测试:

G:\workspace_web2\MyADMLTest\src\test\SelectUserByIdTest.java
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
SqlSession sqlSession = null;
sqlSession = SqlSessionFactoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// User user = userMapper.selectUserById(1);
User user = userMapper.selectUserById2(1);
System.out.println("-----------------------------------------");
System.out.println(user);
sqlSession.close();
}

运行结果:

1
2
3
4
5
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-----------------------------------------
User [id=1, name=小明, sex=男, age=21]

可以看到运行结果与上面的完全一致。

测试insert

mapper接口方法

mapper接口中添加如下方法:

1
2
3
4
5
@Insert("insert into tb_user(name,sex,age) values(#{name},#{sex},#{age})")
// useGeneratedKeys = true,表示使用自动增长的主键
// keyProperty = "id",表示插入数据库表时生成的主键设置到User对象的id属性.
@Options(useGeneratedKeys = true, keyProperty = "id")
void insertUser(User user);

InsertUserTest.java

G:\workspace_web2\MyADMLTest\src\test\InsertUserTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFactoryTools;
import mapper.UserMapper;
public class InsertUserTest {
public static void main(String[] args)
{
SqlSession sqlSession = null;
// 1.加载mybatis-cofig.xml获取SqlSession实例
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 2.获取mapper接口的代理对象.
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = new User("小张", "男", 22);
System.out.println("插入数据库之前:" + user);
System.out.println("--------------------------------------------------");
userMapper.insertUser(user);
System.out.println("--------------------------------------------------");
System.out.println("插入数据库之后:" + user);
// 提交事务
sqlSession.commit();
sqlSession.close();
}
}

运行结果:

1
2
3
4
5
6
7
插入数据库之前:User [id=null, name=小张, sex=男, age=22]
--------------------------------------------------
DEBUG [main] ==> Preparing: insert into tb_user(name,sex,age) values(?,?,?)
DEBUG [main] ==> Parameters: 小张(String), 男(String), 22(Integer)
DEBUG [main] <== Updates: 1
--------------------------------------------------
插入数据库之后:User [id=6, name=小张, sex=男, age=22]

调用insertUser方法,会执行@insert注解中的SQL语句。需要注意的是,insertUser方法还使用了@Options注解,@Options注解的属性

  • useGeneratedKeys=true表示使用数据库自动增长的主键,该操作需要底层数据库的支持。
  • keyProperty="id"表示把插入数据时数据库生成的主键设置到user对象的id变量之中。

测试update

UserMapper接口中添加如下方法:

G:\workspace_web2\MyADMLTest\src\mapper\UserMapper.java
1
2
@Update("update tb_user set name=#{name},sex=#{sex},age=#{age} where id=#{id}")
void updateUser(User user);

UpdateUserTest.java

G:\workspace_web2\MyADMLTest\src\test\UpdateUserTest.java
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
package test;

import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFactoryTools;
import mapper.UserMapper;

public class UpdateUserTest {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
// 1.加载mybatis-cofig.xml配置文件,并获取会话
sqlSession = SqlSessionFactoryTools.getSqlSession();
// 2.创建mapper接口的代理对象.
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 查询要更新的用户
User user = userMapper.selectUserById(6);
// 如果该用户存在
if (user != null) {
System.out.println("更新之前的用户信息:" + user);
User user1 = new User(user.getId(), "老张头", "男", 88);
// 更新用户信息
userMapper.updateUser(user1);
// 从数据库中查询更新后的信息
user = userMapper.selectUserById(6);
System.out.println("更新之后的用户信息:" + user);
sqlSession.commit();
}
} catch (Exception e) {
e.printStackTrace();
sqlSession.rollback();
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 6(Integer)
DEBUG [main] <== Total: 1
更新之前的用户信息:User [id=6, name=小张, sex=男, age=22]
DEBUG [main] ==> Preparing: update tb_user set name=?,sex=?,age=? where id=?
DEBUG [main] ==> Parameters: 老张头(String), 男(String), 88(Integer), 6(Integer)
DEBUG [main] <== Updates: 1
DEBUG [main] ==> Preparing: select * from tb_user where id=?
DEBUG [main] ==> Parameters: 6(Integer)
DEBUG [main] <== Total: 1
更新之前的用户信息:User [id=6, name=老张头, sex=男, age=88]

测试delete

mapper接口方法

G:\workspace_web2\MyADMLTest\src\mapper\UserMapper.java
1
2
@Delete("delete from tb_user where id=#{id}")
void deleteUserById(@Param("id") Integer id);

调用deleteUser方法,会执行@Delete注解中的SQL语句。deleteUser方法参数前面的@Param("id")注解表示给该注解后面的变量取一个参数名称,对应@Delete注解中的#{id}。如果没有使用Param注解,则参数将会以它们的顺序位置来和SQL语句中的表达式进行映射。

DeleteTest.java

G:\workspace_web2\MyADMLTest\src\test\DeleteTest.java
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
package test;

import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFactoryTools;
import mapper.UserMapper;

public class DeleteTest {
public static void main(String[] args) {
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 在数据库中查找要删除的用户
User user = userMapper.selectUserById(5);
// 如果找到用户
if (user != null) {
System.out.println("即将删除记录:" + user);
// 删除用户
userMapper.deleteUserById(user.getId());
}
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
e.printStackTrace();
} finally {
sqlSession.close();
}
}
}

运行效果:

1
2
3
4
5
6
7
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 5(Integer)
DEBUG [main] <== Total: 1
即将删除记录:User [id=5, name=小王, sex=男, age=22]
DEBUG [main] ==> Preparing: delete from tb_user where id=?
DEBUG [main] ==> Parameters: 5(Integer)
DEBUG [main] <== Updates: 1

全部查询

mapper接口方法

1
2
@Select("select * from tb_user")
List<User> seleteAllUser();

SelectAllUser.java

/MyADMLTest/src/test/SelectAllUser.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package test;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFactoryTools;
import mapper.UserMapper;
public class SelectAllUser {
public static void main(String[] args)
{
SqlSession sqlSession = null;
sqlSession = SqlSessionFactoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> users = userMapper.seleteAllUser();
users.forEach(user -> System.out.println(" " + user));
sqlSession.close();
}
}

运行效果:

1
2
3
4
5
6
7
8
DEBUG [main] ==>  Preparing: select * from tb_user 
DEBUG [main] ==> Parameters:
DEBUG [main] <== Total: 5
User [id=1, name=小明, sex=男, age=21]
User [id=2, name=小王, sex=男, age=22]
User [id=3, name=小丽, sex=女, age=18]
User [id=4, name=小芳, sex=女, age=18]
User [id=6, name=老张头, sex=男, age=88]

11.1 常用注解

MyBatis的注解位于org.apache.ibatis.annotations包下。常用的注解如下:

  • Select。映射查询的SQL语句。
  • SelectProviderSelect语句的动态SQL映射。允许指定一个类名和一个方法在执行时返回运行的查询语句。有两个属性:typemethod,
    • type属性是类的完全限定名,
    • method是该类中的方法名。
  • Insert。映射插入的SQL语句。
  • InsertProviderInsert语句的动态SQL映射。允许指定一个类名和一个方法在执行时返回运行的插入语句。有两个属性:typemethod,
    • type属性是类的完全限定名,
    • method是该类中的方法名。
  • Update。映射更新的SQL语句。
  • UpdateProviderUpdate语句的动态SQL映射。允许指定一个类名和一个方法在执行时返回运行的更新语句。有两个属性:typemethod,
    • type属性是类的完全限定名,
    • method是该类中的方法名。
  • Delete。映射删除的SQL语句。
  • DeleteProviderDelete语句的动态SQL映射。允许指定一个类名和一个方法在执行时返回运行的删除语句。有两个属性:typemethod,
    • type属性是类的完全限定名
    • method是该类中的方法名。
  • Result。在列和属性之间的单独结果映射。属性包括:idcolumnpropertyjavaTypejdbcTypetypeHandleronemany
    • id属性是一个布尔值,表示是否被用于主键映射。
    • one属性是单独的映射,和XML配置中的association标签相似,
    • many属性是对集合而言的,和XML配置的collection标签相似。
  • Results。多个结果映射(Result)列表。
  • Options。提供配置选项的附加值,它们通常在映射语句上作为附加功能配置出现。
  • one。复杂类型的单独属性值映射。必须指定select属性,表示已映射的SQL语句的完全限定名。
  • Many。复杂类型的集合属性映射。必须指定select属性,表示已映射的SQL语句的完全限定名。
  • Param。当映射器方法需要多个参数时,这个注解可以被应用于映射器方法参数来给每个参数取一个名字。否则,多参数将会以它们的顺序位置和SQL语句中的表达式进行映射,这是默认的。使用@Param("id")时,SQL中参数应该被命名为#{id}

第11章 MyBatis的注解配置 概述

本章要点

  • MyBatis注解插入、修改、删除和査询操作。
  • MyBatis注解一对一,一对多和多对多操作。
  • MyBatis注解动态SQL
  • MyBatis注解调用存储过程。
  • MyBatis注解使用二级缓存。
    前面的章节介绍了MyBatis的基本用法、关联映射、动态SQL和缓存机制等知识,其所有的配置都使用XML完成,但是大量的XML配置文件的编写是非常烦琐的,因此MyBatis也提供了更加简便的基于注解(annotation)的配置方式。本章将重点介绍MyBatis的注解配置。

10.6 本章小结

本章首先介绍了MyBatis的关联映射,包括最常见的一对多、多对多关联映射,动态SQL机制和存储过程调用。这些都是在实际项目开发时最常用的操作,我们需要重点掌握。
接下来介绍了MyBatis的事务管理,包括Transaction和该接口的两个实现类JdbcTransactionManagedTransaction,以及它们如何处理事务。
最后介绍了MyBatis的缓存机制,包括一级缓存SqlSession和二级缓存mapper。使用缓存可以最大程度地减轻数据査询压力,提高数据库性能.
第11章将重点介绍MyBatis的注解配置。

10.5 MyBatis缓存机制 10.5.2二级缓存(mapper级别)

二级缓存是mapper级别的缓存。使用二级缓存时,多个SqlSession使用同一个mapperSQL语句去操作数据库,得到的数据会存在二级缓存区域,它同样是使用HashMap进行数据存储的。

二级缓存和一级缓存的区别

相比一级缓存SqlSession,二级缓存的范围更大,多个SqlSession可以共享二级缓存中的数据,二级缓存是跨SqlSession的。
二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace。不同的SqlSession两次执行相同的namespace下的SQL语句,且向SQL中传递的参数也相同,即最终执行相同的SQL语句时,当第一个SqlSession调用close()方法关闭一级缓存时,第一次从数据库中査询到的数据会被保存到二级缓存,第二次查询时会从二级缓存中获取数据,不再去底层数据库查询,从而提高査询效率.

二级缓存需要手动开启

MyBatis默认没有开启二级缓存,需要Mybatis的配置文件的setting全局参数中配置开启二级缓存。

如何设置二级缓存

接下来测试MyBatis的二级缓存,所有代码和测试一级缓存的代码完全一样,只是需要在MyBatis配置文件中开启二级缓存.

在mybatis-config.xml中设置开启二级缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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">
<!-- XML 配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
......
<settings>
<!-- 开启二级缓存 -->
<setting
name="cacheEnabled"
value="true"/>
</settings>
......
</configuration>

setting标签的cacheEnabled属性的valuetrue时表示在此配置文件下开启二级缓存,该属性默认为false

二级缓存和命名空间绑定

MyBatis的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml映射文件或者Mapper接口中。

  • 在映射文件中,命名空间就是XML根节点mappernamespace属性。
  • Mapper接口中,命名空间就是接口的全限定名称。

cache标签

在映射文件Mapper.xml中写入cache标签可以开启默认的二级缓存,代码如下:

1
2
3
4
5
6
7
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.UserMapper">
<cache/>
</mapper>

默认的二级缓存作用

默认的二级缓存会有如下作用:

  • 映射语句文件中的所有SELECT语句将会被缓存。
  • 映射语句文件中的所有INSERTUPDATEDELETE语句会刷新缓存。
  • 缓存会使用Least Recently Used(LRU 最近最少使用)策略来收回。
  • 根据时间表(如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新。
  • 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用。
  • 缓存会被视为read/write(可读/可写)的,这意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

cache标签实例

cache标签中所有这些行为都可以通过cache标签的属性来进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.UserMapper">
<!-- 开启二级缓存
回收策略为先进先出
自动刷新时间60s
最多缓存512个引用对象
只读
-->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper>

以上配置创建了一个LRU缓存,并每隔60秒刷新,最大存储512个对象,而且返回的对象被认为是只读的.

cache标签的属性

cache标签用来开启当前mappernamespace下的二级缓存。该标签的属性设置如下:

  • flushInterval属性。刷新间隔。可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况下是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
  • size属性。缓存数目。可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。
  • readonly属性。只读。该属性可以被设置为truefalse
    • 设置为true表示只读,只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改。这提供了很重要的性能优势。
    • 可读写的缓存会返回缓存对象的拷贝(通过序列化)。这种方式会慢一些,但是安全,因此默认是false表示不是只读.
  • eviction属性。收回策略,默认为LRU。有如下几种:
    • LRU最近最少使用策略,移除最长时间不被使用的对象。
    • FIFO先进先出策略,按对象进入缓存的顺序来移除它们.
    • SOFT软引用策略,移除基于垃圾回收器状态和软引用规则的对象.
    • WEAK弱引用策略,更积极地移除基于垃圾收集器状态和弱引用规则的对象.

使用二级缓存的的Java对象必须可序列化

使用二级缓存时,与查询结果映射的Java对象必须实现java.io.Serializable接口的序列化和反序列化操作,如果存在父类,其成员都需要实现序列化接口。实现序列化接口是为了对缓存数据进行序列化和反序列化操作,因为二级缓存数据存储介质多种多样,不一定在内存,有可能是硬盘或者远程服务器.

实例

项目结构

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
E:\workspace_web2\MyTwoLevelCacheTest
├─src
│ ├─db.properties
│ ├─domain
│ │ ├─tb_user.sql
│ │ └─User.java
│ ├─fractory
│ │ └─SqlSessionFratoryTools.java
│ ├─log4j.xml
│ ├─mapper
│ │ ├─UserMapper.java
│ │ └─UserMapper.xml
│ ├─mybatis-config.xml
│ └─test
│ └─TwoLevelCacheTest.java
└─WebContent
└─WEB-INF
└─lib
├─ant-1.9.6.jar
├─ant-launcher-1.9.6.jar
├─asm-5.2.jar
├─cglib-3.2.5.jar
├─commons-logging-1.2.jar
├─javassist-3.22.0-CR2.jar
├─log4j-1.2.17.jar
├─log4j-api-2.3.jar
├─log4j-core-2.3.jar
├─mybatis-3.4.5.jar
├─mysql-connector-java-5.1.44-bin.jar
├─ognl-3.1.15.jar
├─slf4j-api-1.7.25.jar
└─slf4j-log4j12-1.7.25.jar

初始化数据库表

/MyTwoLevelCacheTest/src/domain/tb_user.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 取消外键约束
SET FOREIGN_KEY_CHECKS = 0;
# 创建数据库表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`sex` char(2) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入数据
INSERT INTO `tb_user` VALUES ('1', '小明', '男', '21');
INSERT INTO `tb_user` VALUES ('2', '小王', '男', '22');
INSERT INTO `tb_user` VALUES ('3', '小丽', '女', '18');
INSERT INTO `tb_user` VALUES ('4', '小芳', '女', '18');
INSERT INTO `tb_user` VALUES ('5', '小王', '男', '22');
# 回复外键检查
SET FOREIGN_KEY_CHECKS=1;

数据库配置文件

db.properties:

/MyTwoLevelCacheTest/src/db.properties mysql5.x配置
1
2
3
4
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis
username=root
password=root
/MyTwoLevelCacheTest/src/db.properties mysql8.x配置
1
2
3
4
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&characterEncoding=utf-8
username=root
password=root

log4j.xml

/MyTwoLevelCacheTest/src/log4j.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration
PUBLIC "-//LOG4J//DTD LOG4J//EN"
"https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd" >
<log4j:configuration>
<appender name="STDOUT"
class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%5p [%t] %m%n" />
</layout>
</appender>
<logger name="mapper.UserMapper">
<level value="DEBUG" />
</logger>
<root>
<level value="ERROR" />
<appender-ref ref="STDOUT" />
</root>
</log4j:configuration>

mybatis-config.xml

/MyTwoLevelCacheTest/src/mybatis-config.xml
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
<?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">
<!-- 该配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
<properties resource="db.properties" />
<settings>
<!-- 设置日志实现 -->
<setting name="logImpl" value="log4j" />
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true" />
</settings>
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC" />
<dataSource type="pooled">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mapper/UserMapper.xml" />
</mappers>
</configuration>

User.java

/MyTwoLevelCacheTest/src/domain/User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package domain;
public class User {
private Integer id;
private String name;
private String sex;
private Integer age;
public User()
{
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
}
}

工具类

/MyTwoLevelCacheTest/src/fractory/SqlSessionFactoryTools.java
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
package fractory;

import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

public class SqlSessionFactoryTools {
private static SqlSessionFactory sqlSessionFactory = null;
static
{
try
{
InputStream mybatisConfigXML = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfigXML);
} catch (IOException e)
{
e.printStackTrace();
}
}
public static SqlSession getSqlSession()
{
return sqlSessionFactory.openSession();
}
}

mapper.xml

/MyTwoLevelCacheTest/src/mapper/UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="mapper.UserMapper">

<!-- ############## 下面的代码放在mapper.xml文件中 ############### -->
<!-- flushInterval:刷新时间间隔,默认刷新,只在执行SQL语句时刷新 -->
<!-- size:缓存数目,默认1024 -->
<!-- readOnly:是否只读,true为只读,默认false -->
<!-- eviction:回收策略,默认LRU(最近最少使用), -->
<!-- FIFO(先进先出), -->
<!-- SORT(软引用:移除基于垃圾回收器的状态和软引用规则) -->
<!-- WEAK(弱引用,更积极的移除基于垃圾回收器的窗台和弱引用规则的对象) -->
<cache flushInterval="60000" size="512" readOnly="true"
eviction="LRU" />
<select id="selectUserById" parameterType="int"
resultType="domain.User">
select * from tb_user where id=#{id}
</select>
<delete id="deleteUserById" parameterType="int">
delete from tb_user where id=#{id}
</delete>
</mapper>

mapper接口

/MyTwoLevelCacheTest/src/mapper/UserMapper.java
1
2
3
4
5
6
7
8
package mapper;

import domain.User;

public interface UserMapper {
User selectUserById(int id);
void deleteUserById(int id);
}

测试类

/MyTwoLevelCacheTest/src/test/TwoLevelCacheTest.java
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
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFratoryTools;
import mapper.UserMapper;
public class TwoLevelCacheTest {
public static void main(String[] args)
{
// 获取会话
SqlSession sqlSession = SqlSessionFratoryTools.getSqlSession();
// 获取mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 查询用户
User user = userMapper.selectUserById(1);
System.out.println(user);
System.out.println("--------------------------------------------");
// 关闭会话,清空一级缓存.
sqlSession.close();
// 获取一个新的会话
sqlSession = SqlSessionFratoryTools.getSqlSession();
// 获取新的代理对象
userMapper = sqlSession.getMapper(UserMapper.class);
// 再次查询该用户
user = userMapper.selectUserById(1);
System.out.println(user);
}
}

运行结果:

DEBUG [main] Cache Hit Ratio [mapper.UserMapper]: 0.0
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <==      Total: 1
User [id=1, name=小明, sex=男, age=21]
--------------------------------------------
DEBUG [main] Cache Hit Ratio [mapper.UserMapper]: 0.5
User [id=1, name=小明, sex=男, age=21]

代码详解

仔细观察MyBatis的执行结果,日志中有几条以Cache Hit Ratio 开头的语句,这行日志后面输出的值为当前执行方法的缓存命中率。在第一次査询id1User对象时,执行了一条select语句,接下来调用SqlSessionclose()方法,该方法会关闭SqlSession一级缓存,同时会将査询数据保存到二级缓存中。
当第二次获取id1User对象时,重新获得的一级缓存SqlSession中并没有缓存任何对象,但是因为启用了二级缓存,当MyBatis在一级缓存中没有找到id1User对象时,会去二级缓存中查找,所以不会再次执行select语句。

10.5 MyBatis缓存机制

在实际项目开发中,通常对数据库查询的性能要求很高,而MyBatis提供了查询缓存来缓存数据,从而达到提高查询性能的要求。

MyBatis的查询缓存分为一级缓存和二级缓存。

  • 一级缓存是SqlSession级别的缓存,
  • 二级缓存是mapper级别的缓存,二级缓存是多个SqlSession共享的。

MyBatis通过缓存机制减轻数据压力,提高数据库性能。

10.5.1一级缓存(SqlSession级别)

MyBatis的一级缓存是SqlSession级别的缓存。在操作数据库时需要构造SqlSession对象,SqlSession对象中有一个HashMap用于存储缓存数据不同的SqlSession之间的缓存数据区域(HashMap)是互相不影响的
一级缓存的作用域是SqlSession范围的,当在同一个SqlSession中执行两次相同的SQL语句时,

  • 第一次执行完毕会将从数据库中査询的数据写到缓存(内存),
  • 第二次査询时会从缓存中获取数据,不再去底层数据库查询,从而提高查询效率。

需要注意的是,如果SqlSession执行了DML操作(insertupdatedelete),并提交到数据库,MyBatis则会清空SqlSession中的一级缓存,这样做的目的是为了保证缓存中存储的是最新的信息,避免出现脏读现象。
当一个SqlSession结束后该SqlSession中的一级缓存也就不存在了。**Mybatis默认开启一级缓存,不需要进行任何配置**。

缓存机制

MyBatis的缓存机制是基于id进行缓存的,也就是说,MyBatis使用HashMap缓存数据时,是使用对象的id作为key,对象作为value保存的.

示例: OneLevelCacheTest

接下来我们测试MyBatis的一级缓存.

项目结构

展开/折叠
G:\workspace_web2\MyOneLeveldureTest
├─src\
│ ├─db.properties
│ ├─domain\
│ │ └─User.java
│ ├─fractory\
│ │ └─SqlSessionFactoryTools.java
│ ├─log4j.xml
│ ├─mapper\
│ │ ├─UserMapper.java
│ │ └─UserMapper.xml
│ ├─mybatis-config.xml
│ ├─tb_user.sql
│ └─test\
│   ├─SelectAllUserTest.java
│   ├─TestClearCache.java
│   ├─TestClose.java
│   ├─TestDelete.java
│   └─TestSelectFromCache.java
└─WebContent\
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    └─lib\
      ├─ant-1.9.6.jar
      ├─ant-launcher-1.9.6.jar
      ├─asm-5.2.jar
      ├─cglib-3.2.5.jar
      ├─commons-logging-1.2.jar
      ├─javassist-3.22.0-CR2.jar
      ├─log4j-1.2.17.jar
      ├─log4j-api-2.3.jar
      ├─log4j-core-2.3.jar
      ├─mybatis-3.4.5.jar
      ├─mysql-connector-java-5.1.44-bin.jar
      ├─ognl-3.1.15.jar
      ├─slf4j-api-1.7.25.jar
      └─slf4j-log4j12-1.7.25.jar

环境搭建

创建并初始化数据库表

/MyOneLeveldureTest/src/tb_user.sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建数据库表
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`sex` char(2) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入数据
INSERT INTO `tb_user` VALUES ('1', '小明', '男', '21');
INSERT INTO `tb_user` VALUES ('2', '小王', '男', '22');
INSERT INTO `tb_user` VALUES ('3', '小丽', '女', '18');
INSERT INTO `tb_user` VALUES ('4', '小芳', '女', '18');
INSERT INTO `tb_user` VALUES ('5', '小王', '男', '22');

创建持久化对象

/MyOneLeveldureTest/src/domain/User.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package domain;
public class User {
private Integer id;
private String name;
private String sex;
private Integer age;
public User()
{
super();
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "User [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
}
}

工具类

/MyOneLeveldureTest/src/fractory/SqlSessionFactoryTools.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SqlSessionFactoryTools {
private static SqlSessionFactory sqlSessionFactory = null;
static
{
try
{
InputStream mybatisConfigXML = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mybatisConfigXML);

} catch (IOException e)
{
e.printStackTrace();
}
}
public static SqlSession getSqlSession()
{
return sqlSessionFactory.openSession();
}
}

Mapper.xml映射文件

/MyOneLeveldureTest/src/mapper/UserMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="mapper.UserMapper">
<!-- 根据ID查询用户 -->
<select
id="selectUserById"
parameterType="int"
resultType="domain.User"> select * from tb_user where id=#{id}
</select>
<!-- 查询所有用户 -->
<select
id="selectAllUser"
resultType="domain.User"> select * from tb_user
</select>
<delete
id="deleteUserById"
parameterType="int"> delete from tb_user where id=#{id}
</delete>
</mapper>

Mapper接口

/MyOneLeveldureTest/src/mapper/UserMapper.java
1
2
3
4
5
6
7
8
package mapper;
import java.util.List;
import domain.User;
public interface UserMapper {
User selectUserById(Integer id);
List<User> selectAllUser();
void deleteUserById(Integer id);
}

测试从一级缓存中查询数据

/MyOneLeveldureTest/src/test/TestSelectFromCache.java
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
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFratoryTools;
import mapper.UserMapper;
public class TestSelectFromCache {
public static void main(String[] args)
{
SqlSession sqlSession = null;
try
{
sqlSession = SqlSessionFratoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//第一次查询id为1的用户,将会执行SQL语句,从数据库中查询.
User user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------");
System.out.println(user);
//再次查询id为1的用户,该用户已经缓存了,直接在一级缓存中查找,不需要再次执行SQL查找
user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------");
System.out.println(user);
sqlSession.commit();
} catch (Exception e)
{
sqlSession.rollback();
e.printStackTrace();
} finally
{
if (sqlSession != null)
sqlSession.close();
}
}
}
1
2
3
4
5
6
7
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------
User [id=1, name=小明, sex=男, age=21]
-------------------------------------------------
User [id=1, name=小明, sex=男, age=21]

仔细观察MyBatis的执行结果,在第一次查询id1User对象时执行了一条select语句,但是第二次获取id1User对象时并没有执行select语句。
因为此时一级缓存也就是SqlSession缓存中已经缓存了id1User对象,MyBatis直接从缓存中将对象取出来,并没有再次去数据库查询,所以第二次没有再执行select语句。

执行DML语句时会自动清空缓存

/MyOneLeveldureTest/src/test/TestDelete.java
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
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFratoryTools;
import mapper.UserMapper;
public class TestDelete {
public static void main(String[] args)
{
SqlSession sqlSession = null;
try
{
sqlSession = SqlSessionFratoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询id为1的用户,将会执行SQL语句,从数据库中查询.
User user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------");
System.out.println(user);
// 删除掉一个用户,DML(insert,upate,delete)操作会清空一级缓存.
userMapper.deleteUserById(5);
sqlSession.commit();
System.out.println("-------------------------------------------------");
// 再次查询id为1的用户,因为缓存被清空了,将会再次执行select语句.
user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------");
System.out.println(user);
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 出错回滚
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 结束了关闭会话
if (sqlSession != null)
sqlSession.close();
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------
User [id=1, name=小明, sex=男, age=21]
DEBUG [main] ==> Preparing: delete from tb_user where id=?
DEBUG [main] ==> Parameters: 5(Integer)
DEBUG [main] <== Updates: 0
-------------------------------------------------
DEBUG [main] ==> Preparing: select * from tb_user where id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------
User [id=1, name=小明, sex=男, age=21]

仔细观察MyBatis的执行结果,在第一次查询id1User对象时执行了一条select语句,接下来执行了一个delete操作,MyBatis为了保证缓存中存储的是最新的信息,会清空原来的SqlSession缓存。
当第二次获取id1User对象时.一级缓存也就是SqlSession缓存中并没有缓存任何对象,所以MyBatis再次执行select语句去查询id1User对象。
如果注释delete操作的代码:

1
2
//            userMapper.deleteUserById(5);
// sqlSession.commit();

由于并没有执行DML操作并将操作提交到数据库,故此时MyBatis不会清空SqlSession缓存,当再次查询id1User对象时不会执行select语句。
此时运行效果如下:

1
2
3
4
5
6
7
8
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------
User [id=1, name=小明, sex=男, age=21]
-------------------------------------------------
-------------------------------------------------
User [id=1, name=小明, sex=男, age=21]

手动清除一级缓存 调用SqlSession的clearCache方法

/MyOneLeveldureTest/src/test/TestClearCache.java
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
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFratoryTools;
import mapper.UserMapper;
public class TestClearCache {
public static void main(String[] args)
{
SqlSession sqlSession = null;
try
{
sqlSession = SqlSessionFratoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------------");
System.out.println(user);
System.out.println("-------------------------------------------------------");
// 清空一级缓存
sqlSession.clearCache();
// 因为缓存被清除,将再次执行select语句从数据库中获取信息
user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------------");
System.out.println(user);
} catch (Exception e)
{
// 出错回滚
sqlSession.rollback();
} finally
{
if (sqlSession != null)
{
sqlSession.close();
}
}
}
}

运行效果如下:

1
2
3
4
5
6
7
8
9
10
11
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------------
User [id=1, name=小明, sex=男, age=21]
-------------------------------------------------------
DEBUG [main] ==> Preparing: select * from tb_user where id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------------
User [id=1, name=小明, sex=男, age=21]

仔细观察MyBatis的执行结果,在第一次查询id1User对象时执行了一条select语句,接下来调用SqlSessionclearCache()方法,该方法会清空SqlSession缓存。
当第二次获取id1User对象时,因为一级缓存中并没有缓存任何对象,所以MyBatis会再次执行select语句去查询id1User对象。

关闭SqlSession时缓存会被清空

/MyOneLeveldureTest/src/test/TestClose.java
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
package test;
import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFratoryTools;
import mapper.UserMapper;
public class TestClose {
public static void main(String[] args)
{
SqlSession sqlSession = null;
try
{
sqlSession = SqlSessionFratoryTools.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------------");
System.out.println(user);
System.out.println("-------------------------------------------------------");
// 关闭会话,则缓存也会被清除
sqlSession.close();
// 重新获取会话,
sqlSession = SqlSessionFratoryTools.getSqlSession();
// 重新获取mapper接口的代理对象
userMapper = sqlSession.getMapper(UserMapper.class);
// 新的会话中还没有缓存数据,将执行select语句查询数据
user = userMapper.selectUserById(1);
System.out.println("-------------------------------------------------------");
System.out.println(user);
} catch (Exception e)
{
// 出错回滚
sqlSession.rollback();
e.printStackTrace();
} finally
{
if (sqlSession != null)
{
sqlSession.close();
}
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
DEBUG [main] ==>  Preparing: select * from tb_user where id=? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------------
User [id=1, name=小明, sex=男, age=21]
-------------------------------------------------------
DEBUG [main] ==> Preparing: select * from tb_user where id=?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
-------------------------------------------------------
User [id=1, name=小明, sex=男, age=21]

仔细观察MyBatis的执行结果,在第一次查询id1User对象时执行了一条select语句,接下来调用SqlSessionclose()方法,该方法会关闭SqlSession缓存。
当第二次获取id1User对象时,一级缓存也就是SqlSession缓存是一个全新的对象,因为此时一级缓存中并没有缓存任何对象,所以MyBatis会再次执行select语句去查询id1User对象。

10.4 MyBatis事务管理 10.4.3事务的配置创建和使用

1.事务的配置

我们在使用MyBatis时,一般会在MyBatis的根配置文件mybatis-config.xml中定义类似如下的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="pooled">
<property
name="driver"
value="${driver}"/>
<property
name="url"
value="${url}"/>
<property
name="username"
value="${username}"/>
<property
name="password"
value="${password}"/>
</dataSource>
</environment>

environment标签定义了连接某个数据库的信息,其子标签transactionManagertype属性决定我们用什么类型的事务管理机制。

如何查看mybatis源码

mybatis的源码放在github上了,所以到github上即可查看mybatis的源码.地址为:
https://github.com/mybatis/mybatis-3/tree/master/src/main/java/org/apache/ibatis

2.事务工厂的创建

MyBatis事务的创建是交给org.apache.ibatis.transaction.TransactionFactory事务工厂来完成的。如果我们将transactionManager标签的type属性配置为JDBC,那么,在MyBatis初始化解析environment标签时,会根据type="JDBC“创建一个JdbcTransactionFactory工厂.

  • 如果type="JDBC",则MyBatis会创建一个JdbcTransactionFactory的实例;
  • 如果type="MANAGED",则MyBatis会创建一个MangedTransactionFactory的实例。

3.事务工厂TransactionFactory

通过事务工厂TransactionFactory很容易获取到Transaction对象实例。我们以JdbcTransaction为例,看一下JdbcTransactionFactory是怎样生成JdbcTransaction的。
JdbcTransaction源代码如下:
如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class JdbcTransactionFactory implements TransactionFactory {
@Override
public void setProperties(Properties props) {
}
//根据给定的数据库连接Connection创建Transaction
@Override
public Transaction newTransaction(Connection conn) {
return new JdbcTransaction(conn);
}
//根据DateSource,隔离级别,是否自动提交创建Transacion
@Override
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
return new JdbcTransaction(ds, level, autoCommit);
}
}

如上代码所示,JdbcTransactionFactory会创建JDBC类型的Transaction,即JdbcTransaction。类似地,ManagedTransactionFactory也会创建ManagedTransaction。下面我们分别深入解析JdbcTransactionManagedTransaction,看它们到底是怎样实现事务管理的.

4.JdbcTransaction

JdbcTransaction可直接使用JDBC的提交和回滚事务管理机制。它依赖于从dataSource中取得的连接connection来管理transaction的作用域,connection对象的获取被延迟到调用getConnection()方法时。如果将autocomt设置为on,开启状态的话,则它会忽略commitrollback.
也就是说,JdbcTransaction是使用java.sql.Connection上的commitrollback功能来完成事务操作的,JdbcTransaction只是相当于对java.sql.Connection事务处理进行了再次封装,Transaction的事务管理都是通过java.sql.Connection实现的JdbcTransaction源代码实现如下:

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
public class JdbcTransaction implements Transaction {
private static final Log log = LogFactory.getLog(JdbcTransaction.class);
// 数据库连接
protected Connection connection;
// 数据源
protected DataSource dataSource;
// 隔离级别
protected TransactionIsolationLevel level;
// 是否自动提交
protected boolean autoCommit;
public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
dataSource = ds;
level = desiredLevel;
autoCommit = desiredAutoCommit;
}
public JdbcTransaction(Connection connection) {
this.connection = connection;
}
@Override
public Connection getConnection() throws SQLException {
if (connection == null) {
openConnection();
}
return connection;
}
//使用Connection的commit()
@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}
//使用Connection的rollback();
@Override
public void rollback() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + connection + "]");
}
connection.rollback();
}
}
//使用Connection的close()
@Override
public void close() throws SQLException {
if (connection != null) {
resetAutoCommit();
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + connection + "]");
}
connection.close();
}
}
// 省略部分代码.
}

JdbcTransaction类的源代码可以看出,JdbcTransaction就是使用java.sql.Connection上的commitrollback功能来完成事务操作的.

5.ManagedTransaction

ManagedTransaction让容器来管理事务Transaction的整个生命周期,意思就是说,使用ManagedTransactioncommitrollback功能不会对事务有任何的影响,它什么都不会做,它将事务管理的权力移交给了容器。ManagedTransaction源代码如下:

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
public class ManagedTransaction implements Transaction {
private static final Log log = LogFactory.getLog(ManagedTransaction.class);
private DataSource dataSource;
private TransactionIsolationLevel level;
private Connection connection;
private final boolean closeConnection;
public ManagedTransaction(Connection connection, boolean closeConnection) {
this.connection = connection;
this.closeConnection = closeConnection;
}
public ManagedTransaction(DataSource ds, TransactionIsolationLevel level, boolean closeConnection) {
this.dataSource = ds;
this.level = level;
this.closeConnection = closeConnection;
}
@Override
public Connection getConnection() throws SQLException {
if (this.connection == null) {
openConnection();
}
return this.connection;
}
@Override
public void commit() throws SQLException {
// Does nothing
}
@Override
public void rollback() throws SQLException {
// Does nothing
}
@Override
public void close() throws SQLException {
if (this.closeConnection && this.connection != null) {
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + this.connection + "]");
}
this.connection.close();
}
}
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
this.connection = this.dataSource.getConnection();
if (this.level != null) {
this.connection.setTransactionIsolation(this.level.getLevel());
}
}
@Override
public Integer getTimeout() throws SQLException {
return null;
}
}

ManagedTransaction类的源代码可以看出,提交回滚它什么都没有做,也就是说,当使用ManagedTransactionMyBatis的事务是交给容器来操作管理的.

10.4 MyBatis事务管理 10.4.2Transaction接口

MyBatis事务设计的重点是org.apache.ibatis.transaction.Transaction接口.

Transaction接口及其实现类

Transaction接口有两个实现类,分别是:

  • org.apache.ibatis.transaction.jdbc.JdbcTransaction,
  • org.apache.ibatis.transaction.Managed.ManagedTransaction

TransactionFactory接口及其实现类

同时MyBatis还设计了org.apache.ibatis.transaction.TransactionFactory接口用来获取事务的实例对象。

TransactionFactory接口有如下两个实现类:

  • org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory,
  • org.apache.ibatis.transaction.managed.ManagedTransactionFactor6y

数据库事务动作

对数据库的事务而言,应该具有以下几个动作:

  • 创建(create)、
  • 提交(commit)、
  • 回滚(rollback)、
  • 关闭(close)。

Transaction接口

对应地,MyBatis将事务抽象成了Transaction接口。该接口源代码如下:

1
2
3
4
5
6
7
8
9
10
public interface Transaction{
//获取数据库连接
Connection getConnection() throws SQLException;
//提交
void commit() throws SQLException
//回滚
void rollback() throws SQLException
//关闭数据库连接
void close() throws SQLException
}

MyBatis事务管理机制

MyBatis的事务管理分为两种形式:

  • 使用JDBC的事务管理机制。即利用java.sql.Connection对象完成对事务的提交(commit())、回滚(rollback())和关闭(close())等操作。
  • 使用MANAGED的事务管理机制。对于这种机制,MyBatis自身不会去实现事务管理,而是让容器如WebLogicJBoss等来实现对事务的管理。

10.4 MyBatis事务管理

使用MyBatis可以很方便地以面向对象的方式进行数据库访问。在所有的Java语言数据库框架中,数据库的事务管理都是非常重要的一个方面。同时我们也经常需要合理地利用缓存来加快数据库的査询,进而有效地提升数据库的性能。下面将重点介绍MyBatis的事务管理。

10.4.1事务的概念

每个业务逻辑都是由一系列数据库访问完成的,这一系列数据库访问可能会修改多条数据记录,这一系列修改应该是一个整体,绝不能仅修改其中的几条数据记录。也就是说,多个数据库原子访问应该被绑定成一个整体,这就是事务事务是一个最小的逻辑执行单元,整个事务不能分开执行,要么同时执行,要么同时放弃执行

事务是一步或几步操作组成的逻辑执行单元,这些基本操作作为一个整体执行单元,它们要么全部执行,要么全部取消执行,绝不能仅仅执行一部分。
一般而言,一个用户请求对应一个业务逻辑方法,一个业务逻辑方法往往具有逻辑上的原子性,此时应该使用事务。
例如一个转账操作,需要修改对应两个账户的余额(一个减少,一个增加),这两个账户的修改要么同时生效,要么同时取消,同时生效是转账成功,同时取消是转账失败;但不可只修改其中个账户,那将破坏数据库的完整性。

事务的四个特性

通常来讲,事务具备4个特性:

  • 原子性(Atomicity)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持续性(Durability)

这4个特性也简称为ACID特性,具体描述如下:

原子性

原子性( Atomicity)。事务是应用中最小的执行单位,是应用中不可再分的最小逻辑执行体.

一致性

一致性(Consistency)。事务执行的结果,必须使数据库从一种一致性状态,变到另一种一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。
如果系统运行发生中断,某个事务尚未完成而被迫中断,而该未完成的事务对数据库所做的修改已被写入数据库,此时,数据库就处于一种不正确的状态。
比如银行在两个账户之间转账:从A账户向B账户转入1000元。系统先减少A账户的1000元,然后再为B账户增加1000元。

  • 如果全部执行成功,数据库就处于一致性状态。
  • 如果仅执行完A账户金额的修改,而没有增加B账户的金额,则数据库就处于不一致性状态。

因此,一致性是通过原子性来保证的。

隔离性

隔离性(Isolation)。各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务,都是隔离的,即并发执行的事务之间不能互相影响

持续性

持续性(Durability)。持续性也被称为持久性(Persistence),指事务一旦提交,对数据所做的任何改变都要记录到永久存储器中,通常就是保存到物理数据库。

10.3 MyBatis调用存储过程 10.3.5删除数据

在之前创建的mybatis数据库中创建一个删除tb_user表数据的存储过程。SQL脚本如下所示:

1
2
3
4
5
6
7
8
drop procedure if exists delete_user_by_id;
delimiter $$
create procedure delete_user_by_id(IN p_id integer)
begin
delete from tb_user where id=p_id;
end
$$
delimiter ;

mapper.xml映射

1
2
3
4
5
<delete
id="deleteUserById"
parameterType="int"
statementType="CALLABLE"> {call delete_user_by_id(#{id,mode=IN})}
</delete>

mapper接口方法

1
void deleteUserById(Integer id);

测试类

/MyProcedureTest/src/test/DeleteUserByIdTest.java
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
package test;

import org.apache.ibatis.session.SqlSession;
import domain.User;
import fractory.SqlSessionFratoryTools;
import mapper.UserMapper;

public class DeleteUserByIdTest {
public static void main(String[] args)
{
SqlSession sqlSession = null;
// 1.获取SqlSession实例
sqlSession = SqlSessionFratoryTools.getSqlSession();
try
{
// 2.获取Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int id = 1;
User user = userMapper.selectUserById(id);
if (user != null)
{
System.out.println(" 删除:" + user);
userMapper.deleteUserById(id);
}
// 5.提交事务
sqlSession.commit();
} catch (Exception e)
{
// 6.出错回滚
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 7.关闭会话
if (sqlSession != null)
{
sqlSession.close();
}
}
}
}

运行结果

DEBUG [main] ==>  Preparing: {call select_user_by_id(?)} 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <==      Total: 1
DEBUG [main] <==    Updates: 0
    删除:User [id=1, name=小明, sex=男, age=21]
DEBUG [main] ==>  Preparing: {call delete_user_by_id(?)} 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <==    Updates: 1

执行测试类时,先调用”select_user_by_id“存储过程查询id1User数据,如果找到该数据,则调用”delete_user_by_id“存储过程将该User数据删除。