MyBatis 入门教程之一

大纲

前言

MyBatis 简介

MyBatis 是支持定制化 SQL、存储过程以及高级映射的持久层框架(ORM)。MyBatis 可以使用简单的 XML 或注解用于配置和映射数据表,将 POJO(Plain Old Java Objects,普通的 Java 对象)映射成数据表中的记录。

MyBatis 历史

MyBatis 的前身是 Apache 的一个开源项目 iBatis,2010 年这个项目由 Apache Software Foundation 迁移到了 Google Code,并且改名为 MyBatis,最后于 2013 年 11 月 迁移到了 GitHub(至今)。iBATIS 一词来源于 “internet” 和 “abatis” 的组合,是一个基于 Java 的持久层框架。iBATIS 提供的持久层框架包括 SQL Maps 和 Data Access Objects(DAO)。

MyBatis 特点

由于单纯的 JDBC 是将 SQL 写在代码块里,耦合度高且维护不易,所以就诞生了 ORM 框架,诸如 Hibernate、Mybatis 等。MyBatis 和 Hibernate 都是 对 JDBC 更加抽象的封装,底层都是 JDBC,这二者的区别在于 MyBatis 是一个半自动的持久化层框架,而 Hibernate 是一个全自动化的持久化层框架。为什么呢?我们知道 Hibernate 是旨在消除 SQL 语句,所以当我们使用 Hibernate 时我们可以不写一条 SQL,全交给框架来处理,但是在实际的开发过程中,针对特定的场景我们是需要自己定制优化 SQL 的,针对于此,Hibernate 提出了 HQL(与标准 SQL 类似,但是倾向于面向对象的风格),为此我们还需要学习下 HQL。而 MyBatis 与 Hibernate 最大的不同就是,MyBatis 是让我们自己编写 SQL 语句。可以看出这两者之间没有绝对的壁垒,如何选择就要视情况来定。更多 MyBatis 相关的资料可阅读 MyBatis 官方中文文档

持久化层技术对比

  • JDBC
    – SQL 写在 Java 代码块里,耦合度高导致硬编码内伤
    – 维护不易,且实际开发需求中 SQL 往往是经常变化的,频繁修改的情况会经常出现

  • MyBatis

    • 半自动的持久化框架
    • 容易对 SQL 进行针对性的优化
    • SQL 和 Java 分开编写,功能边界清晰,一个专注于业务,另一个专注于数据
  • Hibernate 和 JPA

    • 反射操作太多,导致数据库性能下降
      – 内部自动生成 SQL,不容易做特殊优化
      – 长难复杂 SQL 对于 Hibernate 而言处理也不容易
      – 基于全映射的全自动框架,拥有大量字段的 POJO 进行部分映射时比较困难,导致数据库性能下降

快速入门

本节会介绍如何开发第一个 MyBatis 应用,所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-1

创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

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
<properties>
<jdk.version>1.8</jdk.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<junit.version>4.12</junit.version>
<slf4j.version>1.7.30</slf4j.version>
<log4j.version>1.2.17</log4j.version>
<mybatis.version>3.5.6</mybatis.version>
<mybatis-generator>1.4.0</mybatis-generator>
<mysql-connector.version>8.0.23</mysql-connector.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector.version}</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>

<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>${mybatis-generator}</version>
</dependency>
</dependencies>
</dependencyManagement>

创建 Maven 子模块工程

在 Maven 子模块的 pom.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
40
41
<artifactId>mybatis-lesson-1</artifactId>
<name>mybatis-lesson-1</name>

<parent>
<groupId>com.clay.mybatis</groupId>
<artifactId>mybatis-share</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
</dependencies>

在 Maven 子模块中创建实体类

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
package com.clay.mybatis.bean;

public class Employee {

private Long id;
private String lastName;
private String gender;
private String email;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

@Override
public String toString() {
return this.id + "_" + this.lastName + "_" + this.gender + "_" + this.email;
}

}

在 Maven 子模块中创建 SQL 映射文件,主要内容是在命名空间 com.clay.mybatis.mapper.EmployeeMapper 中定义了一个名为 selectEmployee 的映射语句,这样就可以在 Java 代码中使用全限定名 com.clay.mybatis.mapper.EmployeeMapper.selectEmployee 来调用唯一的 SQL 映射语句

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- 指定命名空间 -->
<mapper namespace="com.clay.mybatis.mapper.EmployeeMapper">
<select id="selectEmployee" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email from t_employee where id = #{id}
</select>
</mapper>

提示

当 Java Bean 的属性名与表字段名不一样时,在 SQL 中需要使用字段别名来映射。例如上面 last_name 字段的别名是 lastName,前者是表字段的名称,后者是 Java Bean 属性的名称。

在 Maven 子模块中的 src/main/resources 目录下创建 db.properties 配置文件,其中包含了连接 MySQL 数据库所需的基础信息

1
2
3
4
dataSource.user=root
dataSource.password=123456
dataSource.driverClass=com.mysql.cj.jdbc.Driver
dataSource.jdbcUrl=jdbc:mysql://127.0.0.1:3306/mybatis_lesson?characterEncoding=utf8&autoReconnect=true&useSSL=false&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC

在 Maven 子模块中的 src/main/resources 目录下创建 log4j.properties 配置文件,利用 Log4j 打印 MyBatis 的 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
log4j.rootLogger = DEBUG,console

### 输出到控制台 ###
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %d{ABSOLUTE} %5p %c{1}:%L - %m%n

### 输出到日志文件 ###
log4j.appender.file = org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.File = ${uplat.root}/WEB-INF/logs/platform.log
log4j.appender.file.DatePattern=_yyyyMMdd'.log'
#log4j.appender.file.Append = true
#log4j.appender.file.Threshold = INFO
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern =%-d{yyyy-MM-dd HH\:mm\:ss} [ %t\:%r ] - [ %p ] %m%n

### 打印MyBatis的SQL ###
#log4j.logger.com.ibatis=DEBUG
#log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG
#log4j.logger.com.ibatis.common.jdbc.ScriptRunner=DEBUG
#log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
#log4j.logger.java.sql.ResultSet=DEBUG

在 Maven 子模块中的 src/main/resources 目录下创建 MyBatis 的主配置文件 mybatis-config.xml,其中包含了 MyBatis 的核心设置,包括获取数据库连接实例的数据源(DataSource)、决定事务作用域和控制方式的事务管理器(TransactionManager)以及注册 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
27
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 读取db.properties文件 -->
<properties resource="db.properties" />

<environments default="development">
<environment id="development">
<!-- 配置事务 -->
<transactionManager type="JDBC" />
<!-- 配置数据源 -->
<dataSource type="POOLED">
<property name="url" value="${dataSource.jdbcUrl}" />
<property name="username" value="${dataSource.user}" />
<property name="password" value="${dataSource.password}" />
<property name="driver" value="${dataSource.driverClass}" />
</dataSource>
</environment>
</environments>

<!-- SQL映射文件 -->
<mappers>
<mapper resource="com/clay/mybatis/dao/EmployeeMapper.xml" />
</mappers>
</configuration>

在 Maven 子模块中创建主启动类,每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的,SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 提供了一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。

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 com.clay.mybatis;

import com.clay.mybatis.bean.Employee;

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

import java.io.IOException;
import java.io.InputStream;


public class MyBatisApplication {

public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession session = sqlSessionFactory.openSession();
try {
// 使用SqlSession实例,通过全限定名直接执行已经映射的SQL语句,参数说明如下:
// 1) 全限定名(SQL映射文件中的命名空间 + id):statement Unique identifier matching the statement to use.
// 2) 执行SQL所需的参数:parameter A parameter object to pass to the statement.
Employee employee = session.selectOne("com.clay.mybatis.mapper.EmployeeMapper.selectEmployee", 1);
System.out.println(employee);
} finally {
if (session != null) {
session.close();
}
}
}
}

MySQL 数据库初始化

在 MySQL 里执行以下 SQL 语句创建数据库与数据库表,并插入测试数据

1
2
3
4
5
6
7
8
9
10
11
CREATE DATABASE `mybatis_lesson` DEFAULT CHARACTER SET utf8mb4;

CREATE TABLE `t_employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`last_name` varchar(255) DEFAULT NULL,
`gender` char(1) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into t_employee values(null, 'Jim','1', 'jim@gmail.com');

工程的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mybatis-share
├── mybatis-lesson-1
│   ├── pom.xml
│   └── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │   └── clay
│   │   │   └── mybatis
│   │   │   ├── bean
│   │   │   │   └── Employee.java
│   │   │   ├── dao
│   │   │   │   └── EmployeeMapper.xml
│   │   │   └── MyBatisApplication.java
│   │   └── resources
│   │   ├── db.properties
│   │   ├── log4j.properties
│   │   └── mybatis-config.xml
│   └── test
│   └── java
├── pom.xml
└── README.md

代码测试

执行主启动类 MyBatisApplicationmain() 方法,若打印出以下日志信息,则说明 MyBatis 应用正常运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
21:47:20,509 DEBUG LogFactory:105 - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
21:47:20,550 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
21:47:20,550 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
21:47:20,551 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
21:47:20,551 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
21:47:20,648 DEBUG JdbcTransaction:137 - Opening JDBC Connection
21:47:20,997 DEBUG PooledDataSource:434 - Created connection 1564984895.
21:47:20,998 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5d47c63f]
21:47:21,005 DEBUG selectEmployee:137 - ==> Preparing: select id, last_name as lastName, gender, email from t_employee where id = ?
21:47:21,058 DEBUG selectEmployee:137 - ==> Parameters: 1(Integer)
21:47:21,097 DEBUG selectEmployee:137 - <== Total: 1
1_Jim_1_jim@gmail.com
21:47:21,098 DEBUG JdbcTransaction:123 - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5d47c63f]
21:47:21,099 DEBUG JdbcTransaction:91 - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5d47c63f]
21:47:21,099 DEBUG PooledDataSource:391 - Returned connection 1564984895 to pool.

面向接口编程

在上面入门案例的 SQL 映射文件中,由于在命名空间 com.clay.mybatis.mapper.EmployeeMapper 里定义了一个名为 selectEmployee 的映射语句,这样就可以在 Java 代码中使用全限定名 com.clay.mybatis.mapper.EmployeeMapper.selectEmployee 来调用唯一的 SQL 映射语句,如下所示:

1
Employee employee = session.selectOne("com.clay.mybatis.mapper.EmployeeMapper.selectEmployee", 1);

MyBatis 另外还提供了一种面向接口的编程方式,可以直接映射到在命名空间中同名的映射器类(Mapper 类),并将已映射的 SQL 语句匹配到对应名称、参数和返回类型的方法。本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-2

  • 第一步:创建 Mapper 接口类(映射器类)
1
2
3
4
5
6
7
8
9
package com.clay.mybatis.dao;

import com.clay.mybatis.bean.Employee;

public interface EmployeeMapper {

public Employee getById(Long id);

}
  • 第二步:在 SQL 映射文件中,指定命名空间为 Mapper 接口类的全类名,同时指定 id 的属性值为 Mapper 接口类的方法名
1
2
3
4
5
6
7
8
9
10
11
<?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接口类的全类名 -->
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">
<select id="getById" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email from t_employee where id = #{id}
</select>
</mapper>
  • 第三步:通过 Mapper 接口类实现增删改查操作,值得一提的是,MyBatis 会为每个 Mapper 接口类自动创建一个代理对象(将 Mapper 接口类与 SQL 映射文件绑定在一起),最后由代理对象去执行增删改查方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyBatisApplication {
public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();

try {
// MyBatis会自动创建一个代理对象,由代理对象去执行增删改查方法
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee employee = mapper.getById(1L);
System.out.println(employee);
} finally {
if (session != null) {
session.close();
}
}
}
}
  • 第四步:执行主启动类 MyBatisApplicationmain() 方法,若打印出以下日志信息,则说明 MyBatis 应用正常运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
22:13:27,834 DEBUG LogFactory:105 - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
22:13:27,872 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
22:13:27,872 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
22:13:27,873 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
22:13:27,873 DEBUG PooledDataSource:363 - PooledDataSource forcefully closed/removed all connections.
22:13:27,993 DEBUG JdbcTransaction:137 - Opening JDBC Connection
22:13:28,262 DEBUG PooledDataSource:434 - Created connection 27319466.
22:13:28,262 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1a0dcaa]
22:13:28,266 DEBUG getById:137 - ==> Preparing: select id, last_name as lastName, gender, email from t_employee where id = ?
22:13:28,304 DEBUG getById:137 - ==> Parameters: 1(Long)
22:13:28,330 DEBUG getById:137 - <== Total: 1
1_Jim_1_jim@gmail.com
22:13:28,331 DEBUG JdbcTransaction:123 - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1a0dcaa]
22:13:28,331 DEBUG JdbcTransaction:91 - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1a0dcaa]
22:13:28,332 DEBUG PooledDataSource:391 - Returned connection 27319466 to pool.

你可能会注意到,这种方式和用全限定名调用 Java 对象的方法类似。这样,该命名就可以直接映射到在命名空间中同名的映射器类,并将已映射的 select 语句匹配到对应名称、参数和返回类型的方法。因此你就可以像上面那样,不费吹灰之力地在对应的映射器接口调用方法。第二种方式有很多优势,首先它不依赖于字符串字面值,会更安全一点;其次,如果你的 IDE 有代码补全功能,那么代码补全可以帮你快速选择到映射好的 SQL 语句。

进阶使用

命名空间

在之前版本的 MyBatis 中,命名空间(Namespaces)的作用并不大,是可选的。但现在,随着命名空间越发重要,必须指定命名空间。命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也为了实现接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。

为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则:

  • 全限定名:比如 com.mypackage.MyMapper.selectAllThings,将被直接用于查找及使用
  • 短名称:比如 selectAllThings,如果该名称全局唯一也可以作为一个单独的引用。如果不唯一,有两个或两个以上的相同名称(比如 com.foo.selectAllThingscom.bar.selectAllThings),那么使用时就会产生短名称不唯一的错误,这种情况下就必须使用全限定名

对于像 EmployeeMapper 这样的映射器类来说,还有另一种方法来完成语句映射。它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 入门示例可以被替换成如下的配置:

1
2
3
4
5
6
7
8
9
10
package com.clay.mybatis.dao;

import com.clay.mybatis.bean.Employee;

public interface EmployeeMapper {

@Select("select id, last_name as lastName, gender, email from t_employee where id = #{id}")
public Employee getById(Long id);

}

使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML 的语句映射方式间自由移植和切换。

作用域(Scope)和生命周期

理解不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 Bean 中,因此可以直接忽略它们的生命周期。如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-SpringMyBatis-Guice 这两个子项目。

SqlSessionFactoryBuilder

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码 坏习惯。因此 SqlSessionFactory 的最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

SqlSession

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。如果你现在正在使用一种 Web 框架,可以考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后就关闭它。这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。下面的示例就是一个确保 SqlSession 关闭的标准代码:

1
2
3
try (SqlSession session = sqlSessionFactory.openSession()) {
// 应用逻辑代码
}

或者

1
2
3
4
5
6
7
8
SqlSession session = sqlSessionFactory.openSession();
try {
// 应用逻辑代码
} finally {
if (session != null) {
session.close();
}
}

在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。

映射器实例

映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的,虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。映射器实例并不需要被显式地关闭,尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。因此,最好将映射器放在方法作用域内,就像下面的例子一样:

1
2
3
4
try (SqlSession session = sqlSessionFactory.openSession()) {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
// 应用逻辑代码
}

不使用 XML 构建 SqlSessionFactory

如果你更愿意直接从 Java 代码而不是 XML 文件中创建配置,或者想要创建你自己的配置建造器,MyBatis 也提供了完整的配置类,提供了所有与 XML 文件等价的配置项。

1
2
3
4
5
6
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(EmployeeMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

注意该例中,Configuration 添加了一个映射器类(Mapper Class)。映射器类是 Java 类,它们包含 SQL 映射注解从而避免依赖 XML 文件。不过,由于 Java 注解的一些限制以及某些 MyBatis 映射的复杂性,要使用大多数高级映射(比如:嵌套联合映射),仍然需要使用 XML 配置。有鉴于此,如果存在一个同名 SQL 映射文件(XML),MyBatis 会自动查找并加载它;在这个例子中,基于类路径和 EmployeeMapper.class 的类名,会加载 EmployeeMapper.xml