MyBatis

持久层技术

  • Jdbc: 手动封装结果集,SQL语句硬编码在Java代码中
  • Jdbi: 对Jdbc更高层次的抽象,使得数据库操作更便捷、高效,和 MyBatis 非常类似
  • MyBatis: 自动封装结果集,SQL语句写在XML配置文件中(适用于对性能要求高而对数据库可移植性无特定要求)
  • JPA: 不用编写SQL语句,可实现跨数据库(适用于对性能要求不高而对数据库可移植性有一定要求)

Jdbc

Jdbc编程

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
* Java SPI(Service Provider Interface)
* SPI是JDK内置的一种服务提供发现机制.
* 当服务的提供者提供了一种接口的实现之后, 需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件, 这个文件里的内容就是这个接口的具体的实现类.
* 当其他的程序需要这个服务的时候, 就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件, 配置文件中有接口的具体实现类名,
* 可以根据这个类名进行加载实例化, 然后就可以使用该服务了, JDK中查找服务实现的工具类是: {@link java.util.ServiceLoader}
* <p>
* 双亲委派机制: Java类通过Classloader加载, Classloader之间有继承关系, AppClassLoader继承ExtClassloader, ExtClassloader继承BootstrapClassloader.
* 在类加载时, 子加载器会调用父加载器来加载类, 如果父加载器加载失败, 才会交给子加载器来加载; 如果子加载器也加载失败, 那么就报异常.
* <p>
* 那么SPI是如何双亲委派机制?
* 以Jdbc加载驱动为例: SPI实现方式为, 通过ServiceLoader.load(Driver.class)方法, 去各自实现Driver接口的lib下的META-INF/services/java.sql.Driver文件里找到实现类的名字,
* 通过Thread.currentThread().getContextClassLoader()类加载器加载实现类并返回实例.
* 如果不用Thread.currentThread().getContextClassLoader()加载器加载, 整个流程会怎么样?
* (1) 从META-INF/services/java.sql.Driver文件得到实现类名字com.mysql.jdbc.Driver
* (2) Class.forName("com.mysql.jdbc.Driver")来加载实现类
* (3) Class.forName(className)默认使用当前类的ClassLoader, JDBC是在DriverManager类里调用Driver的, 当前类也就是DriverManager, 它的加载器是BootstrapClassLoader.
* (4) 用BootstrapClassLoader去加载非rt.jar包里的类com.mysql.jdbc.Driver就会找不到
* (5) 要加载com.mysql.jdbc.Driver需要用到AppClassLoader或其他自定义ClassLoader
* (6) 最终矛盾出现在: 要在BootstrapClassLoader加载的类里调用AppClassLoader去加载实现类
* <p>
* 如何在父加载器加载的类中去调用子加载器去加载类?
* JDK提供了两种方式: Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader(), 一般都指向AppClassLoader, 它们能加载classpath中的类.
*/
public abstract class JdbcUtils {

private static String url;
private static String username;
private static String password;

static {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
InputStream in = classLoader.getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
prop.load(in);
url = prop.getProperty("jdbc.url");
username = prop.getProperty("jdbc.username");
password = prop.getProperty("jdbc.password");
/**
* classLoader.loadClass(className): 仅加载类不执行类初始化
* class.forName(className): 加载指定类并执行类初始化(类初始化其实就是给static变量赋予用户指定的值以及执行静态代码块)
* <code>
* static {
* try {
* java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
* } catch (SQLException E) {
* throw new RuntimeException("Can't register driver!");
* }
* }
* </code>
*/
Class.forName(prop.getProperty("jdbc.driver"));
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}

public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}

public static void close(ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

public static void close(Statement stmt) {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

public static void close(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// 此处不能往为抛(要确保后面的资源也能一并释放掉)
e.printStackTrace();
}
}
}

}

public class JdbcTest {

@Test
public void testQuery() {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
String sql = "SELECT id, code, name, pass FROM sys_user WHERE id = ?";
conn = JdbcUtils.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, 101L);
rs = pstmt.executeQuery();
List<User> list = new ArrayList<>();
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setCode(rs.getString("code"));
user.setName(rs.getString("name"));
user.setPass(rs.getString("pass"));
list.add(user);
}
Assert.assertTrue(list.size() == 1);
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.close(rs);
JdbcUtils.close(pstmt);
JdbcUtils.close(conn);
}
}

@Test
public void testDelete() {
Connection conn = null;
PreparedStatement pstmt = null;
try {
String sql = "DELETE FROM sys_user WHERE id = ?";
conn = JdbcUtils.getConnection();
conn.setAutoCommit(Boolean.FALSE);
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, 105L);
int row = pstmt.executeUpdate();
Assert.assertTrue(row == 1);
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
} finally {
JdbcUtils.close(pstmt);
JdbcUtils.close(conn);
}
}

}

Jdbc编程的问题

  • 数据库连接:使用时就创建,不使用则立即释放,对数据库进行频繁连接的开启和关闭,造成数据库资源的浪费,影响数据库性能。
    设想解决方案:使用数据库连接池管理数据库连接。
  • SQL语句:SQL语句硬编码在Java代码中,不利于系统的维护。
    设想解决方案:将SQL语句编写在XML配置文件中。
  • 输入参数:SQL语句的输入参数硬编码在Java代码中,不利于系统的维护。
    设想解决方案:将SQL语句的输入参数配置在XML文件中。
  • 输出结果:遍历查询结果集数据时,存在硬编码,不利于系统的维护。
    设想解决方案:将查询的结果集自动映射成Java对象。

MyBatis配置文件

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
67
68
69
70
71
72
73
74
75
76
<?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="jdbc.properties"/>

<!-- 全局设置 -->
<settings>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>

<!-- 别名设置 -->
<typeAliases>
<!-- 内建的常见类型别名 -->
<!--
<typeAlias alias="_byte" type="byte"/>
<typeAlias alias="_long" type="long"/>
<typeAlias alias="_short" type="short"/>
<typeAlias alias="_int" type="int"/>
<typeAlias alias="_integer" type="int"/>
<typeAlias alias="_double" type="double"/>
<typeAlias alias="_float" type="float"/>
<typeAlias alias="_boolean" type="boolean"/>
<typeAlias alias="string" type="String"/>
<typeAlias alias="byte" type="Byte"/>
<typeAlias alias="long" type="Long"/>
<typeAlias alias="short" type="Short"/>
<typeAlias alias="int" type="Integer"/>
<typeAlias alias="integer" type="Integer"/>
<typeAlias alias="double" type="Double"/>
<typeAlias alias="float" type="Float"/>
<typeAlias alias="boolean" type="Boolean"/>
<typeAlias alias="date" type="Date"/>
<typeAlias alias="decimal" type="BigDecimal"/>
<typeAlias alias="bigdecimal" type="BigDecimal"/>
<typeAlias alias="object" type="Object"/>
<typeAlias alias="map" type="Map"/>
<typeAlias alias="hashmap" type="HashMap"/>
<typeAlias alias="list" type="List"/>
<typeAlias alias="arraylist" type="ArrayList"/>
<typeAlias alias="collection" type="Collection"/>
<typeAlias alias="iterator" type="Iterator"/>
-->
<!-- MyBatis自动扫描所指定包中的类并自动创建别名(别名就是类名且大小写均可) -->
<!--<package name="org.mybatis.api.model"/>-->
</typeAliases>

<environments default="development">
<environment id="development">
<!-- 使用JDBC事务管理(由MyBatis管理) -->
<transactionManager type="JDBC"/>
<!-- 使用数据库连接池(由MyBatis管理) -->
<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>

<!-- 映射文件 -->
<mappers>
<!--<mapper resource="org/mybatis/core/mapper/UserMapper.xml"/>-->
<!-- Mapper代理开发(当映射文件和Mapper接口在同级目录下时,可让MyBatis自动扫描指定包下的映射文件) -->
<package name="org.mybatis.core.mapper"/>
</mappers>

</configuration>

DAO开发的两种方式

原始DAO开发

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 class UserDaoImpl implements UserDao {

private SqlSessionFactory sqlSessionFactory;

public UserDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}

@Override
public int insert(User user) throws Exception {
// 由于SqlSession不是线程安全的,所以不可以将SqlSession定义为成员变量
SqlSession sqlSession = null;
try {
sqlSession = sqlSessionFactory.openSession();
int row = sqlSession.insert("user.insert", user);
sqlSession.commit();
return row;
} finally {
if (sqlSession != null) {
sqlSession.close();
}
}
}

}

原始DAO开发的问题:(1)DAO接口实现类方法中存在大量模板方法:(2)调用sqlSession方法时statement的id存在硬编码。

Mapper代理开发

Mapper接口遵循以下开发规范,MyBatis就可以自动生成Mapper接口实现类的代理对象,开发人员无需自己编写接口实现类:

  • 映射文件Mapper.xml中的namespace为Mapper接口的地址
  • 映射文件Mapper.xml中statement的id与Mapper接口中的方法名一致
  • 映射文件Mapper.xml中statement的parameterType所指定的输入参数类型与Mapper接口中方法参数的类型一致
  • 映射文件Mapper.xml中statement的resultType所指定的输出参数类型与Mapper接口中方法返回值的类型一致

mybatis-config.xml配置文件

Mapper接口

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 org.mybatis.core.mapper;

import java.util.List;

import org.mybatis.api.model.User;
import org.mybatis.api.model.UserExample;

public interface UserMapper {

int insert(User record);

int insertSelective(User record);

int deleteByPrimaryKey(Long id);

int updateByPrimaryKey(User record);

int updateByPrimaryKeySelective(User record);

User selectByPrimaryKey(Long id);

List<User> selectByExample(UserExample example);

}

Mapper映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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="org.mybatis.core.mapper.UserMapper">

<insert id="insert" parameterType="org.mybatis.api.model.User">
INSERT INTO sys_user (code, name, pass, salt, status)
VALUES (
#{code,jdbcType=VARCHAR},
#{name,jdbcType=VARCHAR},
#{pass,jdbcType=VARCHAR},
#{salt,jdbcType=VARCHAR},
#{status,jdbcType=CHAR}
)
</insert>

</mapper>

输入映射

  • ${}表示拼接符号,使用字符串拼接,直接参与sql编译,从而不能避免注入攻击。
  • #{}表示占位符,使用PreparedStatement,sql语句会预编译在数据库系统中,不仅提高了安全性,还提高了执行效率。
  • 如果输入的参数是基本数据类型,那么#{}中用于接收输入参数的变量名称可以任意。

问题:为什么使用PreparedStatement就能防止SQL注入?
原因:SQL语句在程序运行前已经进行了预编译,在程序运行时第一次操作数据库之前,SQL语句已经被数据库分析、编译和优化,对应的执行计划也会缓存下来并允许数据库以参数化的形式进行查询,当运行时动态地把参数传给PreparedStatement时,即使参数里有敏感字符(如’ OR 1=1’),数据库也会把它作为参数字段的属性值来处理而不会作为一个SQL指令,这样就有效地防止了SQL注入。

输出映射

使用resultType进行输出映射时,只有查询出来的列名和映射的POJO属性名一致时才能映射成功;而如果查询的列名和映射的POJO属性名不一致,则可通过resultMap来设置列名和属性名之间的对应关系来完成映射。

动态SQL

MyBatis的强大特性之一便是它的动态SQL。如果你有使用JDBC或其他类似框架的经验,你就能体会到根据不同条件拼接SQL语句有多么痛苦。

查询缓存

  • 缓存在查询时候生效,一般也需要在新增、修改或删除时更新
  • Mybatis支持二级缓存机制,对应到不同的executor实现,BaseExecutor(普通的执行器, 包含一级缓存),CachingExecutor(二级缓存)

默认情况下,SELECT是使用缓存的,而INSERT/UPDATE/DELETE是不使用缓存的。

一级缓存

  • Mybatis默认是启用一级缓存的,无需对框架进行任何设置。
  • SqlSession是线程不安全的,所以在开发中我们一般将其与线程进行绑定或者将其声明在方法内部,目的是让多线程不共享SqlSession。SqlSessionTemplate就是将SqlSession和线程进行绑定,从而保证其线程安全。
  • Mybatis的一级缓存生效的范围是SqlSession,是为了在SqlSession没有关闭时,业务需要重复查询相同数据使用的。一旦SqlSession关闭,则由这个SqlSession缓存的数据将会被清空。
  • 如果SqlSession执行了commit(insert、delete、update)操作,则会清空SqlSession中的一级缓存,以保证缓存中保存的是最新信息,避免脏读。
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@Slf4j
public class UserMapperTest {

private SqlSessionFactory sqlSessionFactory;

@Before
public void setUp() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}

@Test
public void testPrimaryCache1() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
log.info("{}", userMapper.selectByPrimaryKey(101L)); // 发出SQL语句
log.info("{}", userMapper.selectByPrimaryKey(101L)); // 不发出SQL语句
}
}

@Test
public void testPrimaryCache2() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
log.info("{}", userMapper.selectByPrimaryKey(101L)); // 发出SQL语句
sqlSession.commit(); // 清空缓存
log.info("{}", userMapper.selectByPrimaryKey(101L)); // 发出SQL语句
}
}

}

public abstract class BaseExecutor implements Executor {

protected PerpetualCache localCache;

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// ......
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 这个主要是处理存储过程用的
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 如果查不到的话就从数据库查(在queryFromDatabase中会对localCache进行写入)
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
// ......
return list;
}

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}

}

public class PerpetualCache implements Cache {

private final String id;

private Map<Object, Object> cache = new HashMap<Object, Object>();

public PerpetualCache(String id) {
this.id = id;
}

@Override
public String getId() {
return id;
}

@Override
public int getSize() {
return cache.size();
}

@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}

@Override
public Object getObject(Object key) {
return cache.get(key);
}

@Override
public Object removeObject(Object key) {
return cache.remove(key);
}

@Override
public void clear() {
cache.clear();
}

@Override
public ReadWriteLock getReadWriteLock() {
return null;
}

}

二级缓存

  • Mybatis的二级缓存是Mapper级别(按照namespace分)的,当多个SqlSession去操作同一个Mapper的sql语句时,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
  • 每一个namespace的Mapper有一个二级缓存区域,如果两个Mapper的namespace相同,这两个Mapper执行的SQL查询到的数据将保存在相同的二级缓存区域中;当这个namespace中执行了非SELECT语句时,整个namespace中的缓存会被清空。
  • 由于MyBatis的二级缓存是基于namespace的,多表查询语句所在的namspace无法感应到其他namespace中的语句对多表查询中涉及的表进行的修改,引发脏数据问题,所以Mybatis的二级缓存不适用于多表查询的情况。
  • Mybatis默认是不开启二级缓存的,需要进行手动配置。
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
@Slf4j
public class UserMapperTest {

private SqlSessionFactory sqlSessionFactory;

@Before
public void setUp() throws Exception {
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
inputStream.close();
}

@Test
public void testSecondaryCache1() {
try (SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession()) {

UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
log.info("{}", userMapper1.selectByPrimaryKey(101L)); // 发出SQL语句
sqlSession1.commit(); // 执行SELECT的commit操作会将SqlSession中的数据存入二级缓存区域

UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
log.info("{}", userMapper2.selectByPrimaryKey(101L)); // 不发出SQL语句
}
}

@Test
public void testSecondaryCache2() {
try (SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession()) {

UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user = userMapper1.selectByPrimaryKey(101L); // 发出SQL语句
log.info("{}", user);
user.setEmail("admin@gmail.com");
userMapper1.updateByPrimaryKey(user);
sqlSession1.commit(); // 当执行了非SELECT语句时整个namespace中的缓存会被清空

UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
log.info("{}", userMapper2.selectByPrimaryKey(101L)); // 发出SQL语句
}
}

}

public class CachingExecutor implements Executor {

private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager();

@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
flushCacheIfRequired(ms);
return delegate.update(ms, parameterObject);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}


@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}

@Override
public void clearLocalCache() {
delegate.clearLocalCache();
}

/**
* 在默认的设置中SELECT语句不会刷新缓存, INSERT/UPDATE/DELETE会刷新缓存
*/
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}

}

public class TransactionalCacheManager {

private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

public void clear(Cache cache) {
getTransactionalCache(cache).clear();
}

public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}

public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}

public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}

public void rollback() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.rollback();
}
}

private TransactionalCache getTransactionalCache(Cache cache) {
TransactionalCache txCache = transactionalCaches.get(cache);
if (txCache == null) {
txCache = new TransactionalCache(cache);
transactionalCaches.put(cache, txCache);
}
return txCache;
}

}

插件机制

MyBatis允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
@Intercepts({@Signature(type = Executor.class, method = "query", args = {
MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {

private static final Logger LOGGER = LoggerFactory.getLogger(PageInterceptor.class);

private static final String SPACE = " ";
private static final String MULTI_SPACE_PATTERN = " +";
private static final String NEWLINE_PATTERN = "[\\n\\r\\t]";

private static final int MAPPED_STATEMENT_INDEX = 0;
private static final int PARAMETER_INDEX = 1;
private static final int ROW_BOUNDS_INDEX = 2;
private static final int RESULT_HANDLER_INDEX = 3;

@Override
public Object intercept(Invocation invocation) throws Throwable {
final Object[] args = invocation.getArgs();
final Object parameter = args[PARAMETER_INDEX];
if (parameter instanceof Pageable) {
Pageable<?> pageable = (Pageable<?>) parameter;
final MappedStatement ms = (MappedStatement) args[MAPPED_STATEMENT_INDEX];
final BoundSql boundSql = ms.getBoundSql(parameter);
int page = pageable.getPage();
int pageSize = pageable.getPageSize();

DialectCountHolder holder = getCount(ms, boundSql);
DialectHandler dialectHandler = holder.getDialectHandler();
int totalItems = holder.getCount();

String limitSql = dialectHandler.getLimitString(boundSql.getSql(), (page - 1) * pageSize, pageSize);
args[ROW_BOUNDS_INDEX] = new RowBounds(RowBounds.NO_ROW_OFFSET, RowBounds.NO_ROW_LIMIT);
args[MAPPED_STATEMENT_INDEX] = newMappedStatement(ms, boundSql, limitSql);
List<?> items = (List<?>) invocation.proceed();

return Collections.singletonList(new PageImpl<>(page, pageSize, items, totalItems));
}
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}

/**
* @see org.springframework.jdbc.core.JdbcTemplate#execute
* @see DataSourceUtils
* @see <a href="https://www.ibm.com/developerworks/cn/java/j-lo-spring-ts1/">Spring Transaction Manager</a>
*/
private DialectCountHolder getCount(MappedStatement ms, BoundSql boundSql) throws SQLException {
DataSource dataSource = ms.getConfiguration().getEnvironment().getDataSource();
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
// 首先尝试从事务上下文中获取连接, 失败后再从数据源获取连接
conn = DataSourceUtils.getConnection(dataSource);
DialectHandler dialectHandler = getDatabaseDialect(conn);
String countSql = dialectHandler.getCountString(boundSql.getSql());
stmt = conn.prepareStatement(countSql);
LOGGER.debug("==> Preparing: {}", countSql.replaceAll(NEWLINE_PATTERN, SPACE).replaceAll(MULTI_SPACE_PATTERN, SPACE));
BoundSql countBoundSql = newBoundSql(ms, boundSql, countSql);
ParameterHandler handler = new DefaultParameterHandler(ms, boundSql.getParameterObject(), countBoundSql);
handler.setParameters(stmt);
LOGGER.debug("==> Parameters: {}", JsonUtils.toJson(boundSql.getParameterObject()));
rs = stmt.executeQuery();
int count = 0;
if (rs.next()) {
count = rs.getInt(1);
}
LOGGER.debug("<== Total: {}", count);
return new DialectCountHolder(dialectHandler, count);
} finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
// 释放连接, 放回到连接池中
DataSourceUtils.releaseConnection(conn, dataSource);
}
}

private MappedStatement newMappedStatement(MappedStatement ms, BoundSql boundSql, String sql) {
return newMappedStatement(ms, p -> newBoundSql(ms, boundSql, sql));
}

private BoundSql newBoundSql(MappedStatement ms, BoundSql boundSql, String sql) {
final BoundSql newBoundSql = new BoundSql(ms.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}
return newBoundSql;
}

/**
* @see org.apache.ibatis.builder.MapperBuilderAssistant
*/
private MappedStatement newMappedStatement(MappedStatement ms, SqlSource sqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), sqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.cache(ms.getCache());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
return builder.build();
}

/**
* @see org.apache.ibatis.mapping.VendorDatabaseIdProvider#getDatabaseProductName
*/
private DialectHandler getDatabaseDialect(Connection conn) throws SQLException {
String productName = conn.getMetaData().getDatabaseProductName();
return DialectHandlerFactory.getDialectHandler().stream()
.filter(h -> h.supportsType(Database.fromType(productName)))
.findFirst()
.orElseThrow(() -> new RuntimeException("Unsupported database type: " + productName));
}

private static class DialectCountHolder {

private final DialectHandler dialectHandler;
private final int count;

DialectCountHolder(DialectHandler dialectHandler, int count) {
this.dialectHandler = dialectHandler;
this.count = count;
}

DialectHandler getDialectHandler() {
return dialectHandler;
}

int getCount() {
return count;
}

}

}

Spring整合MyBatis

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<context:property-placeholder location="classpath:application.properties"/>

<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
<property name="connectionTimeout" value="${ds_connectionTimeout:30000}"/>
<property name="maximumPoolSize" value="${ds_maxPoolSize:10}"/>
<property name="minimumIdle" value="${ds_minIdle:5}"/>
<property name="idleTimeout" value="${ds_idleTimeout:180000}"/>
<property name="driverClassName" value="${ds_driver}"/>
<property name="jdbcUrl" value="${ds_url}"/>
<property name="username" value="${ds_username}"/>
<property name="password" value="${ds_password}"/>
<property name="autoCommit" value="false"/>
<property name="readOnly" value="false"/>
</bean>

<!--
声明式事务有两种方式:
(1)基于 @Transactional 注解方式(更灵活)
(2)基于XML配置文件方式(事务方法命名要遵循指定的规则)
说明:
(1)若两种方式同时存在, 为避免执行两次 {@link TransactionInterceptor#invoke} 方法, 被 @Transactional 标注的方法的命名不能与XML方式中的事务方法命名规则相同;
(2)注解 @Transactional 只应用到public方法和自调用问题, 是由于使用Spring AOP代理造成的, 为解决这两个问题, 使用AspectJ取代Spring AOP代理.
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>

<!-- Integration MyBatis -->

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="${mybatis.configLocation}"/>
<property name="databaseIdProvider">
<bean class="org.apache.ibatis.mapping.VendorDatabaseIdProvider">
<property name="properties">
<props>
<prop key="MySQL">mysql</prop>
<prop key="Oracle">oracle</prop>
</props>
</property>
</bean>
</property>
<!-- 配置SQL映射文件(默认与Mapper类路径相同) -->
<!--<property name="mapperLocations" value="classpath:mapper/*Mapper.xml"/>-->
<!-- 自动创建别名(别名就是类名且大小写均可, 注意此处不支持使用通配符) -->
<property name="typeAliasesPackage" value="${mybatis.typeAliasesPackage}"/>
<!-- 注意:只有纳入Spring容器管理的Bean方可进行依赖注入. -->
<property name="plugins">
<array>
<bean class="com.gavin.ssm.sys.core.plugin.PageInterceptor"/>
</array>
</property>
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--
PropertyPlaceholderConfigurer作为一个BeanFactoryPostProcessor会在载入所有BeanDefinition后运行,
然后利用指定的properties文件来替换BeanDefinition中定义的${}占位符.
而MapperScannerConfigurer作为一个BeanDefinitionRegistryPostProcessor会在Spring扫描Bean定义时回调,
远早于BeanFactoryPostProcessor, 所以变量${mybatis.typeHandlersPackage}没有被替换.
-->
<property name="processPropertyPlaceHolders" value="true"/>
<!-- optional unless there are multiple session factories defined -->
<!--<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>-->
<!--<property name="sqlSessionTemplateBeanName" value="batchSqlSessionTemplate"/>-->
<!--
You can set more than one package by using a semicolon or comma as a separator.
<a href="https://github.com/mybatis/spring/issues/144">Placeholder not resolved for MapperScannerConfigurer</a>
-->
<property name="basePackage" value="${mybatis.typeHandlersPackage}"/>
</bean>

<!--
关于MyBatis的BATCH模式:
(1)Mybatis内置的ExecutorType有三种, 默认的是SIMPLE, 该模式下它为每个语句的执行创建一个新的预处理语句, 单条提交sql; 而BATCH模式重复使用已经预处理的语句, 并且批量执行所有更新语句, 显然BATCH性能将更优;
(2)BATCH模式也有自己的问题, 比如在insert操作时, 在事务没有提交之前, 是没有办法获取到自增的id; 此外, 对于insert、update、delete无法返回更新条数, 这在某型情形下是不符合业务要求的;
(3)在同一事务中BATCH模式和SIMPLE模式之间无法转换, 所以碰到需要批量更新时, 需要在单独的事务中进行.
-->
<bean id="batchSqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
<constructor-arg index="1" value="BATCH"/>
</bean>

</beans>