MyBatis 是一个简化和实现了 Java 数据持久化层(persistence layer)的开源框架,它抽象了大量的 JDBC 冗余代码,并提供了一个简单易用的 API 和数据库交互。
MyBatis 的前身是 iBATIS,iBATIS 于 2002 年由 Clinton Begin 创建。 MyBatis 3 是 iBATIS 的全新设计,支持注解和 Mapper。
MyBatis 流行的主要原因在于它的简单性和易使用性。在 Java 应用程序中,数据持久化层涉及到的工作有:将从数
据库查询到的数据生成所需要的 Java 对象;将 Java 对象中的数据通过 SQL 持久化到数据库中。
MyBatis 通过抽象底层的 JDBC 代码,自动化 SQL 结果集产生 Java 对象、 Java 对象的数据持久化数据库中的过程使得对 SQL 的使用变得容易。
如 果 你 正 在 使 用 iBATIS , 并 且 想 将 iBATIS 移 植 到 MyBatis 上 , 你 可 以 在 MyBatis 的 官 方 网 站( https://code.google.com/p/mybatis/wiki/DocUpgrade )上找到详细的指导步骤。
为什么选择 MyBatis?
当前有很多 Java 实现的持久化框架,而 MyBatis 流行起来有以下原因:
- 它消除了大量的 JDBC 冗余代码
- 它有低的学习曲线
- 它能很好地与传统数据库协同工作
- 它可以接受 SQL 语句
- 它提供了与 Spring 和 Guice 框架的集成支持
- 它提供了与第三方缓存类库的集成支持
- 它引入了更好的性能
消除大量的 JDBC 冗余代码
你不需要创建 Connection 连接,PreparedStatement,不需要自己对每一次数据库操作进行手动设置参数和关闭连接。只需要配置数据库连接属性和 SQL 语句,MyBatis 会处理这些底层工作。
另外,MyBatis 还提供了其他的一些特性来简化持久化逻辑的实现:
- 它支持复杂的 SQL 结果集数据映射到嵌套对象图结构
- 它支持一对一和一对多的结果集和 Java 对象的映射
- 它支持根据输入的数据构建动态的 SQL 语句
能够很好地与传统数据库协同工作
有时我们可能需要用不正规形式与传统数据库协同工作,使用成熟的 ORM 框架(如 Hibernate)有可能、 但是很难跟传统数据库很好地协同工作,因为他们尝试将 Java 对象静态地映射到数据库的表上。
而 MyBatis 是将查询的结果与 Java 对象映射起来,这使得 MyBatis 可以很好地与传统数据库协同工作。你可以根据面相对象的模型创建 Java 域对象,执行传统数据库的查询,然后将结果映射到对应的 Java 对象上。
接受 SQL
成熟的 ORM 框架(如 Hibernate)鼓励使用实体对象(Entity Objects)和在其底层自动产生 SQL 语句。由于这种的 SQL 生成方式,我们有可能不能够利用到数据库的一些特有的特性。 Hibernate 允许执行本地SQL,但是这样会打破持久层和数据库独立的原则。
MyBatis 框架接受 SQL 语句,而不是将其对开发人员隐藏起来。由于 MyBatis 不会产生任何的 SQL 语句,所以开发人员就要准备 SQL 语句,这样就可以充分利用数据库特有的特性并且可以准备自定义的查询。另外,MyBatis 对存储过程也提供了支持。
与 Spring 和 Guice 框架的集成支持
MyBatis 提供了与 流行的依赖注入框架 Spring 和 Guice 的开包即用的集成支持,这将进一步简化 MyBatis 的使用
与第三方缓存类库的集成支持
MyBatis 有内建的 SqlSession 级别的缓存机制,用于缓存 Select 语句查询出来的结果。除此之外,MyBatis 提供了与多种第三方缓存类库的集成支持,如 EHCache,OSCache,Hazelcast。
良好的性能
性能问题是关乎软件应用成功与否的关键因素之一。为了达到更好的性能,需要考虑很多事情,而对很多应用而言,数据持久化层是整个系统性能的关键。
MyBatis 支持数据库连接池,消除了为每一个请求创建一个数据库连接的开销
MyBatis 提供了内建的缓存机制,在 SqlSession 级别提供了对 SQL 查询结果的缓存。即:如果你调用了相同的select 查询,MyBatis 会将放在缓存的结果返回,而不会去再查询数据库。
MyBatis 框架并没有大量地使用代理机制2,因此对于其他的过度地使用代理的 ORM 框架而言,MyBatis 可以获得更好的性能。
在软件开发中,并没有通用的(一体适用)的解决方案,每一个应用会有不同的一系列的要求,而我们应该根据应用的需要来选择我们的工具和框架。在前一节中,我们看到了使用 MyBatis 的优点。然而也有一些情况,MyBatis 并不是理想的或者是最好的解决方案。
如果你的应用是以面向对象模型,并且向动态生成 SQL 语句,那么 MyBatis可能就不符合你的要求。另外,如果你想让你的应用有一个传递性的缓存机制的话(保存父对象时也应该保存关联的子对象),Hibernate 会更适合你。
MyBatis入门(安装和配置简单示例)
- 新建一个 Java 项目,将 MyBatis-3.2.2.jar 添加到 classpath 中
- 新建 mybatis-config.xml 和映射器 StudentMapper.xml 配置文件
新建 MyBatisSqlSessionFactory 单例类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class MyBatisSqlSessionFactory {
private static SqlSessionFactory sqlSessionFactory;
public static SqlSessionFactory getSqlSessionFactory() {
if (sqlSessionFactory == null) {
InputStream inputStream;
try {
inputStream = Resources.getResourceAsStream("mybatis-config.xml"); //加载核心配置文件
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //生成SqlSessionFactory
} catch (IOException e) {
throw new RuntimeException(e.getCause());
}
}
return sqlSessionFactory;
}
public static SqlSession openSession() { //从sqlSessionFactory中获取SqlSession
return getSqlSessionFactory().openSession();
}
}新建 StudentMapper 接口和 StudentService 类
- 新建一个 JUnit 测试类来测试 StudentService
它是怎么工作的
首先,我们配置了 MyBatis 最主要的配置文件-mybatis-config.xml,里面包含了 JDBC 连接参数;配置了映射器Mapper XML 配置文件文件,里面包含了 SQL 语句的映射。
我们使用 mybatis-config.xml 内的信息创建了 SqlSessionFactory 对象。每个数据库环境应该就一个SqlSessionFactory 对象实例,所以我们使用了单例模式只创建一个 SqlSessionFactory 实例。我们创建了一个映射器 Mapper 接口-StudentMapper,其定义的方法签名和在 StudentMapper.xml 中定义的完全一样(即映射器 Mapper 接口中的方法名跟 StudentMapper.xml 中的 id 的值相同)。注意StudentMapper.xml 中namespace 的值被设置成com.mybatis3.mappers.StudentMapper,是StudentMapper 接口的完全限定名。这使我们可以使用接口来调用映射的 SQL 语句。
在 StudentService.java 中,我们在每一个方法中创建了一个新的 SqlSession,并在方法功能完成后关闭SqlSession。每一个线程应该有它自己的 SqlSession 实例。 SqlSession 对象实例不是线程安全的,并且不被共享。所以 SqlSession 的作用域最好就是其所在方法的作用域。从 Web 应用程序角度上看,SqlSession 应该存在于 request 级别作用域上。
MyBatis的配置元素和SqlSessionFactory
MyBatis 最关键的组成部分是 SqlSessionFactory,我们可以从中获取 SqlSession,并执行映射的 SQL 语句。
SqlSessionFactory 对象可以通过基于 XML 的配置信息或者 Java API 创建。
我们将探索各种 MaBatis 配置元素,如 dataSource,environments,全局参数设置,typeAlias,typeHandlers,SQL 映射;接着我们将实例化 SqlSessionFactory。
使用XML配置MyBatis
构建 SqlSessionFactory 最常见的方式是基于 XML 配置(的构造方式)。下面的 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
33
34
35
36
37
38
39
40
41
42
43<?xml version="1.0" encoding="utf-8"?>
<configuration>
<properties resource="application.properties">
<property name="username" value="db_user" />
<property name="password" value="verysecurepwd" />
</properties>
<settings>
<setting name="cacheEnabled" value="true" />
</settings>
<typeAliases>
<typeAlias alias="Tutor" type="com.mybatis3.domain.Tutor" />
<package name="com.mybatis3.domain" />
</typeAliases>
<typeHandlers>
<typeHandler handler="com.mybatis3.typehandlers. PhoneTypeHandler" />
<package name="com.mybatis3.typehandlers" />
</typeHandlers>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
</environment>
<environment id="production">
<transactionManager type="MANAGED" />
<dataSource type="JNDI">
<property name="data_source" value="java:comp/jdbc/MyBatisDemoDS" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/mybatis3/mappers/StudentMapper.xml" />
<mapper url="file:///D:/mybatisdemo/mappers/TutorMapper.xml" />
<mapper class="com.mybatis3.mappers.TutorMapper" />
</mappers>
</configuration>
environment
MyBatis 支持配置多个 dataSource 环境,可以将应用部署到不同的环境上,如 DEV(开发环境),TEST(测试换将),QA(质量评估环境),UAT(用户验收环境),PRODUCTION(生产环境),可以通过将默认 environment 值设置成想要的environment id 值。
在上述的配置中,默认的环境 environment 被设置成 development。当需要将程序部署到生产服务器上时,你不需
要修改什么配置,只需要将默认环境 environment 值设置成生产环境的 environment id 属性即可。
有时候,我们可能需要在相同的应用下使用多个数据库。比如我们可能有 SHOPPING-CART 数据库来存储所有的订单
明细;使用 REPORTS 数据库存储订单明细的合计,用作报告。
如果你的应用需要连接多个数据库,你需要将每个数据库配置成独立的环境,并且为每一个数据库创建一个
SqlSessionFactory。
1 | <environments default="shoppingcart"> |
我们可以如下为每个环境创建一个 SqlSessionFactory:1
2
3
4inputStream = Resources.getResourceAsStream("mybatis-config.xml");
defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
cartSqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "shoppingcart");
reportSqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, "reports");
创建 SqlSessionFactory 时,如果没有明确指定环境 environment id,则会使用默认的环境 environment 来创
建。在上述的源码中,默认的 SqlSessionFactory 便是使用 shoppingcart 环境设置创建的。
对于每个环境 environment,我们需要配置 dataSource 和 transactionManager 元素。
数据源 DataSource
dataSource 元素被用来配置数据库连接属性。1
2
3
4
5
6<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</dataSource>
dataSource 的类型可以配置成其内置类型之一,如 UNPOOLED,POOLED,JNDI。
- 如果将类型设置成 UNPOOLED,MyBatis 会为每一个数据库操作创建一个新的连接,并关闭它。该方式适用于只有小规模数量并发用户的简单应用程序上。
- 如果将属性设置成 POOLED,MyBatis 会创建一个数据库连接池,连接池中的一个连接将会被用作数据库操作。一旦数据库操作完成,MyBatis 会将此连接返回给连接池。在开发或测试环境中,经常使用此种方式。
- 如果将类型设置成 JNDI,MyBatis 从在应用服务器向配置好的 JNDI 数据源 dataSource 获取数据库连接。在生产环境中,优先考虑这种方式。
事务管理器 TransactionManager
MyBatis 支持两种类型的事务管理器: JDBC and MANAGED.
- JDBC 事务管理器被用作当应用程序负责管理数据库连接的生命周期(提交、回退等等)的时候。当你将TransactionManager 属性设置成 JDBC,MyBatis 内部将使用 JdbcTransactionFactory 类创建TransactionManager。例如,部署到 Apache Tomcat 的应用程序,需要应用程序自己管理事务。
- MANAGED 事务管理器是当由应用服务器负责管理数据库连接生命周期的时候使用。当你将TransactionManager 属性设置成 MANAGED 时, MyBatis 内部使用 ManagedTransactionFactory 类创建事务管理器TransactionManager。例如,当一个 JavaEE的应用程序部署在类似 JBoss, WebLogic,GlassFish 应用服务器上时,它们会使用 EJB 进行应用服务器的事务管理能力。在这些管理环境中,你可以使用 MANAGED 事务管理器。
(译者注:Managed 是托管的意思,即是应用本身不去管理事务,而是把事务管理交给应用所在的服务器进行管理。)
属性 Properties
属性配置元素可以将配置值具体化到一个属性文件中,,并且使用属性文件的 key 名作为占位符。在上述的配置中,我们将数据库连接属性具体化到了 application.properties 文件中,并且为 driver,URL 等属性使用了占位符。
在 applications.properties 文件中配置数据库连接参数,如下所示:
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatisdemo
jdbc.username=root
jdbc.password=admin在 mybatis-config.xml 文件中,为属性使用 application.properties 文件中定义的占位符:
1 | <properties resource="application.properties"> |
并且,你可以在
1 | <properties resource="application.properties"> |
这里,如果 application.properties 文件包含值 jdbc.username 和 jdbc.password,则上述定义的 username 和
password 的值 db_user 和 verysecurepwd 将会被 application.properties 中定义的对应的 jdbc.username 和
jdbc.password 值覆盖。
类型别名 typeAliases
在 SQLMapper 配置文件中,对于 resultType 和 parameterType 属性值,我们需要使用 JavaBean 的完全限定名。
如下例子所示:1
2
3
4
5
6
7
8
9
10<select id="findStudentById" parameterType="int"
resultType="com.mybatis3.domain.Student">
SELECT STUD_ID AS ID, NAME, EMAIL, DOB
FROM STUDENTS WHERE STUD_ID=#{Id}
</select>
<update id="updateStudent" parameterType="com.mybatis3.domain. Student">
UPDATE STUDENTS
SET NAME=#{name}, EMAIL=#{email}, DOB=#{dob}
WHERE STUD_ID=#{id}
</update>
这里我们为 resultType 和 parameterType 属性值设置为 Student 类型的完全限定名:
com.mybatis3.domain.Student
我们可以为完全限定名取一个别名(alias),然后其需要使用完全限定名的地方使用别名,而不是到处使用完全限定
名。如下例子所示,为完全限定名起一个别名:1
2
3
4
5<typeAliases>
<typeAlias alias="Student" type="com.mybatis3.domain.Student" />
<typeAlias alias="Tutor" type="com.mybatis3.domain.Tutor" />
<package name="com.mybatis3.domain" />
</typeAliases>
然后在 SQL Mapper 映射文件中, 如下使用 Student 的别名:1
2
3
4
5
6
7
8
9<select id="findStudentById" parameterType="int" resultType="Student">
SELECT STUD_ID AS ID, NAME, EMAIL, DOB
FROM STUDENTS WHERE STUD_ID=#{id}
</select>
<update id="updateStudent" parameterType="Student">
UPDATE STUDENTS
SET NAME=#{name}, EMAIL=#{email}, DOB=#{dob}
WHERE STUD_ID=#{id}
</update>
你可以不用为每一个 JavaBean 单独定义别名, 你可以为提供需要取别名的 JavaBean 所在的包(package),MyBatis会自动扫描包内定义的 JavaBeans,然后分别为 JavaBean 注册一个小写字母开头的非完全限定的类名形式的别名。如下所示,提供一个需要为 JavaBeans 起别名的包名:1
2
3<typeAliases>
<package name="com.mybatis3.domain" />
</typeAliases>
如果 Student.java 和 Tutor.java Bean 定义在 com.mybatis3.domain 包中,则com.mybatis3.domain.Student的别名会被注册为 student。而 com.mybatis3.domain.Tutor 别名将会被注册为 tutor。示例如下:1
2
3
4
5
6<typeAliases>
<typeAlias alias="Student" type="com.mybatis3.domain.Student" />
<typeAlias alias="Tutor" type="com.mybatis3.domain.Tutor" />
<package name="com.mybatis3.domain" />
<package name="com.mybatis3.webservices.domain" />
</typeAliases>
还有另外一种方式为 JavaBeans 起别名,使用注解@Alias:1
2
3
4("StudentAlias")
public class Student
{
}
类型处理器 typeHandlers
如上一章已经讨论过,MyBatis 通过抽象 JDBC 来简化了数据持久化逻辑的实现。 MyBatis 在其内部使用 JDBC,提供了更简洁的方式实现了数据库操作。
当 MyBatis 将一个 Java 对象作为输入参数执行 INSERT 语句操作时,它会创建一个 PreparedStatement 对象,并且使用 setXXX()方式对占位符设置相应的参数值。
这里,XXX 可以是 Int,String,Date 等 Java 对象属性类型的任意一个。示例如下:1
2
3
4<insert id="insertStudent" parameterType="Student">
INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,DOB)
VALUES(#{studId},#{name},#{email},#{dob})
</insert>
为执行这个语句,MyBatis 将采取以下一系列动作:
创建一个有占位符的 PreparedStatement 接口,如下:
1
2PreparedStatement pstmt = connection
.prepareStatement("INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,DOB) VALUES(?,?,?,?)");检查 Student 对象的属性 studId 的类型,然后使用合适 setXXX 方法去设置参数值。这里 studId 是 integer类型,所以会使用 setInt()方法:
1
pstmt.setInt(1,student.getStudId());
类似地,对于 name 和 email 属性都是 String 类型,MyBatis 使用 setString()方法设置参数。
1
2pstmt.setString(2, student.getName());
pstmt.setString(3, student.getEmail());至于 dob 属性, MyBatis 会使用 setDate() 方法设置 dob 处占位符位置的值。
- MyBaits 会将 java.util.Date 类型转换为 into java.sql.Timestamp 并设值:
1
pstmt.setTimestamp(4, new Timestamp((student.getDob()).getTime()));
厉害!但 MyBatis 是怎么知道对于 Integer 类型属性使用 setInt() 和 String 类型属性使用 setString()方法呢?
其实 MyBatis 是通过使用类型处理器(type handlers)来决定这么做的。
MyBatis 对于以下的类型使用内建的类型处理器:所有的基本数据类型、基本类型的包裹类型、 byte[]、
java.util.Date、 java.sql.Date、 java,sql.Time、 java.sql.Timestamp、 java 枚举类型等。所以当 MyBatis 发现属性的类型属于上述类型,他会使用对应的类型处理器将值设置到 PreparedStatement 中,同样地,当从 SQL 结果集构建 JavaBean 时,也有类似的过程。
那如果我们给了一个自定义的对象类型,来存储存储到数据库呢?示例如下:
假设表 STUDENTS 有一个 PHONE 字段,类型为 VARCHAR(15),而 JavaBean Student 有一个 PhoneNumber 类定义类型的 phoneNumber 属性。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
39public class PhoneNumber {
private String countryCode;
private String stateCode;
private String number;
public PhoneNumber() {
}
public PhoneNumber(String countryCode, String stateCode, String number) {
this.countryCode = countryCode;
this.stateCode = stateCode;
this.number = number;
}
public PhoneNumber(String string) {
if (string != null) {
String[] parts = string.split("-");
if (parts.length > 0)
this.countryCode = parts[0];
if (parts.length > 1)
this.stateCode = parts[1];
if (parts.length > 2)
this.number = parts[2];
}
}
public String getAsString() {
return countryCode + "-" + stateCode + "-" + number;
}
// Setters and getters
}
public class Student {
private Integer id;
private String name;
private String email;
private PhoneNumber phone;
// Setters and getters
}
1 | <insert id="insertStudent" parameterType="Student"> |
这里,phone 参数需要传递给#{phone};而 phone 对象是 PhoneNumber 类型。然而,MyBatis 并不知道该怎样来处理这个类型的对象。
为了让 MyBatis 明白怎样处理这个自定义的 Java 对象类型,如 PhoneNumber,我们可以创建一个自定义的类型处理器,如下所示:
MyBatis 提供了抽象类 BaseTypeHandler
,我们可以继承此类创建自定义类型处理器。 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
31packagecom.mybatis3.typehandlers;
importjava.sql.CallableStatement;
importjava.sql.PreparedStatement;
importjava.sql.ResultSet;
importjava.sql.SQLException;
importorg.apache.ibatis.type.BaseTypeHandler;
importorg.apache.ibatis.type.JdbcType;
importcom.mybatis3.domain.PhoneNumber;
public class PhoneTypeHandler extends BaseTypeHandler<PhoneNumber> {
public void setNonNullParameter(PreparedStatement ps, int i, PhoneNumber parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(i, parameter.getAsString());
}
public PhoneNumber getNullableResult(ResultSet rs, String columnName) throws SQLException {
return new PhoneNumber(rs.getString(columnName));
}
public PhoneNumber getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return new PhoneNumber(rs.getString(columnIndex));
}
public PhoneNumber getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return new PhoneNumber(cs.getString(columnIndex));
}
}我们使用 ps.setString()和 rs.getString()方法是因为 phone 列是 VARCHAR 类型。
- 一旦我们实现了自定义的类型处理器,我们需要在 mybatis-config.xml 中注册它:
1
2
3
4
5
6
7
8
9<?xml version="1.0" encoding="utf-8"?>
<configuration>
<properties resource="application.properties" />
<typeHandlers>
<typeHandler handler="com.mybatis3.typehandlers. PhoneTypeHandler" />
</typeHandlers>
</configuration>
注册 PhoneTypeHandler 后, MyBatis 就能够将 Phone 类型的对象值存储到 VARCHAR 类型的列上。
全局参数设置 Settings
为满足应用特定的需求,MyBatis 默认的全局参数设置可以被覆盖(overridden)掉,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<settings>
<setting name="cacheEnabled" value="true" />
<setting name="lazyLoadingEnabled" value="true" />
<setting name="multipleResultSetsEnabled" value="true" />
<setting name="useColumnLabel" value="true" />
<setting name="useGeneratedKeys" value="false" />
<setting name="autoMappingBehavior" value="PARTIAL" />
<setting name="defaultExecutorType" value="SIMPLE" />
<setting name="defaultStatementTimeout" value="25000" />
<setting name="safeRowBoundsEnabled" value="false" />
<setting name="mapUnderscoreToCamelCase" value="false" />
<setting name="localCacheScope" value="SESSION" />
<setting name="jdbcTypeForNull" value="OTHER" />
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode ,toString" />
</settings>
SQL 映射定义 Mappers
Mapper XML 文件中包含的 SQL 映射语句将会被应用通过使用其 statementid 来执行。我们需要在mybatisconfig.xml 文件中配置 SQL Mapper 文件的位置。1
2
3
4
5
6mappers>
<mapper resource="com/mybatis3/mappers/StudentMapper.xml" />
<mapper url="file:///D:/mybatisdemo/app/mappers/TutorMapper.xml" />
<mapper class="com.mybatis3.mappers.TutorMapper" />
<package name="com.mybatis3.mappers" />
</mappers>
以上每一个
- resource 属性用来指定在 classpath 中的 mapper 文件。
- url 属性用来通过完全文件系统路径或者 web URL 地址来指向 mapper 文件
- class 属性用来指向一个 mapper 接口
- package 属性用来指向可以找到 Mapper 接口的包名
使用Java API配置MyBatis
上一节中,我们已经讨论了各种 MyBatis 配置元素,如 envronments,typeAlias,和 typeHandlers,以及如何使用XML 配置它们。即使你想使用基于 Java API 的 MyBatis 配置,经历上一节的学习是大有好处的,它可以帮你对这些配置元素有更好的理解。在本节中,我们会引用到前一节中描述的一些类。
MyBatis 的 SqlSessionFactory 接口除了使用基于 XML 的配置创建外也可以通过 Java API 编程式地被创建。每个在 XML 中配置的元素,都可以编程式的创建。使用 Java API 创建 SqlSessionFactory,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static SqlSessionFactory getSqlSessionFactory() {
SqlSessionFactory sqlSessionFactory = null;
try {
DataSource dataSource = DataSourceFactory.getDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.getTypeAliasRegistry().registerAlias("student", Student.class);
configuration.getTypeHandlerRegistry().register(PhoneNumber.class, PhoneTypeHandler.class);
configuration.addMapper(StudentMapper.class);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
} catch (Exception e) {
throw new RuntimeException(e);
}
return sqlSessionFactory;
}
环境配置 Environment
我们需要为想使用 MaBatis 连接的每一个数据库创建一个 Enviroment 对象。为了使用每一个环境,我们需要为每一个环境 environment 创建一个 SqlSessionFactory 对象。而创建 Environment 对象,我们需要 java.sql.DataSource和 TransactionFactory 实例。下面让我们看看如何创建 DataSource 和TransactionFactory 对象。
数据源 DataSource
MyBatis 支持三种内建的 DataSource 类型: UNPOOLED, POOLED, 和 JNDI.
- UNPOOLED 类型的数据源 dataSource 为每一个用户请求创建一个数据库连接。在多用户并发应用中,不建议
使用。 - POOLED 类型的数据源 dataSource 创建了一个数据库连接池,对用户的每一个请求,会使用缓冲池中的一个
可 用 的 Connection 对 象 , 这 样 可 以 提 高 应 用 的 性 能 。 MyBatis 提 供 了
org.apache.ibatis.datasource.pooled.PooledDataSource 实现 javax.sql.DataSource 来创建连
接池。 - JNDI类型的数据源 dataSource使用了应用服务器的数据库连接池,并且使用 JNDI查找来获取数据库连接。
让我们看一下怎样通过 MyBatis 的 PooledDataSource 获得 DataSource 对象,如下:1
2
3
4
5
6
7
8
9
10public class DataSourceFactory {
public static DataSource getDataSource() {
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/mybatisdemo";
String username = "root";
String password = "admin";
PooledDataSource dataSource = new PooledDataSource(driver, url, username, password);
return dataSource;
}
}
一般在生产环境中,DataSource 会被应用服务器配置,并通过 JNDI 获取 DataSource 对象,如下所示:1
2
3
4
5
6
7
8
9
10
11
12public class DataSourceFactory {
public static DataSource getDataSource() {
String jndiName = "java:comp/env/jdbc/MyBatisDemoDS";
try {
InitialContext ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);
return dataSource;
} catch (NamingException e) {
throw new RuntimeException(e);
}
}
}
当前有一些流行的第三方类库,如 commons-dbcp 和 c3p0 实现了 java.sql.DataSource,你可以使用它们来创建
dataSource。
事务工厂 TransactionFactory
MyBatis 支持一下两种 TransactionFactory 实现:
- JdbcTransactionFactory
- ManagedTransactionFactory
如果你的应用程序运行在未托管(non-managed)的环境中,你应该使用 JdbcTransactionFactory。1
2
3DataSource dataSource = DataSourceFactory.getDataSource();
TransactionFactory txnFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", txnFactory, dataSource);
类型别名 typeAliases
MyBatis 提供以下几种通过 Configuration 对象注册类型别名的方法:
根据默认的别名规则,使用一个类的首字母小写、非完全限定的类名作为别名注册,可使用以下代码:
1
configuration.getTypeAliasRegistry().registerAlias(Student.class);
指定指定别名注册,可使用以下代码:
1
configuration.getTypeAliasRegistry().registerAlias("Student",Student.class);
通过类的完全限定名注册相应类别名,可使用一下代码:
1
configuration.getTypeAliasRegistry().registerAlias("Student","com.mybatis3.domain.Student");
为某一个包中的所有类注册别名,可使用以下代码:
1 | configuration.getTypeAliasRegistry().registerAliases("com.mybatis3.domain"); |
- 为在 com.mybatis3.domain package 包中所有的继承自 Identifiable 类型的类注册别名,可使用以下代码:
1
configuration.getTypeAliasRegistry().registerAliases("com.mybatis3.domain",Identifiable.class);
类型处理器 typeHandlers
MyBatis 提供了一系列使用 Configuration 对象注册类型处理器(type handler)的方法。我们可以通过以下方式注册自定义的类处理器:
为某个特定的类注册类处理器:
1
configuration.getTypeHandlerRegistry().register(PhoneNumber.class, PhoneTypeHandler.class);
注册一个类处理器:
1
configuration.getTypeHandlerRegistry().register(PhoneTypeHandler.class);
注册 com.mybatis3.typehandlers 包中的所有类型处理器:
1
configuration.getTypeHandlerRegistry().register("com.mybatis3.typehandlers");
全局参数设置 Settings
MyBatis 提供了一组默认的,能够很好地适用大部分的应用的全局参数设置。然而,你可以稍微调整这些参数,让它更好地满足你应用的需要。你可以使用下列方法将全局参数设置成想要的值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19configuration.setCacheEnabled(true);
configuration.setLazyLoadingEnabled(false);
configuration.setMultipleResultSetsEnabled(true);
configuration.setUseColumnLabel(true);
configuration.setUseGeneratedKeys(false);
configuration.setAutoMappingBehavior(AutoMappingBehavior.PARTIAL);
configuration.setDefaultExecutorType(ExecutorType.SIMPLE);
configuration.setDefaultStatementTimeout(25);
configuration.setSafeRowBoundsEnabled(false);
configuration.setMapUnderscoreToCamelCase(false);
configuration.setLocalCacheScope(LocalCacheScope.SESSION);
configuration.setAggressiveLazyLoading(true);
configuration.setJdbcTypeForNull(JdbcType.OTHER);
Set<String> lazyLoadTriggerMethods = new HashSet<String>();
lazyLoadTriggerMethods.add("equals");
lazyLoadTriggerMethods.add("clone");
lazyLoadTriggerMethods.add("hashCode");
lazyLoadTriggerMethods.add("toString");
configuration.setLazyLoadTriggerMethods(lazyLoadTriggerMethods );
Mappers
MyBatis 提供了一些使用 Configuration 对象注册 Mapper XML 文件和 Mappe 接口的方法。
- 添加一个 Mapper 接口,可使用以下代码:
configuration.addMapper(StudentMapper.class); - 添加 com.mybatis3.mappers 包中的所有 Mapper XML 文件或者 Mapper 接口,可使用以下代码:
configuration.addMappers(“com.mybatis3.mappers”); - 添加所有 com.mybatis3.mappers 包中的拓展了特定 Mapper 接口的 Maper 接口,如 aseMapper,可使用如下代码:
configuration.addMappers(“com.mybatis3.mappers”, BaseMapper.class);
Mappers 应该在 typeAliases 和 typeHandler 注册后再添加到 configuration 中。
自定义MyBatis日志
MyBatis 使用其内部 LoggerFactory 作为真正的日志类库使用的门面。其内部的 LaggerFactory 会将日志记录任务委托给如下的所示某一个日志实现,日志记录优先级由上到下顺序递减:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging4
如果 MyBatis 未发现上述日志记录实现,则 MyBatis 的日志记录功能无效。
如果你的运行环境中,在 classpath 中有多个可用的日志类库,并且你希望 MyBaits 使用某个特定的日志实现,你可
以通过调用以下其中一个方法: - org.apache.ibatis.logging.LogFactory.useSlf4jLogging();
- org.apache.ibatis.logging.LogFactory.useLog4JLogging();
- org.apache.ibatis.logging.LogFactory.useLog4J2Logging();
- org.apache.ibatis.logging.LogFactory.useJdkLogging();
- org.apache.ibatis.logging.LogFactory.useCommonsLogging();
- org.apache.ibatis.logging.LogFactory.useStdOutLogging();
如果你想自定义 MyBatis 日志记录,你应该在调用任何其它方法之前调用以上的其中一个方法。如果你想切换到的日志记录类库在运行时期无效,MyBatis 将会忽略这一请求。
使用XMl配置SQL映射器
关系型数据库和 SQL 是经受时间考验和验证的数据存储机制。和其他的 ORM 框架如 Hibernate 不同,MyBatis 鼓励开发者可以直接使用数据库,而不是将其对开发者隐藏,因为这样可以充分发挥数据库服务器所提供的 SQL 语句的巨大威力。与此同时,MyBaits 消除了书写大量冗余代码的痛苦,它使使用 SQL 更容易。
在代码里直接嵌套 SQL 语句是很差的编码实践,并且维护起来困难。 MyBaits 使用了映射器配置文件或注解来配置 SQL语句。在本章中,我们会看到具体怎样使用映射器配置文件来配置映射 SQL 语句。
本章将涵盖以下话题:
- 映射器配置文件 和 映射器接口
- 映射语句
- 配置 INSERT, UPDATE, DELETE, and SELECT 语句
- 结果映射 ResultMaps
- 简单 ResultMaps
- 使用内嵌 select 语句子查询的一对一映射
- 使用内嵌的结果集查询的一对一映射
- 使用内嵌 select 语句子查询的一对多映射
- 使用内嵌的结果集查询的一对一映射
- 动态 SQL 语句
- If 条件
- choose (when, otherwise) 条件
- trim (where, set) 条件
- foreach 循环
- MyBatis 菜谱
映射器配置文件和映射器接口
1 | public Student findStudentById(Integer studId){ |
我们可以通过字符串(字符串形式为:映射器配置文件所在的包名 namespace + 在文件内定义的语句 id,如上,即包名 com.mybatis3.mappers.StudentMapper 和语句 id findStudentById 组成)调用映射的 SQL 语句,但是这种方式容易出错。你需要检查映射器配置文件中的定义,以保证你的输入参数类型和结果返回类型是有效的。
MyBatis 通过使用映射器 Mapper 接口提供了更好的调用映射语句的方法。一旦我们通过映射器配置文件配置了映射语句,我们可以创建一个完全对应的一个映射器接口,接口名跟配置文件名相同,接口所在包名也跟配置文件所在包名完全一 样 ( 如 StudentMapper.xml 所 在 的 包 名 是 com.mybatis3.mappers , 对 应 的 接 口 名 就 是
com.mybatis3.mappers.StudentMapper.java )。映射器接口中的方法签名也跟映射器配置文件中完全对应:方法名为配置文件中 id 值;方法参数类型为 parameterType 对应值;方法返回值类型为 returnType 对应值。
对于上述的 StudentMapper.xml 文件,我们可以创建一个映射器接口 StudentMapper.java 如下:1
2
3
4package com.mybatis3.mappers;
public interface StudentMapper{
Student findStudentById(Integer id);
}
在 StudentMapper.xml 映射器配置文件中,其名空间 namespace 应该跟 StudentMapper 接口的完全限定名保持一致。另外, StudentMapper.xml 中语句 id, parameterType, returnType 应该分别和 StudentMapper 接口中的方法名,参数类型,返回值相对应。使用映射器接口我们可以以类型安全的形式调用调用映射语句。如下所示:1
2
3
4
5
6
7
8
9
10
11public Student findStudentById(Integer studId){
SqlSession sqlSession = MyBatisUtil.getSqlSession();
try{
StudentMapper studentMapper =sqlSession.getMapper(StudentMapper.class);
return studentMapper.findStudentById(studId);
}
finally
{
sqlSession.close();
}
}
即使映射器 Mapper 接口可以以类型安全的方式调用映射语句,但是我们我负责书写正确的,匹配方法名、参数类型、 和返回值的映射器 Mapper 接口。如果映射器 Mapper 接口中的方法和 XML 中的映射语句不能匹配,会在运行期抛出一个异常。实际上,指定 parameterType 是可选的;MyBatis 可以使用反射机制来决定 parameterType。 但是,从配置可读性的角度来看,最好指定parameterType 属性。如果 parameterType 没有被提及,开发者必须查看Mapper XML 配置和 Java 代码了解传递给语句的输入参数的数据类型。
映射语句
MyBatis 提供了多种元素来配置不同类型的语句,如 SELECT,INSERT,UPDATE,DELETE。接下来让我们看看如何具体配置映射语句
- Insert语句
一个 INSERT SQL 语句可以在元素在映射器 XML 配置文件中配置,如下所示: 1
2
3
4<insert id="insertStudent" parameterType="Student">
INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE)
VALUES(#{studId},#{name},#{email},#{phone})
</insert>
这里我们使用一个 ID insertStudent,可以在名空间 com.mybatis3.mappers.StudentMapper.insertStudent 中唯一标识。 parameterType 属性应该是一个完全限定类名或者是一个类型别名(alias) 。我们可以如下调用这个语句:1
int count = sqlSession.insert("com.mybatis3.mappers.StudentMapper.insertStudent", student);
sqlSession.insert() 方法返回执行 INSERT 语句后所影响的行数。
如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安全的方式调用方法,如下所示:1
2
3
4package com.mybatis3.mappers;
public interface StudentMapper{
int insertStudent(Student student);
}
你可以如下调用 insertStudent 映射语句:1
2StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int count = mapper.insertStudent(student);
自动生成主键
在上述的 INSERT 语句中,我们为可以自动生成(auto-generated)主键的列 STUD_ID 插入值。 我们可以使用
useGeneratedKeys 和 keyProperty 属性让数据库生成 auto_increment 列的值,并将生成的值设置到其中一个
输入对象属性内,如下所示:1
2
3
4
5<insert id="insertStudent" parameterType="Student" useGeneratedKeys="true"
keyProperty="studId">
INSERT INTO STUDENTS(NAME, EMAIL, PHONE)
VALUES(#{name},#{email},#{phone})
</insert>
这里 STUD_ID 列值将会被 MySQL 数据库自动生成,并且生成的值会被设置到 student 对象的 studId 属性上。
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
mapper.insertStudent(student);
现在可以如下获取插入的 STUDENT 记录的 STUD_ID 的值:
int studentId = student.getStudId();
有些数据库如 Oracle 并不支持 AUTO_INCREMENT 列,其使用序列(SEQUENCE)来生成主键值。
假设我们有一个名为 STUD_ID_SEQ 的序列来生成 SUTD_ID 主键值。使用如下代码来生成主键:1
2
3
4
5
6
7<insert id="insertStudent" parameterType="Student">
<selectKey keyProperty="studId" resultType="int" order="BEFORE">
SELECT ELEARNING.STUD_ID_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE)
VALUES(#{studId},#{name},#{email},#{phone})
</insert>
这里我们使用了
order=“before”表示 MyBatis 将取得序列的下一个值作为主键值,并且在执行 INSERT SQL 语句之前将值设置到
studId 属性上。
我们也可以在获取序列的下一个值时,使用触发器(trigger)来设置主键值,并且在执行 INSERT SQL 语句之
前将值设置到主键列上。 如果你采取这样的方式,则对应的 INSERT 映射语句如下所示:1
2
3
4
5
6
7<insert id="insertStudent" parameterType="Student">
INSERT INTO STUDENTS(NAME,EMAIL, PHONE)
VALUES(#{name},#{email},#{phone})
<selectKey keyProperty="studId" resultType="int" order="AFTER">
SELECT ELEARNING.STUD_ID_SEQ.CURRVAL FROM DUAL
</selectKey>
</insert>
- UPDATE 语句
一个 UPDATE SQL 语句可以在元素在映射器 XML 配置文件中配置,如下所示: 1
2
3
4<update id="updateStudent" parameterType="Student">
UPDATE STUDENTS SET NAME=#{name}, EMAIL=#{email}, PHONE=#{phone}
WHERE STUD_ID=#{studId}
</update>
我们可以如下调用此语句:
int noOfRowsUpdated = sqlSession.update("com.mybatis3.mappers.StudentMapper.updateStudent", student);
sqlSession.update() 方法返回执行 UPDATE 语句之后影响的行数。
如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型
安全的方式调用方法,如下所示:
package com.mybatis3.mappers;
public interface StudentMapper{
int updateStudent(Student student);
}
你可以使用映射器 Mapper 接口来调用 updateStudent 语句,如下所示:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int noOfRowsUpdated = mapper.updateStudent(student);
- 删除语句
一个 DELETE SQL 语句可以在元素在映射器 XML 配置文件中配置,如下所示: 1
2
3<delete id="deleteStudent" parameterType="int">
DELETE FROM STUDENTS WHERE STUD_ID=#{studId}
</delete>
我们可以如下调用此语句:
int studId = 1;
int noOfRowsDeleted = sqlSession.delete("com.mybatis3.mappers.StudentMapper.deleteStudent", studId);
sqlSession.delete() 方法返回 delete 语句执行后影响的行数。
如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安全的方式调用方法,如下所示:
package com.mybatis3.mappers;
public interface StudentMapper{
int deleteStudent(int studId);
}
你可以使用映射器 Mapper 接口来调用 deleteStudent 语句,如下所示:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int noOfRowsDeleted = mapper.deleteStudent(studId);
- SELECT语句
MyBatis 真正强大的功能,在于映射 SELECT 查询结果到 JavaBeans 方面的极大灵活性。
让我们看看一个简单的 select 查询是如何(在 MyBatis 中)配置的,如下所示:1
2
3
4
5
6<select id="findStudentById" parameterType="int"
resultType="Student">
SELECT STUD_ID, NAME, EMAIL, PHONE
FROM STUDENTS
WHERE STUD_ID=#{studId}
</select>
我们可以如下调用此语句:
int studId =1;
Student student = sqlSession.selectOne("com.mybatis3.mappers.StudentMapper.findStudentById", studId);
如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安全的方式调用方法,如下所示:
package com.mybatis3.mappers;
public interface StudentMapper{
Student findStudentById(Integer studId);
}
你可以使用映射器 Mapper 接口来调用 updateStudent 语句,如下所示:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.findStudentById(studId);
如果你检查 Student 对象的属性值,你会发现 studId 属性值并没有被 stud_id 列值填充。这是因为 MyBatis 自动对JavaBean 中和列名匹配的属性进行填充。 这就是为什么 name ,email,和 phone 属性被填充,而 studId 属性没有被填充。
为了解决这一问题,我们可以为列名起一个可以与 JavaBean 中属性名匹配的别名,如下所示:1
2
3
4
5
6<select id="findStudentById" parameterType="int"
resultType="Student">
SELECT STUD_ID AS studId, NAME,EMAIL, PHONE
FROM STUDENTS
WHERE STUD_ID=#{studId}
</select>
现在,Student 这个 Bean 对象中的值将会恰当地被 stud_id,name,email,phone 列填充了。
现在,让我们看一下如何执行返回多条结果的 SELECT 语句查询,如下所示:1
2
3
4<select id="findAllStudents" resultType="Student">
SELECT STUD_ID AS studId, NAME,EMAIL, PHONE
FROM STUDENTS
</select>
List<Student> students = sqlSession.selectList("com.mybatis3.mappers.StudentMapper.findAllStudents");
映射器 Mapper 接口 StudentMapper 可以如下定义:
package com.mybatis3.mappers;
public interface StudentMapper{
List<Student> findAllStudents();
}
使用上述代码,我们可以如下调用 findAllStudents 语句:
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.findAllStudents();
如果你注意到上述的 SELECT 映射定义,你可以看到,我们为所有的映射语句中的 stud_id 起了别名。
我们可以使用 ResultMaps,来避免上述的到处重复别名。我们稍后会继续讨论。
除了 java.util.List,你也可以是由其他类型的集合类,如 Set,Map,以及(SortedSet) 。 MyBatis 根据集合的类型,会采用适当的集合实现,如下所示:
- 对于 List,Collection,Iterable 类型,MyBatis 将返回 java.util.ArrayList
- 对于 Map 类型,MyBatis 将返回 java.util.HashMap
- 对于 Set 类型,MyBatis 将返回 java.util.HashSet
- 对于 SortedSet 类型,MyBatis 将返回 java.util.TreeSet
结果集映射 ResultMaps
ResultMaps 被用来 将 SQL SELECT 语句的结果集映射到 JavaBeans的属性中。我们可以定义结果集映射 ResultMaps并且在一些 SELECT 语句上引用 resultMap。 MyBatis 的结果集映射 ResultMaps 特性非常强大,你可以使用它将简单的SELECT 语句映射到复杂的一对一和一对多关系的 SELECT 语句上。
简单 ResultMap
一个映射了查询结果和 Student JavaBean 的简单的 resultMap 定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14<resultMap id="StudentResult" type="com.mybatis3.domain.Student">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>
<select id="findAllStudents" resultMap="StudentResult">
SELECT * FROM STUDENTS
</select>
<select id="findStudentById" parameterType="int" resultMap="StudentResult">
SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}
</select>
表示 resultMap 的 StudentResult id 值应该在此名空间内是唯一的。并且 type 属性应该是完全限定类名或者是返回类型的别名。<result>子元素被用来将一个 resultset 列映射到 JavaBean 的一个属性中。<id>元素和<result>元素功能相同,不过它被用来映射到唯一标识属性, 用来区分和比较对象 (一般和主键列相对应) 。
在<select>语句中,我们使用了 resultMap 属性,而不是 resultType 来引用 StudentResult 映射。 当<select>语句中配置了 resutlMap 属性,MyBatis 会使用此数据库列名与对象属性映射关系来填充 JavaBean 中的属性。
resultType 和 resultMap 二者只能用其一,不能同时使用。
让我们来看另外一个<select>映射语句定义的例子,怎样将查询结果填充到 HashMap 中。 如下所示:1
2
3<select id="findStudentById" parameterType="int" resultType="map">
SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}
</select>
在上述的<select>语句中,我们将 resultType 配置成 map,即 java.util.HashMap 的别名。在这种情况下,结果集的列名将会作为 Map 中的 key 值,而列值将作为 Map 的 value 值。1
2
3
4
5
6HashMap<String,Object> studentMap = sqlSession.selectOne("com.
mybatis3.mappers.StudentMapper.findStudentById", studId);
System.out.println("stud_id :"+studentMap.get("stud_id"));
System.out.println("name :"+studentMap.get("name"));
System.out.println("email :"+studentMap.get("email"));
System.out.println("phone :"+studentMap.get("phone"));
让我们再看一个 使用 resultType=”map”,返回多行结果的例子:1
2
3<select id="findAllStudents" resultType="map">
SELECT STUD_ID, NAME, EMAIL, PHONE FROM STUDENTS
</select>
由于 resultType=”map”和语句返回多行,则最终返回的数据类型应该是 List<HashMap<String,Object>>,如下所示:1
2
3
4
5
6
7
8
9
10List<HashMap<String, Object>> studentMapList =
sqlSession.selectList("com.mybatis3.mappers.StudentMapper.findAllStudents");
for(HashMap<String, Object> studentMap : studentMapList)
{
System.out.println("studId :" + studentMap.get("stud_id"));
System.out.println("name :" + studentMap.get("name"));
System.out.println("email :" + studentMap.get("email"));
System.out.println("phone :" + studentMap.get("phone"));
}
拓展 ResultMap
我们可以从从另外一个1
2
3
4
5
6
7
8
9
10
11
12
13
14<resultMap type="Student" id="StudentResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>
<resultMap type="Student" id="StudentWithAddressResult" extends="StudentResult">
<result property="address.addrId" column="addr_id" />
<result property="address.street" column="street" />
<result property="address.city" column="city" />
<result property="address.state" column="state" />
<result property="address.zip" column="zip" />
<result property="address.country" column="country" />
</resultMap>
id 为 StudentWithAddressResult 的 resultMap 拓展了 id 为 StudentResult 的 resultMap。
如果你只想映射 Student 数据,你可以使用 id 为 StudentResult 的 resultMap,如下所示:1
2
3
4<select id="findStudentById" parameterType="int"
resultMap="StudentResult">
SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}
</select>
如果你想将映射 Student 数据和 Address 数据,你可以使用 id 为 StudentWithAddressResult 的resultMap:1
2
3
4
5
6
7
8<select id="selectStudentWithAddress" parameterType="int"
resultMap="StudentWithAddressResult">
SELECT STUD_ID, NAME, EMAIL, PHONE, A.ADDR_ID, STREET, CITY,
STATE, ZIP, COUNTRY
FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON
S.ADDR_ID=A.ADDR_ID
WHERE STUD_ID=#{studId}
</select>
一对一映射
在我们的域模型样例中,每一个学生都有一个与之关联的地址信息。 表 STUDENTS 有一个 ADDR_ID 列,是 ADDRESSES表的外键。
STUDENTS 表的样例数据如下所示:1
2
3STUD_ID NAME EMAIL PHONE ADDR_ID
1 John john.com 123-456-7890 1
2 Paul paul.com 111-222-3333 2
ADDRESSES 表的样例输入如下所示:1
2
3ADDR_ID STREET CITY STATE ZIP COUNTRY
1 Naperville CHICAGO IL 60515 USA
2 Paul CHICAGO IL 60515 USA
下面让我们看一下怎样取 Student 明细和其 Address 明细。
Student 和 Address 的 JavaBean 以及映射器 Mapper XML 文件定义如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Address{
private Integer addrId;
private String street;
private String city;
private String state;
private String zip;
private String country;
// setters & getters
}
public class Student
{
private Integer studId;
private String name;
private String email;
private PhoneNumber phone;
private Address address;
//setters & getters
}
1 | <resultMap type="Student" id="StudentWithAddressResult"> |
我们可以使用圆点记法为内嵌的对象的属性赋值。 在上述的 resultMap 中,Student 的 address 属性使用了圆点记法被赋上了 address 对应列的值。同样地,我们可以访问任意深度的内嵌对象的属性。 我们可以如下访问内嵌对象属性:1
2
3
4
5
6
7
8
9
10//接口定义
public interface StudentMapper{
Student selectStudentWithAddress(int studId);
}
//使用
int studId = 1;
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.selectStudentWithAddress(studId);
System.out.println("Student :" + student);
System.out.println("Address :" + student.getAddress());
上述样例展示了一对一关联映射的一种方法。然而,使用这种方式映射,如果 address 结果需要在其他的 SELECT 映射语句中映射成 Address 对象,我们需要为每一个语句重复这种映射关系。 MyBatis 提供了更好地实现一对一关联映射的方法:嵌套结果 ResultMap 和嵌套 select 查询语句。接下来,我们将讨论这两种方式。
使用嵌套结果 ResultMap 实现一对一关系映射
我们可以使用一个嵌套结果 ResultMap 方式来获取 Student 及其 Address 信息,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>
<resultMap type="Student" id="StudentWithAddressResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<association property="address" resultMap="AddressResult" />
</resultMap>
<select id="findStudentWithAddress" parameterType="int"
resultMap="StudentWithAddressResult">
SELECT STUD_ID, NAME, EMAIL, A.ADDR_ID, STREET, CITY, STATE,
ZIP, COUNTRY
FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON
S.ADDR_ID=A.ADDR_ID
WHERE STUD_ID=#{studId}
</select>
元素
引用了另外的在同一个 XML 文件中定义的
我们也可以使用1
2
3
4
5
6
7
8
9
10
11
12
13
14<resultMap type="Student" id="StudentWithAddressResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<association property="address" javaType="Address">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</association>
</resultMap>
使用嵌套结果 ResultMap 方式,关联的数据可以通过简单的查询语句(如果需要的话,需要与 joins 连接操作配合)进行加载。
使用嵌套查询实现一对一关系映射
我们可以通过使用嵌套 select 查询来获取 Student 及其 Address 信息,代码如下: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<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>
<select id="findAddressById" parameterType="int"
resultMap="AddressResult">
SELECT * FROM ADDRESSES WHERE ADDR_ID=#{id}
</select>
<resultMap type="Student" id="StudentWithAddressResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<association property="address" column="addr_id" select="findAddressById" />
</resultMap>
<select id="findStudentWithAddress" parameterType="int"
resultMap="StudentWithAddressResult">
SELECT * FROM STUDENTS WHERE STUD_ID=#{Id}
</select>
在此方式中,
Addr_id 列的值将会被作为输入参数传递给 selectAddressById 语句。
我们可以如下调用 findStudentWithAddress 映射语句:1
2
3
4StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = mapper.selectStudentWithAddress(studId);
System.out.println(student);
System.out.println(student.getAddress());
一对多映射
在我们的域模型样例中,一个讲师可以教授一个或者多个课程。 这意味着讲师和课程之间存在一对多的映射关系。
我们可以使用
TUTORS 表的样例数据如下:1
2
3TUTOR_ID NAME EMAIL PHONE ADDR_ID
1 John john.com 123-456-7890 1
2 Ying ying.com 111-222-3333 2
COURSE 表的样例数据如下:1
2
3
4COURSE_ID NAME DESCRIPTION START_DATE END_DATE TUTOR_ID
1 JavaSE Java SE 2013-01-10 2013-02-10 1
2 JavaEE Java EE 6 2013-01-10 2013-03-10 2
3 MyBatis MyBatis 2013-01-10 2013-02-20 2
在上述的表数据中,John 讲师教授一个课程,而 Ying 讲师教授两个课程。
Course 和 Tutor 的 JavaBean 定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 public class Course
{
private Integer courseId;
private String name;
private String description;
private Date startDate;
private Date endDate;
private Integer tutorId;
//setters & getters
}
public class Tutor
{
private Integer tutorId;
private String name;
private String email;
private Address address;
private List<Course> courses;
/ setters & getters
}
现在让我们看看如何获取讲师信息以及其所教授的课程列表信息。<collection>元素被用来将多行课程结果映射成一个课程 Course 对象的一个集合。 和一对一映射一样,我们可以使用嵌套结果 ResultMap 和嵌套 Select 语句两种方式映射实现一对多映射。
使用内嵌结果 ResultMap 实现一对多映射
我们可以使用嵌套结果 resultMap 方式获得讲师及其课程信息,代码如下:
1 | <resultMap type="Course" id="CourseResult"> |
这里我们使用了一个简单的使用了 JOINS 连接的 Select 语句获取讲师及其所教课程信息。
使用嵌套 Select 语句实现一对多映射
我们可以使用嵌套 Select 语句方式获得讲师及其课程信息,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<resultMap type="Course" id="CourseResult">
<id column="course_id" property="courseId" />
<result column="name" property="name" />
<result column="description" property="description" />
<result column="start_date" property="startDate" />
<result column="end_date" property="endDate" />
</resultMap>
<resultMap type="Tutor" id="TutorResult">
<id column="tutor_id" property="tutorId" />
<result column="tutor_name" property="name" />
<result column="email" property="email" />
<association property="address" resultMap="AddressResult" />
<collection property="courses" column="tutor_id" select="findCoursesByTutor" />
</resultMap>
<select id="findTutorById" parameterType="int" resultMap="TutorResult">
SELECT T.TUTOR_ID, T.NAME AS TUTOR_NAME, EMAIL
FROM TUTORS T WHERE T.TUTOR_ID=#{tutorId}
</select>
<select id="findCoursesByTutor" parameterType="int" resultMap="CourseResult">
SELECT * FROM COURSES WHERE TUTOR_ID=#{tutorId}
</select>
在这种方式中,1
2
3
4
5
6
7
8
9
10
11
12
13public interface TutorMapper
{
Tutor findTutorById(int tutorId);
}
TutorMapper mapper = sqlSession.getMapper(TutorMapper.class);
Tutor tutor = mapper.findTutorById(tutorId);
System.out.println(tutor);
List<Course> courses = tutor.getCourses();
for (Course course : courses)
{
System.out.println(course);
}
嵌套 Select 语句查询会导致 N+1 选择问题。 首先,主查询将会执行(1 次) ,对于主
查询返回的每一行,另外一个查询将会被执行(主查询 N 行,则此查询 N 次) 。 对于
大型数据库而言,这会导致很差的性能问题。
动态 SQL
有时候,静态的 SQL 语句并不能满足应用程序的需求。我们可以根据一些条件,来动态地构建 SQL 语句。
例如,在 Web 应用程序中,有可能有一些搜索界面,需要输入一个或多个选项,然后根据这些已选择的条件去执行检索操作。在实现这种类型的搜索功能,我们可能需要根据这些条件 来构建动态的 SQL 语句。 如果用户提供了任何输入条件,我们需要将那个条件 添加到 SQL 语句的 WHERE 子句中。
MyBatis 通过使用
IF条件
假定我们有一个课程搜索界面,设置了 讲师(Tutor)下拉列表框,课程名称(CourseName)文本输入框,开始
时间(StartDate)输入框,结束时间(EndDate)输入框,作为搜索条件。假定课讲师下拉列表是必须选的,其他的都是可选的。
当用户点击 搜索 按钮时,我们需要显示符合以下条件的成列表:
- 特定讲师的课程
- 课程名 包含输入的课程名称关键字的课程;如果课程名称输入为空,则取所有课程
- 在开始时间和结束时间段内的课程
我们可以对应的映射语句,如下所示:
1 | <resultMap type="Course" id="CourseResult"> |
1 | public interface CourseMapper |
此处将生成查询语句 SELECT * FROM COURSES WHERE TUTOR_ID= ? AND NAME like ? AND START_DATE >= ?。准备根据给定条件的动态 SQL 查询将会派上用场。
MyBatis 是使用 ONGL(Object Graph Navigation Language)表达式来构建动态 SQL 语句。
choose,when 和 otherwise 条件
有时候,查询功能是以查询类别为基础的。 首先,用户需要选择是否希望通过选择 讲师,课程名称,开始时间,
或结束时间作为查询条件类别来进行查询,然后根据选择的查询类别,输入相应的参数。在这样的情景中,我们需要只使用其中一种查询类别。
MyBatis 提供了<choose>元素支持此类型的 SQL 预处理。
现在让我们书写一个适用此情景的 SQL 映射语句。如果没有选择查询类别,则查询开始时间在今天之后的课程,
代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14<select id="searchCourses" parameterType="hashmap" resultMap="CourseResult">
SELECT * FROM COURSES
<choose>
<when test="searchBy == 'Tutor'">
WHERE TUTOR_ID= #{tutorId}
</when>
<when test="searchBy == 'CourseName'">
WHERE name like #{courseName}
</when>
<otherwise>
WHERE TUTOR start_date >= now()
</otherwise>
</choose>
</select>
MyBatis 计算<otherwise>内的子句。
Where 条件
有时候,所有的查询条件 (criteria)应该是可选的。 在需要使用至少一种查询条件的情况下, 我们应该使用 WHERE子句。并且, 如果有多个条件,我们需要在条件中添加 AND 或 OR。 MyBatis 提供了<where>元素支持这种类型的动态 SQL 语句。
在我们查询课程界面,我们假设所有的查询条件是可选的。 进而,当需要提供一个或多个查询条件时,应该改使用
WHERE 子句。
1 | <select id="searchCourses" parameterType="hashmap" resultMap="CourseResult"> |
<where>元素只有在其内部标签有返回内容时才会在动态语句上插入 WHERE 条件语句。 并且,如果 WHERE 子句以AND 或者 OR 打头,则打头的 AND 或 OR 将会被移除。
如果 tutor_id 参数值为 null,并且 courseName 参数值不为 null,则<where>标签会将
AND name like #{courseName} 中的 AND 移除掉,生成的 SQL WHERE 子句为:where name like #{courseName}。
条件
<trim>元素和<where>元素类似,但是<trim>提供了在添加前缀/后缀 或者 移除前缀/后缀方面提供更大的灵活性。
1 | <select id="searchCourses" parameterType="hashmap" resultMap="CourseResult"> |
这里如果任意一个<if>条件为 true,<trim>元素会插入 WHERE,并且移除紧跟 WHERE 后面的 AND 或 OR
foreach 循环
另外一个强大的动态 SQL 语句构造标签即是
假设我们想找到 tutor_id 为 1,3,6 的讲师所教授的课程,我们可以传递一个 tutor_id 组成的列表给映射语句,然后通过1
2
3
4
5
6
7
8
9
10<select id="searchCoursesByTutors" parameterType="map" resultMap="CourseResult">
SELECT * FROM COURSES
<if test="tutorIds != null">
<where>
<foreach item="tutorId" collection="tutorIds">
OR tutor_id=#{tutorId}
</foreach>
</where>
</if>
</select>
1 | public interface CourseMapper |
现在让我们来看一下怎样使用<foreach>生成 IN 子句:1
2
3
4
5
6
7
8
9
10
11
12<select id="searchCoursesByTutors" parameterType="map" resultMap="CourseResult">
SELECT * FROM COURSES
<if test="tutorIds != null">
<where>
tutor_id IN
<foreach item="tutorId" collection="tutorIds" open="("
separator="," close=")">
#{tutorId}
</foreach>
</where>
</if>
</select>
set条件
<set>元素和<where>元素类似,如果其内部条件判断有任何内容返回时,他会插入 SET SQL 片段。1
2
3
4
5
6
7
8
9<update id="updateStudent" parameterType="Student">
update students
<set>
<if test="name != null">name=#{name},</if>
<if test="email != null">email=#{email},</if>
<if test="phone != null">phone=#{phone},</if>
</set>
where stud_id=#{id}
</update>
这里,如果<set>将会插入 set 关键字和其文本内容,并且会剔除将末尾的 “,”。
在上述的例子中,如果 phone!=null,<set>将会让会移除 phone=#{phone}后的逗号“,”,
生成 set phone=#{phone} 。
MyBaits 食谱
除了简化数据库编程外,MyBatis 还提供了各种功能,这些对实现一些常用任务非常有用,比如按页加载表数据,存取 CLOB/BLOB 类型的数据,处理枚举类型值,等等。让我们来看看其中一些特性吧。
处理枚举类型
MyBatis 支持开箱方式持久化 enum 类型属性。假设 STUDENTS 表中有一列 gender(性别)类型为 varchar,存储”MALE”或者“FEMALE”两种值。并且,Student 对象有一个 enum 类型的 gender 属性,如下所示:
1 | public enum Gender |
默认情况下,MyBatis 使用 EnumTypeHandler 来处理 enum 类型的 Java 属性,并且将其存储为 enum 值的名称。 你不需要为此做任何额外的配置。你可以可以向使用基本数据类型属性一样使用 enum 类型属性,代码如下:1
2
3
4
5
6
7
8
9
10public class Student
{
private Integer id;
private String name;
private String email;
private PhoneNumber phone;
private Address address;
private Gender gender;
//setters and getters
}
1 | <insert id="insertStudent" parameterType="Student" |
当你执行 insertStudent 语句的时候, MyBatis 会取 Gender 枚举(FEMALE/MALE) 的名称, 然后将其存储到 GENDER列中。
如果你希望存储原 enum 的顺序位置,而不是 enum 名,,你需要明确地配置它。
如果你想存储 FEMALE 为 0, MALE 为 1 到 gender 列中,你需要在 mybatis-config.xml 文件中配置
EnumOrdinalTypeHandler:1
2<typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler"
javaType="com.mybatis3.domain.Gender"/>
使用顺序位置为值存储到数据库时要当心。 顺序值是根据 enum 中的声明顺序赋值的。如果你改变了 Gender enum 的声明顺序,则数据库存储的数据和此顺序值就不匹配了。
处理 CLOB/BLOB 类型数据
MyBatis 提供了内建的对 CLOB/BLOB 类型列的映射处理支持。
假设我们有如下的表结构来存储学生和讲师的照片和简介信息:1
2
3
4
5
6
7
8CREATE TABLE USER_PICS
(
ID INT(11) NOT NULL AUTO_INCREMENT,
NAME VARCHAR(50) DEFAULT NULL,
PIC BLOB,
BIO LONGTEXT,
PRIMARY KEY (ID)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=LATIN1;
这里,照片可以是 PNG,JPG 或其他格式的。简介信息可以是学生或者讲师的漫长的人生经历。 默认情况下,MyBatis将 CLOB 类型的列映射到 java.lang.String 类型上、 而把 BLOB 列映射到 byte[] 类型上。
1 | public class UserPic |
创建 UserPicMapper.xml 文件,配置映射语句,代码如下:
1 | <insert id="insertUserPic" parameterType="UserPic"> |
下列的 insertUserPic()展示了如何将数据插入到 CLOB/BLOB 类型的列上: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
26public void insertUserPic() {
byte[] pic = null;
try {
File file = new File("C:\\Images\\UserImg.jpg");
InputStream is = new FileInputStream(file);
pic = new byte[is.available()];
is.read(pic);
is.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
String name = "UserName";
String bio = "put some lenghty bio here";
UserPic userPic = new UserPic(0, name, pic, bio);
SqlSession sqlSession = MyBatisUtil.openSession();
try {
UserPicMapper mapper = sqlSession.getMapper(UserPicMapper.class);
mapper.insertUserPic(userPic);
sqlSession.commit();
} finally {
sqlSession.close();
}
}
下面的 getUserPic()方法展示了怎样将 CLOB 类型数据读取到 String 类型,BLOB 类型数据读取成 byte[]属性:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public void getUserPic() {
UserPic userPic = null;
SqlSession sqlSession = MyBatisUtil.openSession();
try {
UserPicMapper mapper = sqlSession.getMapper(UserPicMapper.class);
userPic = mapper.getUserPic(1);
} finally {
sqlSession.close();
}
byte[] pic = userPic.getPic();
try {
OutputStream os = new FileOutputStream(new File("C:\\Images\\UserImage_FromDB.jpg"));
os.write(pic);
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
传入多个输入参数
MyBatis 中的映射语句有一个 parameterType 属性来制定输入参数的类型。如果我们想给映射语句传入多个参数的话,我们可以将所有的输入参数放到 HashMap 中,将 HashMap 传递给映射语句。
MyBatis 还提供了另外一种传递多个输入参数给映射语句的方法。假设我们想通过给定的 name 和 email 信息查找学生信息,定义查询接口如下:1
2
3
4Public interface StudentMapper
{
List<Student> findAllStudentsByNameEmail(String name, String email);
}
MyBatis 支持 将多个输入参数传递给映射语句,并以#{param}的语法形式引用它们:1
2
3
4<select id="findAllStudentsByNameEmail" resultMap="StudentResult">
select stud_id, name,email, phone from Students
where name=#{param1} and email=#{param2}
</select>
这里#{param1}引用第一个参数 name,而#{param2}引用了第二个参数 email。1
2StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
studentMapper.findAllStudentsByNameEmail(name, email);
多行结果集映射成 Map
如果你有一个映射语句返回多行记录,并且你想以 HashMap 的形式存储记录的值,使用记录列名作为 key 值,而记录对应值或为 value 值。我们可以使用 sqlSession.selectMap(),如下所示:
1 | <select id=" findAllStudents" resultMap="StudentResult"> |
1 | Map<Integer, Student> studentMap = sqlSession.selectMap("com.mybatis3.mappers.StudentMapper.findAllStudents", "studId"); |
这里 studentMap 将会将 studId 作为 key 值,而 Student 对象作为 value 值。
使用 RowBounds 对结果集进行分页
有时候,我们会需要跟海量的数据打交道,比如一个有数百万条数据级别的表。 由于计算机内存的现实我们不可能一次性加载这么多数据,我们可以获取到数据的一部分。特别是在 Web 应用程序中,分页机制被用来以一页一页的形式展示海量的数据。
MyBatis 可以使用 RowBounds 逐页加载表数据。 RowBounds 对象可以使用 offset 和 limit 参数来构建。 参数offset 表示开始位置,而 limit 表示要取的记录的数目。
假设如果你想每页加载并显示 25 条学生的记录,你可以使用如下的代码:1
2
3<select id="findAllStudents" resultMap="StudentResult">
select * from Students
</select>
然后,你可以加载如下加载第一页数据(前 25 条):
1 | int offset =0 , limit =25; |
若要展示第二页,使用 offset=25,limit=25;第三页,则为 offset=50,limit=25。
使用 ResultSetHandler 自定义结果集 ResultSet 处理
MyBatis 在将查询结果集映射到 JavaBean 方面提供了很大的选择性。 但是,有时候我们会遇到由于特定的目的,需要我们自己处理 SQL 查询结果的情况。 MyBatis 提供了 ResultHandler 插件形式允许我们以任何自己喜欢的方式处理结果集 ResultSet。
假设我们想从学生的 stud_id 被用作 key,而 name 被用作 value 的 HashMap 中获取到 student 信息。
mybatis-3.2.2 并不支持使用 resultMap 配置将查询的结果集映射成一个属性为key,而另外属性为 value 的 HashMap。
sqlSession.selectMap()则可以返回 以给定列为 key,记录对象为 value 的 map。
我们不能将其配置成使用其中一个属性作为 key,而另外的属性作为 value。
对于 sqlSession.select()方法,我们可以传递给它一个 ResultHandler 的实现,它会被调用来处理 ResultSet的每一条记录。1
2
3
4public interface ResultHandler
{
void handleResult(ResultContext context);
}
现在然我们来看一下怎么使用 ResultHandler 来处理结果集 ResultSet,并返回自定义化的结果。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public Map<Integer, String> getStudentIdNameMap()
{
final Map<Integer, String> map = new HashMap<Integer, String>();
SqlSession sqlSession = MyBatisUtil.openSession();
try
{
sqlSession.select("com.mybatis3.mappers.StudentMapper.findAllStudents",
new ResultHandler(){
public void handleResult(ResultContext context){
Student student = (Student) context.getResultObject();
map.put(student.getStudId(), student.getName());
}
}
);
}
finally
{
sqlSession.close();
}
return map;
}
在上述的代码中,我们提供了匿名内部 ResultHandler 实现类,在 handleResult()方法中,我们使用context.getResultObject()获取当前的 result 对象,即 Student 对象,因为我们定义了 findAllStudent 映射语句的 resultMap=”studentResult“。对查询返回的每一行都会调用 handleResult()方法,并且我们从 Student 对象中取出 studId 和 name,将其放到 map 中。
缓存
将从数据库中加载的数据缓存到内存中,是很多应用程序为了提高性能而采取的一贯做法。 MyBatis 对通过映射的SELECT 语句加载的查询结果提供了内建的缓存支持。默认情况下,启用一级缓存;即,如果你使用同一个 SqlSession接口对象调用了相同的 SELECT 语句,则直接会从缓存中返回结果,而不是再查询一次数据库。
我们可以在 SQL 映射器 XML 配置文件中使用<cache />元素添加全局二级缓存。
当你加入了<cache />元素,将会出现以下情况:
- 所有的在映射语句文件定义的
<select>语句的查询结果都会被缓存 - 所有的在映射语句文件定义的
<insert>,<update>和<delete>语句将会刷新缓存 - 缓存根据最近最少被使用(Least Recently Used,LRU)算法管理
- 缓存不会被任何形式的基于时间表的刷新(没有刷新时间间隔) ,即不支持定时刷新机制
- 缓存将存储 1024 个 查询方法返回的列表或者对象的引用
- 缓存会被当作一个读/写缓存。 这是指检索出的对象不会被共享,并且可以被调用者安全地修改,不会其他潜
在的调用者或者线程的潜在修改干扰。(即,缓存是线程安全的)
你也可以通过复写默认属性来自定义缓存的行为,如下所示:1
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
以下是对上述属性的描述:
- eviction:此处定义缓存的移除机制。默认值是 LRU,其可能的值有:LRU(least recently used,最近最
少使用),FIFO(first in first out,先进先出),SOFT(soft reference,软引用),WEAK(weakreference,弱引用) 。 - flushInterval:定义缓存刷新间隔,以毫秒计。默认情况下不设置。所以不使用刷新间隔,缓存 cache 只
有调用语句的时候刷新。 - size:此表示缓存 cache 中能容纳的最大元素数。默认值是 1024,你可以设置成任意的正整数。
- readOnly:一个只读的缓存 cache 会对所有的调用者返回被缓存对象的同一个实例(实际返回的是被返回对
象的一份引用)。一个读/写缓存 cache 将会返回被返回对象的一分拷贝(通过序列化) 。默认情况下设
置为 false。 可能的值有 false 和 true。
一个缓存的配置和缓存实例被绑定到映射器配置文件所在的名空间(namespace)上,所以在相同名空间内的所有语句被绑定到一个 cache 中。
默认的映射语句的 cache 配置如下:
1 | <select ... flushCache="false" useCache="true"/> |
你可以为任意特定的映射语句复写默认的 cache 行为;例如,对一个 select 语句不使用缓存,可以设置
useCache=“false”。
除了内建的缓存支持,MyBatis 也提供了与第三方缓存类库如 Ehcache,OSCache,Hazelcast 的集成支持。 你可以在 MyBatis 官方网站 https://code.google.com/p/mybatis/wiki/Caches 上找到关于继承第三方缓存类库的更多信息。
使用注解配置 SQL 映射器
在上一章,我们看到了我们是怎样在映射器 Mapper XML 配置文件中配置映射语句的。 MyBatis 也支持使用注解来
配置映射语句。当我们使用基于注解的映射器接口时,我们不再需要在 XML 配置文件中配置了。 如果你愿意,你也可以同时使用基于 XML 和基于注解的映射语句。
本章将涵盖以下话题:
- 在映射器 Mapper 接口上使用注解
- 映射语句
- @Insert, @Update, @Delete,@SeelctStatements
- 结果映射
- 一对一映射
- 一对多映射
- 动态 SQL
- @SelectProvider
- @InsertProvider
- @UpdateProvider
- @DeleteProvider
在映射器 Mapper 接口上使用注解
MyBatis 对于大部分的基于 XML 的映射器元素(包括<select>,<update>)提供了对应的基于注解的配置项。然而在某些情况下,基于注解配置 还不能支持基于 XML 的一些元素。
映射语句
MyBatis 提供了多种注解来支持不同类型的语句(statement)如 SELECT,INSERT,UPDATE,DELETE。 让我们看一下具体怎样配置映射语句。
- @Insert
我们可以使用@Insert 注解来定义一个 INSERT 映射语句:1
2
3
4
5
6
7package com.mybatis3.mappers;
public interface StudentMapper
{
("INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,ADDR_ID, PHONE)
VALUES(#{studId},#{name},#{email},#{address.addrId},#{phone})")
int insertStudent(Student student);
}
使用了@Insert 注解的 insertMethod()方法将返回 insert 语句执行后影响的行数。
自动生成主键
在上一章中我们讨论过主键列值可以自动生成。我们可以使用@Options 注解的 userGeneratedKeys 和keyProperty 属性让数据库产生 auto_increment(自增长)列的值,然后将生成的值设置到输入参数对象的属性中。
1 | ("INSERT INTO STUDENTS(NAME,EMAIL,ADDR_ID, PHONE) |
这里 STUD_ID 列值将会通过 MySQL 数据库自动生成。并且生成的值将会被设置到 student 对象的 studId 属性中。1
2
3StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
mapper.insertStudent(student);
int studentId = student.getStudId();
有一些数据库如 Oracle,并不支持 AUTO_INCREMENT 列属性,它使用序列(SEQUENCE)来产生主键的值。
我们可以使用@SelectKey 注解来为任意 SQL 语句来指定主键值,作为主键列的值。
假设我们有一个名为 STUD_ID_SEQ 的序列来生成 STUD_ID 主键值。1
2
3
4
5("INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL,ADDR_ID, PHONE)
VALUES(#{studId},#{name},#{email},#{address.addrId},#{phone})")
(statement="SELECT STUD_ID_SEQ.NEXTVAL FROM DUAL",
keyProperty="studId", resultType=int.class, before=true)
int insertStudent(Student student);
这里我们使用了@SelectKey 来生成主键值,并且存储到了 student 对象的 studId 属性上。 由于我们设置了
before=true,该语句将会在执行 INSERT 语句之前执行。
如果你使用序列作为触发器来设置主键值,我们可以在 INSERT 语句执行后,从 sequence_name.currval 获取数据库产生的主键值。
1 | ("INSERT INTO STUDENTS(NAME,EMAIL,ADDR_ID, PHONE) |
- @Update
我们可以使用@Update 注解来定义一个 UPDATE 映射语句,如下所示:1
2
3("UPDATE STUDENTS SET NAME=#{name}, EMAIL=#{email},
PHONE=#{phone} WHERE STUD_ID=#{studId}")
int updateStudent(Student student);
使用了@Update 的 updateStudent()方法将会返回执行了 update 语句后影响的行数。1
2StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int noOfRowsUpdated = mapper.updateStudent(student);
- @Delete
我们可以使用@Delete 注解来定义一个 DELETE 映射语句,如下所示:1
2("DELETE FROM STUDENTS WHERE STUD_ID=#{studId}")
int deleteStudent(int studId);
- @Select
我们可以使用@ Select 注解来定义一个 SELECT 映射语句。
让我们看一下怎样使用注解配置一个简单的 select 查询。1
2
3
4
5
6
7package com.mybatis3.mappers;
public interface StudentMapper
{
("SELECT STUD_ID AS STUDID, NAME, EMAIL, PHONE FROM
STUDENTS WHERE STUD_ID=#{studId}")
Student findStudentById(Integer studId);
}
为了将列名和 Student bean 属性名匹配,我们为 stud_id 起了一个 studId 的别名。如果返回了多行结果,将抛出 TooManyResultsException 异常。
结果映射
我们可以将查询结果通过别名或者是@Results 注解与 JavaBean 属性映射起来。
现在让我们看看怎样使用@Results 注解将指定列于指定 JavaBean 属性映射器来,执行 SELECT 查询的:1
2
3
4
5
6
7
8
9
10
11
12
13package com.mybatis3.mappers;
public interface StudentMapper
{
("SELECT * FROM STUDENTS")
(
{
(id = true, column = "stud_id", property = "studId"),
(column = "name", property = "name"),
(column = "email", property = "email"),
(column = "addr_id", property = "address.addrId")
})
List<Student> findAllStudents();
}
@Results 注解和映射器 XML 配置文件元素
想对应。 然而,MyBatis3.2.2 不能为@Results 注解赋予一个 ID。 所以,不像 元素,我们不应在不同的映射语句中重用@Results 声明。这意味着即使@Results注解完全相同,我们也需要(在不同的映射接口中)重复@Results 声明。
例如,看下面的 findStudentById()和 findAllStudents()方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19("SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}")
(
{
(id = true, column = "stud_id", property = "studId"),
(column = "name", property = "name"),
(column = "email", property = "email"),
(column = "addr_id", property = "address.addrId")
})
Student findStudentById(int studId);
("SELECT * FROM STUDENTS")
(
{
(id = true, column = "stud_id", property = "studId"),
(column = "name", property = "name"),
(column = "email", property = "email"),
(column = "addr_id", property = "address.addrId")
})
List<Student> findAllStudents();
这里两个语句的@Results 配置完全相同,但是我必须得重复它。 这里有一个解决方法。 我们可以创建一个映射器Mapper 配置文件, 然后配置
在 StudentMapper.xml 中定义一个 ID 为 StudentResult 的1
2
3
4
5
6
7
8<mapper namespace="com.mybatis3.mappers.StudentMapper">
<resultMap type="Student" id="StudentResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>
</mapper>
在 StudentMapper.java 中,使用@ResultMap 引用名为 StudentResult 的 resultMap。1
2
3
4
5
6
7
8
9
10public interface StudentMapper
{
("SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}")
("com.mybatis3.mappers.StudentMapper.StudentResult")
Student findStudentById(int studId);
("SELECT * FROM STUDENTS")
("com.mybatis3.mappers.StudentMapper.StudentResult")
List<Student> findAllStudents();
}
一对一映射
MyBatis 提供了@One 注解来使用嵌套 select 语句(Nested-Select)加载一对一关联查询数据。
让我们看看怎样使用@One 注解获取学生及其地址信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public interface StudentMapper
{
("SELECT ADDR_ID AS ADDRID, STREET, CITY, STATE, ZIP, COUNTRY
FROM ADDRESSES WHERE ADDR_ID=#{id}")
Address findAddressById(int id);
("SELECT * FROM STUDENTS WHERE STUD_ID=#{studId} ")
(
{
(id = true, column = "stud_id", property = "studId"),
(column = "name", property = "name"),
(column = "email", property = "email"),
(property = "address", column = "addr_id",
one = (select = "com.mybatis3.mappers.StudentMapper.
findAddressById"))
})
Student selectStudentWithAddress(int studId);
}
这里我们使用了@One 注解的 select 属性来指定一个使用了完全限定名的方法上,该方法会返回一个 Address 对象。 使用 column=”addr_id”,则 STUEDNTS 表中列 addr_id 的值将会作为输入参数传递给findAddressById()方法。
如果@One SELECT 查询返回了多行结果,则会抛出 TooManyResultsException 异常。1
2
3
4
5
6int studId = 1;
StudentMapper studentMapper =
sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.selectStudentWithAddress(studId);
System.out.println("Student :"+student);
System.out.println("Address :"+student.getAddress());
在第三章,使用 XML 配置 SQL 映射器中我们讨论过,我们可以通过基于 XML 的映射器配置, 使用嵌套结果 ResultMap来加载一对一关联的查询。 而 MyBatis3.2.2 版本,并没有对应的注解支持。但是我们可以在映射器 Mapper 配置文件中配置
在 StudentMapper.xml 中配置1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<mapper namespace="com.mybatis3.mappers.StudentMapper">
<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>
<resultMap type="Student" id="StudentWithAddressResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<association property="address" resultMap="AddressResult" />
</resultMap>
</mapper>
1 | public interface StudentMapper |
一对多映射
MyBatis 提供了@Many 注解,用来使用嵌套 Select 语句加载一对多关联查询。
现在让我们看一下如何使用@Many 注解获取一个讲师及其教授课程列表信息: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
33public interface TutorMapper
{
("select addr_id as addrId, street, city, state, zip,
country from addresses where addr_id=#{id}")
Address findAddressById(int id);
("select * from courses where tutor_id=#{tutorId}")
(
{
(id = true, column = "course_id", property = "courseId"),
(column = "name", property = "name"),
(column = "description", property = "description"),
(column = "start_date" property = "startDate"),
(column = "end_date" property = "endDate")
})
List<Course> findCoursesByTutorId(int tutorId);
("SELECT tutor_id, name as tutor_name, email, addr_id
FROM tutors where tutor_id=#{tutorId}")
(
{
(id = true, column = "tutor_id", property = "tutorId"),
(column = "tutor_name", property = "name"),
(column = "email", property = "email"),
(property = "address", column = "addr_id",
one = (select = " com.mybatis3.
mappers.TutorMapper.findAddressById")),
(property = "courses", column = "tutor_id",
many = (select = "com.mybatis3.mappers.TutorMapper.
findCoursesByTutorId"))
})
Tutor findTutorById(int tutorId);
}
这里我们使用了@Many 注解的 select 属性来指向一个完全限定名称的方法,该方法将返回一个List<Course>对象。使用 column=”tutor_id”,TUTORS 表中的 tutor_id 列值将会作为输入参数传递给 findCoursesByTutorId()方法。
在第三章,使用 XML 配置 SQL 映射器中我们讨论过,我们可以通过基于 XML 的映射器配置, 使用嵌套结果 ResultMap来加载一对多关联的查询。 而 MyBatis3.2.2 版本,并没有对应的注解支持。但是我们可以在映射器 Mapper 配置文件中配置<resultMap>并且使用@ResultMap 注解来引用它。
在 TutorMapper.xml 中配置<resultMap>,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<mapper namespace="com.mybatis3.mappers.TutorMapper">
<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>
<resultMap type="Course" id="CourseResult">
<id column="course_id" property="courseId" />
<result column="name" property="name" />
<result column="description" property="description" />
<result column="start_date" property="startDate" />
<result column="end_date" property="endDate" />
</resultMap>
<resultMap type="Tutor" id="TutorResult">
<id column="tutor_id" property="tutorId" />
<result column="tutor_name" property="name" />
<result column="email" property="email" />
<association property="address" resultMap="AddressResult" />
<collection property="courses" resultMap="CourseResult" />
</resultMap>
</mapper>
1 | public interface TutorMapper |
动态 SQL
有 时 候 我 们 需 要 根 据 输 入 条 件 动 态 地 构 建 SQL 语 句 。 MyBatis 提 供 了 各 种 注 解 如@InsertProvider,@UpdateProvider,@DeleteProvider 和@SelectProvider,来帮助构建动态 SQL 语句,然后让MyBatis 执行这些 SQL 语句。
@SelectProvider
现在让我们来看一个使用@SelectProvider 注解来创建一个简单的 SELECT 映射语句的例子。
创建一个 TutorDynaSqlProvider.java 类,以及 findTutorByIdSql()方法,如下所示:1
2
3
4
5
6
7
8
9
10package com.mybatis3.sqlproviders;
import org.apache.ibatis.jdbc.SQL;
public class TutorDynaSqlProvider
{
public String findTutorByIdSql(int tutorId)
{
return "SELECT TUTOR_ID AS tutorId, NAME, EMAIL FROM TUTORS
WHERE TUTOR_ID=" + tutorId;
}
}
在 TutorMapper.java 接口中创建一个映射语句,如下:1
2(type=TutorDynaSqlProvider.class, method="findTutorByIdSql")
Tutor findTutorById(int tutorId);
这里我们使用了@SelectProvider 来指定了一个类,及其内部的方法,用来提供需要执行的 SQL 语句。
但是使用字符串拼接的方法唉构建 SQL 语句是非常困难的,并且容易出错。所以 MyBaits 提供了一个 SQL 工具类不使用字符串拼接的方式,简化构造动态 SQL 语句。
现在,让我们看看如何使用 org.apache.ibatis.jdbc.SQL 工具类来准备相同的 SQL 语句。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package com.mybatis3.sqlproviders;
import org.apache.ibatis.jdbc.SQL;
public class TutorDynaSqlProvider
{
public String findTutorByIdSql(final int tutorId)
{
return new SQL(){
{
SELECT("tutor_id as tutorId, name, email");
FROM("tutors");
WHERE("tutor_id=" + tutorId);
}
} .toString();
}
}
SQL 工具类会处理以合适的空格前缀和后缀来构造 SQL 语句。
动态 SQL provider 方法可以接收以下其中一种参数:
- 无参数
- 和映射器 Mapper 接口的方法同类型的参数
- java.util.Map
如果 SQL 语句的准备不取决于输入参数,你可以使用不带参数的 SQL Provider 方法。
1 | public String findTutorByIdSql() |
这里我们没有使用输入参数构造 SQL 语句,所以它可以是一个无参方法。
如果映射器 Mapper 接口方法只有一个参数,那么可以定义 SQL Provider 方法,它接受一个与 Mapper 接口方法相同类型的参数。
例如映射器 Mapper 接口有如下定义:1
Tutor findTutorById(int tutorId);
这里 findTutorById(int)方法只有一个 int 类型的参数。我们可以定义 findTutorByIdSql(int)方法作为 SQLprovider 方法。1
2
3
4
5
6
7
8
9
10
11public String findTutorByIdSql(final int tutorId)
{
return new SQL()
{
{
SELECT("tutor_id as tutorId, name, email");
FROM("tutors");
WHERE("tutor_id=" + tutorId);
}
} .toString();
}
如果映射器 Mapper 接口有多个输入参数,我们可以使用参数类型为 java.util.Map 的方法作为 SQLprovider 方法。然后映射器 Mapper 接口方法所有的输入参数将会被放到 map 中,以 param1,param2 等等作为 key,将输入参数按序作为 value。 你也可以使用 0,1,2 等作为 key 值来取的输入参数。
1 | (type = TutorDynaSqlProvider.class, |
SQL 工具类也提供了其他的方法来表示 JOINS,ORDER_BY,GROUP_BY 等等。
让我们看一个使用 LEFT_OUTER_JOIN 的例子: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
27public class TutorDynaSqlProvider
{
public String selectTutorById()
{
return new SQL()
{
{
SELECT("t.tutor_id, t.name as tutor_name, email");
SELECT("a.addr_id, street, city, state, zip, country");
SELECT("course_id, c.name as course_name, description,
start_date, end_date");
FROM("TUTORS t");
LEFT_OUTER_JOIN("addresses a on t.addr_id=a.addr_id");
LEFT_OUTER_JOIN("courses c on t.tutor_id=c.tutor_id");
WHERE("t.TUTOR_ID = #{id}");
}
} .toString();
}
}
public interface TutorMapper
{
(type = TutorDynaSqlProvider.class,
method = "selectTutorById")
("com.mybatis3.mappers.TutorMapper.TutorResult")
Tutor selectTutorById(int tutorId);
}
由于没有支持使用内嵌结果 ResultMap 的一对多关联映射的注解支持,我们可以使用基于 XML 的
1 | <mapper namespace="com.mybatis3.mappers.TutorMapper"> |
使用了动态的 SQL provider,我们可以取得讲师及其地址和课程明细。
@InsertProvider
我们可以使用@InsertProvider 注解创建动态的 INSERT 语句,如下所示: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
27public class TutorDynaSqlProvider
{
public String insertTutor(final Tutor tutor)
{
return new SQL()
{
{
INSERT_INTO("TUTORS");
if (tutor.getName() != null)
{
VALUES("NAME", "#{name}");
}
if (tutor.getEmail() != null)
{
VALUES("EMAIL", "#{email}");
}
}
} .toString();
}
}
public interface TutorMapper
{
(type = TutorDynaSqlProvider.class,
method = "insertTutor")
(useGeneratedKeys = true, keyProperty = "tutorId")
int insertTutor(Tutor tutor);
}
@UpdateProvider
我们可以通过@UpdateProvider 注解创建 UPDATE 语句,如下所示: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
28public class TutorDynaSqlProvider
{
public String updateTutor(final Tutor tutor)
{
return new SQL()
{
{
UPDATE("TUTORS");
if (tutor.getName() != null)
{
SET("NAME = #{name}");
}
if (tutor.getEmail() != null)
{
SET("EMAIL = #{email}");
}
WHERE("TUTOR_ID = #{tutorId}");
}
} .toString();
}
}
public interface TutorMapper
{
(type = TutorDynaSqlProvider.class,
method = "updateTutor")
int updateTutor(Tutor tutor);
}
@DeleteProvider
我们可以使用@DeleteProvider 注解创建动态地 DELETE 语句,如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class TutorDynaSqlProvider
{
public String deleteTutor(int tutorId)
{
return new SQL()
{
{
DELETE_FROM("TUTORS");
WHERE("TUTOR_ID = #{tutorId}");
}
} .toString();
}
}
public interface TutorMapper
{
(type = TutorDynaSqlProvider.class,
method = "deleteTutor")
int deleteTutor(int tutorId);
}
与Spring集成
MyBatis-Spring 是 MyBatis 框架的子模块,用来提供与当前流行的依赖注入框架 Spring 的无缝集成。
Spring 框 架 是 一 个 基 于 依 赖 注 入 ( Dependency Injection ) 和 面 向 切 面 编 程 (Aspect Oriented Programming,AOP)的 Java 框架,鼓励使用基于 POJO 的编程模型。另外,Spring 提供了声明式和编程式的事务管理能力,可以很大程度上简化应用程序的数据访问层(data access layer)的实现。 在本章中,我们将看到在基于 Spring的应用程序中使用 MyBatis 并且使用 Spring 的基于注解的事务管理机制。
本章将包含以下话题:
- 在 Spring 应用程序中配置 MyBatis
- 安装
- 配置 MyBatis Beans
- 使用 SqlSession
- 使用映射器
- 使用 Spring 进行事务管理
在 Spring 应用程序中配置 MyBatis
本节将讨论如何在基于 Spring 的应用程序中安装和配置 MyBatis
安装
如果你正在使用 Maven 构建工具,你可以配置 MyBatis 的 spring 依赖如下: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<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>3.1.3.RELEASE</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>3.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.1.3.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.6.8</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
如果你没有使用 Maven,你可以从 http://code.google.com/p/mybatis/ 上下载 mybatis-spring-1.2.0-
boundle.zip。 将其加入,将 mybatis-1.2.0.jar 包添加到 classpath 中。
你可以从 http://www.springsource.org/download/community/ 上下载 Spring 框架包 spring-framework-3.1.3.RELEASE.zip。 将其内所有 jar 包添加到 classpath 中。
如果你只使用 MyBatis 而没有使用 Spring,在每一个方法中,我们需要手动创建 SqlSessionFactory 对象,并且从 SqlSessionFactory 对象中创建 SqlSession。 而且我们还要负责提交或者回滚事务、 关闭 SqlSession 对象。
通过使用 MyBatis-Spring 模块,我们可以在 Spring 的应用上下文 ApplicationContext 中配置 MyBatis
Beans,Spring 会负责实例化 SqlSessionFactory 对象以及创建 SqlSession 对象,并将其注入到 DAO 或者 Service类中。 并且,你可以使用 Spring 的基于注解的事务管理功能,不用自己在数据访问层中书写事务处理代码了。
配置 MyBatis Beans
为了让 Spring 来实例化 MyBatis 组件如 SqlSessionFactory、 SqlSession、以及映射器 Mapper 对象,我们需要在 Spring 的 bean 配置文件中配置它们,假设在 applicationContext.xml 中,配配置如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<beans>
<bean id="dataSource"
class="org.springframework.jdbc.datasource. DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/elearning" />
<property name="username" value="root" />
<property name="password" value="admin" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="typeAliases"
value="com.mybatis3.domain.Student, com.mybatis3.domain.Tutor" />
<property name="typeAliasesPackage" value="com.mybatis3.domain" />
<property name="typeHandlers" value="com.mybatis3.typehandlers.PhoneTypeHandler" />
<property name="typeHandlersPackage" value="com.mybatis3.typehandlers" />
<property name="mapperLocations" value="classpath*:com/mybatis3/**/*.xml" />
<property name="configLocation" value="WEB-INF/mybatisconfig.xml" />
</bean>
</beans>
使用上述的 bean 定义,Spring 会使用如下配置属性创建一个 SqlSessionFactory 对象:
- dataSource:它引用了 dataSource bean
- typeAliases:它指定了一系列的完全限定名的类名列表,用逗号隔开,这些别名将通过默认的别名规则创建
(将首字母小写的非无完全限定类名作为别名)。 - typeAliasesPackage:它指定了一系列包名列表,用逗号隔开,包内含有需要创建别名的 JavaBeans。
- typeHandlers:它指定了一系列的类型处理器类的完全限定名的类名列表,用逗号隔开。
- typeHandlersPackage: 它指定了一系列包名列表,用逗号隔开,包内含有需要被注册的类型处理器类。
- mapperLocations:它指定了 SQL 映射器 Mapper XML 配置文件的位置
- configLocation:它指定了 MyBatisSqlSessionFactory 配置文件所在的位置。
使用 SqlSession
一旦 SqlSessionFactory bean 被配置,我们需要配置 SqlSessionTemplate bean,SqlSessionTemplate bean是一个线程安全的 Spring bean,我们可以从中获取到线程安全的 SqlSession 对象。由于 SqlSessionTemplate 提供线程安全的 SqlSession 对象,你可以在多个 Spring bean 实体对象中共享 SqlSessionTemplate 对象。 从概念上看,SqlSessionTemplate 和 Spring 的 DAO 模块中的 JdbcTemplate 非常相似。1
2
3<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
现在我肯可以将 SqlSession bean 实体对象注射到任意的 Spring bean 实体中,然后使用 SqlSession 对象调用SQL 映射语句。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class StudentDaoImpl implements StudentDao
{
private SqlSession sqlSession;
public void setSqlSession(SqlSession session)
{
his.sqlSession = session;
}
public void createStudent(Student student)
{
StudentMapper mapper =
this.sqlSession.getMapper(StudentMapper.class);
mapper.insertStudent(student);
}
}
如果你正在使用基于 XML 来配置 Spring beans,你可以将 SqlSession bean 实体对象注射到 StudenDaoImplbean 实体对象中,如下:1
2
3<bean id="studentDao" class="com.mybatis3.dao.StudentDaoImpl">
<property name="sqlSession" ref="sqlSession" />
</bean>
如果你使用基于注解的方式配置 Spring beans,你如下将 SqlSession bean 实体对象注入到 StudentDaoImpl bean 实体对象中:
1 |
|
还有另外一种注入 Sqlsession 对象的方法,即,通过拓展继承 SqlSessionDaoSupport。 这种方式让我们可以在执行映射语句时,加入任何自定义的逻辑。
1 | public class StudentMapperImpl extends SqlSessionDaoSupport implements |
1 | <bean id="studentMapper" class="com.mybatis3.dao.StudentMapperImpl"> |
在以上的这些方式中,我们注入了 SqlSession 对象,获取 Mapper 实例,然后执行映射语句。这里 Spring 会为我们提供一个线程安全的 SqlSession 对象,以及当方法结束后关闭 SqlSession 对象。
然而,MyBatis-Spring 模块提供了更好的方式,我们可以不通过 SqlSession 获取映射器 Mapper,直接注射 Sql映射器 Mapper bean。我们下节将讨论它。
使用映射器
我们可以使用 MapperFactoryBean 将映射器 Mapper 接口配置成 Spring bean 实体。 如下所示:1
2
3
4
5
6public interface StudentMapper
{
("select stud_id as studId, name, email, phone from
students where stud_id=#{id}")
Student findStudentById(Integer id);
}
1 | <bean id="studentMapper" class="org.mybatis.spring.mapper. MapperFactoryBean"> |
现在 StudentMapper bean 实体对象可以被注入到任意的 Spring bean 实体对象中,并调用映射语句方法,如下所示:
1 | public class StudentService |
1 | <bean id="studentService" class="com.mybatis3.services. StudentService"> |
分别配置每一个映射器 Mapper 接口是一个非常单调的过程。我们可以使用 MapperScannerConfigurer 来扫描包(package)中的映射器 Mapper 接口,并自动地注册。
1 | <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> |
如果映射器 Mapper 接口在不同的包(package)中,你可以为 basePackage 属性指定一个以逗号分隔的包名列表。
MyBatis-Spring-1.2.0 介绍了两种新的扫描映射器 Mapper 接口的方法:
- 使用mybatis:scan/元素
- 使用@MapperScan 注解(需 Spring3.1+版本)
<mybatis:scan />
<mybatis:scan>元素将在特定的以逗号分隔的包名列表中搜索映射器 Mapper 接口。 使用这个新的 MyBatisSpring 名空间你需要添加以下的 schema 声明:1
2
3
4
5
6
7
8
9
10<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://mybatis.org/schema/mybatis-spring
http://mybatis.org/schema/mybatis-spring.xsd">
<mybatis:scan base-package="com.mybatis3.mappers" />
</beans>
mybatis:scan元素提供了下列的属性来自定义扫描过程:
- annotation: 扫描器将注册所有的在 base-package 包内并且匹配指定注解的映射器 Mapper 接口。
- factory-ref: 当 Spring 上 下 文 中 有 多 个 SqlSessionFactory 实 例 时 , 需 要 指 定 某 一 特 定 的SqlSessionFactory 来创建映射器 Mapper 接口。正常情况下,只有应用程序中有一个以上的数据源
才会使用。 - marker-interface: 扫描器将注册在 base-package 包中的并且继承了特定的接口类的映射器 Mapper 接
口 - template-ref: 当 Spring 上下文中有多个 SqlSessionTemplate 实例时,需要指定某一特定的
SqlSessionTemplate 来创建映射器 Mapper 接口。 正常情况下,只有应用程序中有一个以上的数据源
才会使用。 - name-generator:BeannameGenerator 类的完全限定类名,用来命名检测到的组件。
MapperScan
Spring 框架 3.x+版本支持使用@Configuration 和@Bean 注解来提供基于 Java 的配置。如果你倾向于使用基于Java 的配置,你可以使用@MapperScan 注解来扫描映射器 Mapper 接口。 @MapperScan 和mybatis:scan/工作方式相同,并且也提供了对应的自定义选项。
1 |
|
@MapperScan 注解有以下属性供自定义扫描过程使用:
- annotationClass: 扫描器将注册所有的在 base-package 包内并且匹配指定注解的映射器 Mapper 接口。
- markerInterface: 扫描器将注册在 base-package 包中的并且继承了特定的接口类的映射器 Mapper 接口
- sqlSessionFactoryRef:当 Spring 上 下 文 中 有 一 个以 上 的 SqlSesssionFactory 时 , 用 来 指 定 特 定SqlSessionFactory
- sqlSessionTemplateRef: 当 Spring 上下文中有一个以上的 sqlSessionTemplate 时,用来指定特定
sqlSessionTemplate - nameGenerator:BeanNameGenerator 类用来命名在 Spring 容器内检测到的组件。
- basePackageClasses:basePackages()的类型安全的替代品。 包内的每一个类都会被扫描。
- basePackages:扫描器扫描的基包,扫描器会扫描内部的 Mapper 接口。 注意包内的至少有一个方法声明的才会被
注册。 具体类将会被忽略。与注入 Sqlsession 相比,更推荐使用注入 Mapper,因为它摆脱了对MyBatis API 的依赖。
使用Spring进行事务管理
只使用 MyBatis,你需要写事务控制相关代码,如提交或者回退数据库操作。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public Student createStudent(Student student)
{
SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession();
try
{
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
mapper.insertAddress(student.getAddress());
mapper.insertStudent(student);
sqlSession.commit();
return student;
}
catch (Exception e)
{
sqlSession.rollback();
throw new RuntimeException(e);
}
finally
{
sqlSession.close();
}
}
我们可以使用 Spring 的基于注解的事务处理机制来避免书写上述的每个方法中控制事务的冗余代码。
为了能使用 Spring 的事务管理功能,我们需要在 Spring 应用上下文中配置 TransactionManager bean 实体对象:
1 | <bean id="transactionManager" |
事务管理器引用的 dataSource 和 SqlSessionFactory bean 使用的 dataSource 相同。
在 Spring 中使用基于注解的事务管理特性,如下:1
<tx:annotation-driven transaction-manager="transactionManager"/>
现在你可以在 Spring service bean 上使用@Transactional 注解,表示在此 service 中的每一个方法都应该在一个事务中运行。 如果方法成功运行完毕,Spring 会提交操作。如果有运行期异常发生,则会执行回滚操作。 另外,Spring 会将 MyBatis 的异常转换成合适的 DataAccessExceptions,这样会为特定错误上提供额外的信息。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17Service
public class StudentService
{
private StudentMapper studentMapper;
public Student createStudent(Student student)
{
studentMapper.insertAddress(student.getAddress());
if(student.getName().equalsIgnoreCase(""))
{
throw new RuntimeException("Student name should not be empty.");
}
studentMapper.insertStudent(student);
return student;
}
}
下面是一个 Spring 的 applicationContext.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<beans>
<context:annotation-config />
<context:component-scan base-package="com.mybatis3" />
<context:property-placeholder location="classpath:application.properties" />
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.mybatis3.mappers" />
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="typeAliases"
value="com.mybatis3.domain.Student,com.mybatis3.domain.Tutor" />
<property name="typeAliasesPackage" value="com.mybatis3.domain" />
<property name="typeHandlers" value="com.mybatis3.typehandlers.PhoneTypeHandler" />
<property name="typeHandlersPackage" value="com.mybatis3.typehandlers" />
<property name="mapperLocations" value="classpath*:com/mybatis3/**/*.xml" />
</bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
</beans>
现在让我们写一个独立的测试客户端来测试 StudentService,如下: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(SpringJUnit4ClassRunner.class)
(locations = "classpath:applicationContext.xml"
)
public class StudentServiceTest
{
private StudentService studentService;
public void testCreateStudent()
{
Address address = new Address(0, "Quaker Ridge
Rd.", "Bethel", "Brooklyn", "06801", "USA");
Student stud = new Student();
long ts = System.currentTimeMillis();
stud.setName("stud_" + ts);
stud.setEmail("stud_" + ts + "@gmail.com");
stud.setAddress(address);
Student student = studentService.createStudent(stud);
assertNotNull(student);
assertEquals("stud_" + ts, student.getName());
assertEquals("stud_" + ts + "@gmail.com", student.getEmail());
System.err.println("CreatedStudent: " + student);
}
(expected = DataAccessException.class)
public void testCreateStudentForException()
{
Address address = new Address(0, "Quaker Ridge
Rd.", "Bethel", "Brooklyn", "06801", "USA");
Student stud = new Student();
long ts = System.currentTimeMillis();
stud.setName("Timothy");
stud.setEmail("stud_" + ts + "@gmail.com");
stud.setAddress(address);
studentService.createStudent(stud);
fail("You should not reach here");
}
}
这里在 testCreateStudent()方法中,我们为 Address 和 Student 赋上了合适的数据,所以 Address 和 Student会被分别插入到表 ADDRESSES 和 STUDENTS 中。在 testCreateStudentForException()方法我们设置了名字为Timothy,该名称在数据库中已经存在了,所以当你尝试将此 student 记录插入到数据库中,MySQL 会抛出一个 UNIQUEKEY 冲突的异常,Spring 会将此异常转换成 DataAccessException 异常,并且将插入 ADDRESSES 表中的数据回滚(rollback)掉。
总结
在本章中我们学习了怎样将 MyBatis 与 Spring 框架集成。我们还学习了怎样安装 Spring 类库并且在 Spring 的应用上下文 ApplicationContext 上注册 MyBatis bean 实体对象。我们还看到怎样配置和注入 SqlSession 和 Mapper bean 实体对象以及调用映射语句。我们还学习了利用 Spring 基于注解的事务处理机制来使用 MyBatis。
你已经读完本书,祝贺你!现在,你应该知道怎样高效地使用 MyBatis 与数据库工作。 你学会了怎样发挥你的 Java 和 SQL 技巧的优势使 MyBatis 更富有成效。你知道了怎样以更清晰的方式使用 MyBatis 写出数据持久化代码,不用管被 MyBatis 框架处理的所有底层细节。 另外,你学会了怎样在最流行的依赖注入框架-Spring 中使用 MyBatis。
MyBatis 框架非常易于使用,但它提供了强大的特性,因此它对于基于 Java 的项目而言,是一个非常好的数据库持久化解决方案。 MyBatis 也提供了一些工具如 MyBatis Generator(http://www.mybatis.org/generator/),可以被用来从已经存在的数据库 schema 中,产生持久化代码如数据库实体(database entities),映射器 Mapper 接口,MapperXML 配置文件,使 MyBatis 入门非常方便。 另外,MyBatis 还有它的姊妹项目如 MyBatis.NET 和 MyBatisScala,分别为.NET 和 Scala 编程语言提供了一样强大的特性。
MyBatis 随着每一个版本的发布,增加了一些特性,正变得越来越好。 想了解更多的新特性,你可以访问 MyBatis官方网站 https://code.google.com/p/mybatis/ .订阅 MyBatis 的使用者邮件列表是一个不错的想法。