MyBatis使用与原理深入总结

零、前言

对于初阶Java程序员而言,只需在前人们写好的应用框架中编写DAO接口和SQL语句,便能使用MyBatis框架来实现CRUD,实现具体业务。

随着经验逐渐丰富,使用MyBatis的频次变大,需要自己搭建项目,整合数据库驱动程序、连接池、MyBatis、事务和Spring,到这时,我们得深入学习MyBatis的原理,才能很好地完成上述任务,否则,仅参考网络教程和Demo,云里雾里,知其然不知其所以然,存在很大隐患。

这篇总结面对的读者,是接触MyBatis有一段时间的新人,也可以是使用MyBatis好几年的老鸟。

总结内容包括Java JDBC的相关知识、连接池的相关概念、MyBatis的用法和基础语法、MyBatis的原理和源码解析,最后部分是MyBatis和Spring的整合、事务的整合(这段比较有意思,读者可以对照自己的项目,试试更高阶层的整合)。

一、JDBC概念

Java数据库连接(Java Database Connectivity,简称JDBC),是Java语言中用来规范客户端如何与数据库交互的接口。通俗地讲,程序员只需学会这一套接口,便能够操作MySql数据库、Oracle数据库或其他数据库,极大程度地减少了程序员的技能学习投入。

在JDBC中,重要的对象主要有connection/statement/prepareStatement,具体的内容总结在下面这幅脑图中。

值得注意的是,在MySql驱动程序中用Socket和服务器进行通信(其实大部分涉及到网络的组件都是用Socket,万变不离其宗,所以资深程序员是需要熟悉Socket编程的,至少要了解如何使用)。

使用JDBC的示例代码如下,最开始的时候程序员执行一个sql查询操作需要写这么多代码,可见是多么繁琐。

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
‌Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
Class.forName("com.mysql.jdbc.Driver");
// 获取与数据库连接的对象-Connetcion
connection = DriverManager.getConnection(
"jdbc:mysql://10.72.234.31:3306/xxx?characterEncoding=UTF8&allowMultiQueries=true&socketTimeout=60000",
“xxx”,
"dp!@7JAn3My1m");
// 获取执行sql语句的statement对象
statement = connection.createStatement();
// 执行sql语句,拿到结果集
resultSet = statement.executeQuery("select * from OD_OtaConfigInfo");
// 遍历结果集,得到数据
while (resultSet.next()) {
System.out.println(resultSet.getMetaData().toString());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
assert resultSet != null;
resultSet.close();
statement.close();
connection.close();
}

利用元数据可以针对查询结果写转换Util的代码如下,值得注意的是,后面MyBatis中也有类似的处理逻辑,用于将sql结果转成java bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private Object convertResultSet(Class clazz, ResultSet resultSet) {
try {
Object instance = clazz.newInstance();
if (resultSet.next()) {
ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
for (int i = 0; i < resultSetMetaData.getColumnCount(); i++) {
String name = resultSetMetaData.getColumnName(i + 1);
String value = resultSet.getString(i + 1);
try {
// 首字母大写->驼峰
name = name.substring(0, 1).toLowerCase() + name.substring(1);
Field field = clazz.getDeclaredField(name);
field.setAccessible(true);
field.set(instance, value);
} catch (Exception ignored) {
}
}
}
return instance;
} catch (InstantiationException | IllegalAccessException | SQLException ignored) {
}
return null;
}

二、连接池概念

如果了解上面JDBC的概念,且有一定使用经验后,就差不多能想到我们为何需要连接池了。

连接池目前有c3p0/tomcat-jdbc/druid,其中c3p0特别庞大但性能差(尴尬),tomcat-jdbc核心文件少,druid由阿里出品(据说性能最好)。

相关总结在下面这幅图中。

小结:连接池的用途是用来获取连接

三、MyBatis基础与原理

3.1 ORM框架

在使用MyBatis之前,我们可以想一想它的作用是什么?我们为什么需要它。

还记得在第一部分中是如何使用JDBC的吗?我们手动打开Connection和Statement,手动拆解我们的查询参数并填充进sql语句,手动把查询的结果组装成java bean,可谓是历经重重困难。

而MyBatis是一个ORM框架(Object Relational Mapping),ORM框架的意思就是帮我们把java bean映射为sql查询参数,把sql结果映射为java bean,程序员再也不需要纠结sql与java bean之间的转换,再也不需要写result handler。另外,MyBatis也帮我们管理Connection和Statement(这部分被封装进MyBatis的sqlSession中)。

小结:MyBatis内部封装JDBC,帮我们简化数据查询的工作。查询数据时,我们不需要像第一部分那样直接使用JDBC,我们直接使用MyBatis就好了。

3.2 MyBatis使用与原理

3.2.1 引入MyBatis

为了使用MyBatis,需要在项目中引入Mybatis和mysql-connector-java,后者是数据库驱动程序,Mybatis依赖它来与数据库通信交互,依赖的Pom如下。

1
2
3
4
5
6
7
8
9
10
11
12
<!--mysql驱动程序-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<!--mybatis(orm框架)-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
3.2.2 MyBatis全局配置文件

在类路径(一般是resources根目录)下面新建MyBatis的全局配置文件mybatis-config.xml,该文件指定了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
<?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>
<settings>
<setting name="cacheEnabled" value="false" />
<setting name="useGeneratedKeys" value="true" />
<setting name="defaultExecutorType" value="REUSE" />
</settings>

<typeAliases>
<typeAlias alias="User" type="bean.User"/>
</typeAliases>

<environments default="development">
<environment id="development">
<transactionManager type="jdbc"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://10.72.234.31:3306/test_user"/>
<property name="username" value="ewwge"/>
<property name="password" value="dp!@7JAn3My1m"/>
</dataSource>
</environment>
</environments>

<mappers>
<mapper resource="mappers/UserMapper.xml" />
</mappers>
</configuration>

各项配置语义说明

  • settings元素:是否启用MyBatis某项功能特性。
  • environments元素:各个环境的配置,大多数时候线下测试环境和线上生产环境的配置项不一样。每个environment需要有事务管理器transactionManager和数据源dataSource。
    • transactionManager元素:事务管理器,属性type有JDBC或MANAGED 2种值。JDBC表示MyBatis会自动开启和提交事务,MANAGED则表示事务交由应用程序控制,程序员需要手动开启事务,手动提交事务。后面我们把MyBatis和Spring整合在一起,type应该是MANAGED,把事务交给Spring来管理。
    • dataSource元素:即数据源,配置数据库对应的jdbc url和密码等。属性type表示使用的数据源类型,有UNPOOLED、POOLED和JNDI三个选项:,这里的POOLED表示使用MyBati自带的PooledDataSource。而dataSource内部通过property元素配置的属性,都是PooledDataSource支持的。注意不同的数据源实现,可以配置的property是不同的。
  • mappers元素:指明映射文件的位置,对每一个表,我们都会建立一个单独的映射文件,里面存放多条sql语句。当我们调用MyBatis执行sql时,MyBatis会到映射文件中寻找具体的sql语句
    • resource属性:表示映射文件位于classpath下。例如上面的配置中就表示在classpath的mappers目录里有一个UserMapper.xml映射文件。
    • url属性:使用完全限定资源定位符指定映射文件路径(这种用法比较少),如file:///var/mappers/UserMapper.xml
    • class属性:通过java类来配置映射关系,可以一个java映射类对应一个xml映射文件。后面我们删除xml映射文件,把sql以注解的方式写在java类中时会用到class属性。
    • package:如果有多个java映射类,且位于同一个包下面,我们可以直接使用package属性指定包名。
  • typeAliases元素:Mybatis别名配置。配置该元素后,Mybatis会把User当做bean.User,在用到bean.User的地方,我们只需填User,不需要冗长的路径。
3.2.3 MyBatis映射文件

就像上面所说,当我们调用MyBatis执行sql时,MyBatis会到映射文件中寻找具体的sql语句,所以我们需要给OD_YUser表建立对应的映射文件,并将对应的sql语句填入其中。

在类路径(一般是resources根目录)下面新建mappers目录,在mappers目录中新建userMapper.xml文件,内容如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="qiuyongchen-namespace">

<insert id="insertUser" parameterType="User">
insert into OD_YUser(name,age) values(#{name},#{age})
</insert>

<select id="getUser" resultType="User" parameterType="java.lang.String">
select * from OD_YUser where name=#{name}
</select>
</mapper>

各项配置语义说明

  • mapper元素:按照我们的约定,一个mapper即对应着一张数据库中的表。这里需要注意namespace属性,命名空间,稍后我们会看到它的重要性。
  • insert/select等元素:代表着sql语句。这里需要注意的是id属性,稍后我们会看到它的重要性。
    • parameterType属性:参数类型,MyBatis可以将这里指定的java bean类型拆解成sql参数。
    • resultType属性:结果类型,MyBatis可以将sql执行结果解析成resultType指定的java bean。比如查询user表所有行,MyBatis可以将查询结果解析成List

映射文件中的namespace和id两个属性非常重要。试想一下,到目前为止,我们的sql写在xml文件中,而下面我们将在java类中调用MyBatis查询数据库,那MyBatis怎么知道具体的sql是哪一条呢?

答案是namespace.id,MyBatis在启动时扫描所有的映射文件,以namespace.id为key,以id对应的sql语句为value,建立一个map结构,MyBatis会根据这个map来得到sql。

小结:MyBatis用namespace.id来定位具体的sql语句。只要理解这句话,我们差不多就明白了MyBatis的执行流程。

3.2.3 调用MyBatis的SqlSession查询数据库

在测试代码中编写如下的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws IOException {
User user = getUserByName("myName");
System.out.println(user);
}

/**
* 根据用户名查询该用户信息
* @param myName
* @return
* @throws IOException
*/
private static User getUserByName(String myName) throws IOException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(
Resources.getResourceAsReader("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
return sqlSession.selectOne("qiuyongchen-namespace.getUser", myName);
}

上面代码中最重要的部分是SqlSession对象,它也是MyBatis最核心的内容。当我们调用sqlSession.selectOne(“qiuyongchen-namespace.getUser”)时,MyBatis会根据qiuyongchen-namespace.getUser这条namespace.id获取映射文件中对应的sql,也就是select * from OD_YUser where name=#{name},然后执行该sql。

3.2.4 SqlSession的原理

可能有好奇心的同学会问:SqlSession获取到sql语句之后是怎么执行呢,具体的过程是怎么样?

最开始的时候,我们了解了JDBC是怎么执行sql语句,先调用mysql驱动程序与数据库建立socket连接,然后发送sql语句。后面我们又知道MyBatis依赖于JDBC。综合起来,我们脑海中差不多有个模糊的印象:MyBatis很有可能调用了JDBC中的statement对象来执行sql。

下面我们来看看SqlSession执行的时序图。

可见,和我们猜想一致,MyBatis调用JDBC中的statement对象来执行sql

回顾一下,getUserByName目的是根据用户名查询该用户信息,在方法的实现中我们调用sqlSession.selectOne方法,并传入一个namespace.id和参数。传入参数是没有问题的,但是namespace.id属于系统技术实现中映射文件的属性,理论上我们不应该把它掺杂入业务代码中,有没有更好的做法呢?

MyBatis也考虑到了这个问题,于是MyBatis在sqlSession中加入一个getMapper方法,利用这个方法我们无需手动调用sqlSession.selectOne,详情参考下一小节。

3.2.5 简化:调用Mapper接口查询数据库

首先,我们把上面映射文件中的namespace值改成dao.UserMapper,并在dao包下面新建UserMapper接口,内容如下:

1
2
3
4
5
6
package dao;
import bean.User;
public interface UserMapper {
public void insertUser(User user);
public User getUser(String name);
}

然后修改getUserByName方法,内容如下:

1
2
3
4
5
6
7
8
9
10
    private static User getUserByName(String myName) throws IOException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(
Resources.getResourceAsReader("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();

// return sqlSession.selectOne("qiuyongchen-namespace.getUser", myName);

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
return userMapper.getUser("xxx");
}

可以看到我们不是调用sqlSession.selectOne,而是先调用sqlSession.getMapper得到我们刚编写好的UserMapper接口实现,然后调用了UserMapper里边的方法getUser,这样一来代码便优雅了许多。

3.2.5 Mapper接口的原理

上面一小节是怎么做到的呢,getMapper方法里面是怎么实现的?有经验的同学看到UserMapper是个接口,大概就能猜到getMapper里面用代理技术生成实现类,在调用userMapper.getUser时,代理方法里面其实是执行sql。下面我们看看从getMapper方法开始的时序图。

可以看到,我们调用mapper接口,实际上底层仍旧是调用MyBatis的SqlSession,依赖的还是映射文件,所以mapper接口是更高层级的封装。

注:如果mapper接口中方法的返回值是List类型,那么MyBatis会自动调用selectList而不是selectOne。

值得注意的是,这里用mapper接口名当做namespace,用被调用的方法当做id,组装出namespace.id,映射文件里的namespace值必须改成mapper的全路径类名,id值和mapper中被调用方法名必须一样,否则MyBatis会报错。

3.3 MyBatis获取自增主键

MyBatis提供了useGeneratedKeys、keyProperty两个属性,我们在insert中可以用它们来获取数据库自增主键。

useGeneratedKeys:如果这个属性值为true,MyBatis会调用JDBC 的ResultSet.getGeneratedKeys来取出数据库自增主键。

keyProperty:MyBatis会通过getGeneratedKeys 的返回值来设置它的键值,比如keyProperty=”id”,MyBatis会将数据库生成的主键塞到id变量中。

往数据库插入一行数据,获取一个生成的主键,语句如下:

1
2
3
4
5
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>

往数据库中插入批量数据,获取批量生成的主键(注意,直到3.4.0版本才真正支持该功能),语句如下:

1
2
3
4
5
6
7
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username, password, email, bio) values
<foreach item="item" collection="list" separator=",">
(#{item.username}, #{item.password}, #{item.email}, #{item.bio})
</foreach>
</insert>

3.4 MyBatis动态sql

MyBatis的动态sql是个很强大的功能,可以根据具体的参数来构造不同的sql,我们常用的语法有if判断、where判断和foreach循环。

具体的用法可以参考MyBatis官网教程:动态 SQL

3.5 MyBatis结果集映射

前面说过,MyBatis是个ORM框架,它会自动将java bean转成sql参数,也可以自动将sql查询结果转成java bean。这一节的结果集映射指的便是MyBatis将sql查询结果转成java bean。

resultType

如果是类型简单且变量名和数据库字段一一对应的bean,那么我们可以直接用resultType指定java bean,比如User类里面有id、username和password三个变量,且数据库中users表有id、username和password三个字段,那么我们可以在select元素中给resultType属性赋值User,比如下面:

1
2
3
4
5
<select id="selectUsers" resultType="User">
select id, username, password
from users
where id = #{id}
</select>
resultMap

如果java bean中变量名称与数据表中字段名称对不上,或是变量数量与字段数量对不上,那么我们可以用resultMap来做映射。

在实际的开发中,resultMap用的很多,因为java bean一般以驼峰命名,而数据表字段一般都是小写加下划线,字段名不匹配,所以用resultMap做映射。

比如我们建立一个resultMap,

1
2
3
4
<resultMap id="userResultMap" type="User">
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>

resultMap元素中的type属性指明了java bean类型是User;
result元素的property属性是java bean中的变量名;
result元素的column属性是数据库的字段名;

MyBatis提供的resultMap功能非常强大,能够映射特别复杂的类型,它甚至能校验结果,在这里不展开详述,详情可以参考MyBatis官网教程:结果集

四、MyBatis与Spring整合

4.1 引入Spring

前面我们已经学习了如何使用MyBatis(创建SqlSessionFactory,通过该工厂获取SqlSession,最后通过SqlSession执行sql),为了方便,把上面的代码拷贝下来:

1
2
3
4
5
6
7
8
private static User getUserByName(String myName) throws IOException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(
Resources.getResourceAsReader("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
return userMapper.getUser("xxx");
}

我们也吐嘲过,上面的代码过于臃肿,有很多代码是我们不想写的有木有!怎么办呢?与Spring结合,让Spring帮我们生成代码。众所周知,Spring是个IOC框架,可以帮我们管理Java类,需要用哪个对象直接向Spring索要即可。

MyBatis和Spring整合到一起之后,初阶我们可以直接向Spring索要SqlSessionFactory,再高阶一些我们可以忽略SqlSessionFactory直接索要SqlSession,甚至再高阶一些我们直接操作userMapper(大部分Java程序员现在应该是直接操作userMapper)!

为了整合MyBatis和Spring,我们需要引入中间桥梁组件mybatis-spring(当然,Spring也是要事先引入的),比如:

1
2
3
4
5
6
<!--mybatis spring 依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>

在展开具体的整合操作前,请记住一件事,那就是我们下面所有的努力,都是为了减少程序员的工作量,都是为了让代码更加简洁。

4.2 初级阶段-Spring帮我们管理SqlSessionFactory

上面我们用SqlSessionFactoryBuilder来生成SqlSessionFactory,而我们知道Spring本身对工厂bean有非常好的支持,所以mybatis-spring利用了这一点,给我们提供了SqlSessionFactoryBean,SqlSessionFactoryBean实现了 Spring 的 FactoryBean 接口,用于创建SqlSessionFactory对象。

我们将很多配置仍旧保留在核心配置文件中,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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>
<settings>
<setting name="cacheEnabled" value="false" />
<setting name="useGeneratedKeys" value="true" />
<setting name="defaultExecutorType" value="REUSE" />
</settings>

<typeAliases>
<typeAlias alias="User" type="bean.User"/>
</typeAliases>

<mappers>
<mapper resource="mappers/UserMapper.xml" />
</mappers>
</configuration>

在Spring的bean管理xml中增加下面的bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="sqlSessionFactory" class=“org.mybatis.spring.SqlSessionFactoryBean">
<!--数据源配置,必须显示配置一个数据源-->
<property name="dataSource" ref=“dataSource"/>
<!--指定mybatis核心配置文件mybatis-config.xml路径-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="xxx"/>
<property name="password" value="xxyyzz"/>
<property name="url" value="jdbc:mysql://localhost:3306/qyc_test"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>

这样一来,我们可以利用Spring往代码中注入sqlSessionFactory对象,不必使用SqlSessionFactoryBuilder

不过,一般整合了Spring之后,我们就不再直接使用sqlSessionFactory,我们更倾向于使用sqlSession,我们借助mybatis-spring提供的SqlSessionTemplate、SqlSessionDaoSupport来直接使用sqlSession。

4.3 中级阶段-Spring帮我们管理SqlSession

SqlSessionTemplate

mybatis-spring提供的SqlSessionTemplate实现了SqlSession,所以你可以把它当做SqlSession来使用。在使用SqlSessionTemplate之后,我们不再需要通过SqlSessionFactory.openSession()方法来创建SqlSession。

通过分析阅读源码可以知道,实际上执行SqlSessionTemplate.selectOne()时,底层实现仍是通过SqlSessionFactory.openSession()方法来创建SqlSession,然后执行SqlSession.selectOne(),所以SqlSessionTemplate依赖于SqlSessionFactory,bean配置如下:

1
2
3
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>

这样一来,我们可以利用Spring往代码中注入SqlSessionTemplate,把它当做SqlSession来使用

SqlSessionDaoSupport

mybatis提供抽象类SqlSessionDaoSupport,本意是让我们在Dao中继承它,可以使用父类方法getSqlSession()方法得到一个 SqlSessionTemplate。

通过翻阅源码我们可以知道,SqlSessionDaoSupport本质是对SqlSessionTemplate的封装,依赖于SqlSessionTemplate,所以我们得跟上一小节一样往代码中注入SqlSessionTemplate,使用时直接调用父类方法getSqlSession()。

在下面一节中介绍的组件,将会使用到SqlSessionDaoSupport。

4.4 高级阶段-将userMapper给Spring管理

在4.1节的代码中,我们通过sqlSession.getMapper(UserMapper.class)来获取UserMapper接口的实现类,那么,我们能否直接向Spring索取UserMapper的一个实现对象,完全不理会SqlSessionFactory和SqlSession呢?比如像下面这样:

1
2
3
4
5
6
7
public class UserService {
@Autowired
private UserMapper userMapper;
public void insert(User user){
userMapper.insert(user);
}
}

直接这样做是会报错的,因为UserMapper在这里只是个接口,没有具体的实现代码,不能用Spring注入。等等,说到具体实现的代码,我们能想到Java接口代理,对了,我们可以用代理来生成具体的代码。

设想有那么一个组件,可以给UserMapper接口生成代理类,执行UserMapper接口中的方法时,代理方法能够调用底层逻辑去数据库查询数据。听起来是不是有点耳熟,像不像上面我们曾经分析过的SqlSession原理?但是显然我们这里不能用SqlSession,因为我们本来的目的就是要忽视它。

这里我们要介绍的是MapperFactoryBean,它的原理类似SqlSession,可以给UserMapper接口生成代理类。它们的差别在哪儿呢?

在SqlSession生成的代理方法中,主要是根据接口名称和方法名称组装namespace.id,然后执行sqlSession.select();而MapperFactoryBean继承了上面介绍的SqlSessionDaoSupport,在它生成的代理方法中,会调用SqlSessionDaoSupport的getSession()来得到sqlSession,进而执行sqlSession.select(),MapperFactoryBean比SqlSession更高一级。

具体的Spring使用配置如下:

1
2
3
4
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="dao.UserMapper" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

这样一来,我们可以利用Spring往代码中注入userMapper对象,完全不理会SqlSessionFactory和SqlSession

4.5 懒人阶段-让Spring自动扫描Mapper接口

我们在上面几小节中,成功地把userMapper接口交由Spring管理,理论上已经非常方便了,只不过,当我们项目中涉及到的表数量变多,我们就需要在Spring配置中编写注册N多个Mapper Bean。

为了继续节省人力,解放劳动力,mybatis-spring提供了MapperScannerConfigurer,通过它,Spring可以扫描某个包下面的mapper接口,并它们当做MapperFactoryBean,注册进Spring中,配置如下:

1
2
3
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.iloveqyc.dao" />
</bean>

这样一来,Spring启动时会自动到com.iloveqyc.dao目录下寻找接口,并把它们当做Mapper接口对待。

到这里,每次新增一个数据表,我们只需编写Mapper接口和对应的sql语句文件即可(当然,也可以以全注解的方式使用,这里不再展开详述)。

4.6 关于事务

回想一下,在使用JDBC时,我们调用connection.commit来提交事务;使用MyBatis之后,我们调用sqlSession.commit来提交事务(本质上也是执行connection.commit)。现在整合Spring,于是Spring担起了管理事务的职责。

在Spring中,本地事务核心是DataSourceTransactionManager,依托它,Spring提供了声明式(再细分成注解式和切面式)和编程式事务管理,由于切面式事务比较复杂难用,所以这里只介绍声明注解式事务和编程式。

1
2
3
4
<!-- JDBC事务管理器配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="multipleDataSource" />
</bean>
4.6.1 声明注解式事务

首先声明使用主键,如下:

1
<tx:annotation-driven transaction-manager="transactionManager"/>

然后可以在代码方法上打@Transactional,该方法便成为有事务保障的方法,遇到异常自动回滚。

4.6.2 编程式事务

如果觉得切面式事务不够灵活,我们可以往代码中注入事务管理器,手动控制事务的提交与回滚,如下:

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
public class AccountService{
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private AccountMapper accountMapper;
public void transfer(final String out,final String in,final Double money){
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

TransactionStatus status = transactionManager.getTransaction(def);

try {
// 业务逻辑开始
// xxx
// 业务逻辑结束

// 手动提交事务
transactionManager.commit(status);
}
catch (MyException ex) {
// 手动回滚事务
transactionManager.rollback(status);
throw ex;
}
}
}

这种直接使用事务管理器的方式不是特别简介,Spring提供了TransactionTemplate来简化编程式事务代码的编写,如下:

1
2
3
4
5
6
7
8
9
10
11
public void transfer(final String out,final String in,final Double money) {  

transactionTemplate.execute(new TransactionCallbackWithoutResult(){
@Override
protected void doInTransactionWithoutResult(TransactionStatustransactionstatus){
// 业务逻辑开始
// xxx
// 业务逻辑结束
}
});
}

我们只需在doInTransactionWithoutResult方法中编写业务逻辑即可。

五、后话

这篇博文中大部分配置均使用了xml配置,而现在流行纯注解化、去xml化,spring boot更是将纯注解发挥得淋漓尽致,项目中不保留任何一个xml文件。

MyBatis的sql编写、Spring配置、事务配置都支持纯注解使用,读者有兴趣的话,可以深入研究。