Mybatis 框架
Mybatis 概述
MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。
MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。
MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
Mybatis 的特点:
- 简单易学:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个 jar 文件 + 配置几个 sql 映射文件。易于学习,易于使用。通过文档和源代码,可以比较完全的掌握它的设计思路和实现。``
- 灵活:mybatis 不会对应用程序或者数据库的现有设计强加任何影响。sql 写在 xml 里,便于统一管理和优化。通过 sql 语句可以满足操作数据库的所有需求。
- 解除 sql 与程序代码的耦合:通过提供 DAO 层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。sql 和代码的分离,提高了可维护性。
- 提供映射标签,支持对象与数据库的 orm 字段关系映射。
- 提供对象关系映射标签,支持对象关系组建维护。
- 提供 xml 标签,支持编写动态 sql。
Mybatis 运行原理
引入 Mybatis
将 mybatis-x.x.x.jar 文件置于类路径(classpath)中即可
另一种方法是通过 Maven 引入普通的 Mybatis 版本
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>{version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>{version}</version>
</dependency>
XML 配置
Mybatis 使用 XML 文件配置全局配置,MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。配置文档的顶层结构如下(加粗的为常用配置):
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
XML 配置文件示例(mybatis-config.xml):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 属性配置 -->
<properties resource="application.yml">
</properties>
<!-- mybatis 设置 -->
<settings>
<!-- 指定日志框架 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<typeAlias alias="User" type="tech.alexchen.mybatis.entity.SysUser"/>
</typeAliases>
<!-- 类型处理器 -->
<typeHandlers>
<typeHandler handler="tech.alexchen.mybatis.config.StringTypeHandler"/>
</typeHandlers>
<!-- 对象工厂 -->
<objectFactory type="tech.alexchen.mybatis.config.SysUserObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
<!-- 插件 -->
<plugins>
<plugin interceptor="tech.alexchen.mybatis.config.MyPlugin">
<property name="someProperty" value="100"/>
</plugin>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="param1" value="value1"/>
</plugin>
</plugins>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<!-- 跳过连接关闭时自动提交事务 -->
<property name="skipSetAutoCommitOnClose" value="false"/>
</transactionManager>
<!-- 如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载: -->
<!-- 1. 首先读取在 properties 元素体内指定的属性。 -->
<!-- 2. 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。 -->
<!-- 3. 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。 -->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!-- mapper文件 -->
<mappers>
<!-- 使用相对于类路径的资源引用 -->
<mapper resource="mapper/SysUserMapper.xml"/>
<!-- 使用映射器接口实现类的完全限定类名 -->
<!-- <mapper class="tech.alexchen.mybatis.mapper.SysUserMapper"/> -->
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<!-- <package name="tech.alexchen.mybatis.mapper"/> -->
</mappers>
</configuration>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 属性配置 -->
<properties resource="application.yml">
</properties>
<!-- mybatis 设置 -->
<settings>
<!-- 指定日志框架 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<typeAlias alias="User" type="tech.alexchen.mybatis.entity.SysUser"/>
</typeAliases>
<!-- 类型处理器 -->
<typeHandlers>
<typeHandler handler="tech.alexchen.mybatis.config.StringTypeHandler"/>
</typeHandlers>
<!-- 对象工厂 -->
<objectFactory type="tech.alexchen.mybatis.config.SysUserObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
<!-- 插件 -->
<plugins>
<plugin interceptor="tech.alexchen.mybatis.config.MyPlugin">
<property name="someProperty" value="100"/>
</plugin>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="param1" value="value1"/>
</plugin>
</plugins>
<!-- 环境配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<!-- 跳过连接关闭时自动提交事务 -->
<property name="skipSetAutoCommitOnClose" value="false"/>
</transactionManager>
<!-- 如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载: -->
<!-- 1. 首先读取在 properties 元素体内指定的属性。 -->
<!-- 2. 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。 -->
<!-- 3. 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。 -->
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!-- mapper文件 -->
<mappers>
<!-- 使用相对于类路径的资源引用 -->
<mapper resource="mapper/SysUserMapper.xml"/>
<!-- 使用映射器接口实现类的完全限定类名 -->
<!-- <mapper class="tech.alexchen.mybatis.mapper.SysUserMapper"/> -->
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<!-- <package name="tech.alexchen.mybatis.mapper"/> -->
</mappers>
</configuration>
注意:这些标签的必须按照要求的顺序,否则会提示错误!
属性(properties)
<properties>
标签可以通过 resources
属性从 Java 属性文件中引入属性配置,也可以在子标签 <property>
中设置属性,属性可以通过 ${propertyName}
的方式使用。设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值。
示例:
<properties resource="application.yml">
<property name="property" value="propertyValue"/>
</properties>
<properties resource="application.yml">
<property name="property" value="propertyValue"/>
</properties>
设置(settings)
settings
中配置 Mybatis 运行时的全局配置,这里配置会通过org.apache.ibatis.session.Configuration
类的实例存储,在 Mybatis 的整个流程中被使用。具体配置见官方文档 设置(settings)。
示例:
<settings>
<!-- 指定日志框架 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<settings>
<!-- 指定日志框架 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
类型别名(typeAliases)
类型别名可为 Java 类型设置一个缩写名字。它仅用于 XML 配置,意在降低冗余的全限定类名书写。
示例:
<typeAliases>
<typeAlias alias="User" type="tech.alexchen.mybatis.entity.SysUser"/>
</typeAliases>
<typeAliases>
<typeAlias alias="User" type="tech.alexchen.mybatis.entity.SysUser"/>
</typeAliases>
在配置 XML 映射文件时,可以使用 User
替代其全限定类名。
环境配置(environments)
MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中,现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。
不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
示例:
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
每一个
<environment>
子标签表示一个数据库环境,其id
属性是其标识,可以在<environments>
标签中通过default
属性指定默认的数据库,这样,Mybatis 在进行数据库连接时,如果没有通过参数指定特定的数据库,则会使用该默认数据库信息进行连接。<transactionManager>
可以配置使用的事物管理,在 MyBatis 中有两种类型的事务管理器:JDBC
和MANAGED
,在使用 Spring 时无需配置,因为 Spring 会覆盖它。JDBC – 这个配置直接使用了 JDBC 的提交和回滚功能,它依赖从数据源获得的连接来管理事务作用域;默认情况下,它在关闭连接时启用自动提交,可以通过设置关闭它:
xml<transactionManager type="JDBC"> <property name="skipSetAutoCommitOnClose" value="true"/> </transactionManager>
<transactionManager type="JDBC"> <property name="skipSetAutoCommitOnClose" value="true"/> </transactionManager>
MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期。默认情况下它会关闭连接,可以将 closeConnection 属性设置为 false 来阻止默认的关闭行为:
xml<transactionManager type="MANAGED"> <property name="closeConnection" value="false"/> </transactionManager>
<transactionManager type="MANAGED"> <property name="closeConnection" value="false"/> </transactionManager>
<dataSource>
标签用来配置数据库连接信息,它有一个type
属性用来配置数据源类型,支持三种数据源类型:- UNPOOLED:无数据池,这个数据源的实现会每次请求时打开和关闭连接。
- POOLED:使用数据库连接池,这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。这种处理方式很流行,能使并发 Web 应用快速响应请求。
- JNDI :这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
映射器(mappers)
<mappers>
是非常重要的配置,它用来配置 MyBatis 去哪里找映射文件,支持四种配置方式:
使用相对于类路径的资源引用
xml<mappers> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> <mapper resource="org/mybatis/builder/BlogMapper.xml"/> <mapper resource="org/mybatis/builder/PostMapper.xml"/> </mappers>
<mappers> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> <mapper resource="org/mybatis/builder/BlogMapper.xml"/> <mapper resource="org/mybatis/builder/PostMapper.xml"/> </mappers>
使用完全限定资源定位符(URL)
xml<mappers> <mapper url="file:///var/mappers/AuthorMapper.xml"/> <mapper url="file:///var/mappers/BlogMapper.xml"/> <mapper url="file:///var/mappers/PostMapper.xml"/> </mappers>
<mappers> <mapper url="file:///var/mappers/AuthorMapper.xml"/> <mapper url="file:///var/mappers/BlogMapper.xml"/> <mapper url="file:///var/mappers/PostMapper.xml"/> </mappers>
使用映射器接口实现类的完全限定类名
xml<mappers> <mapper class="org.mybatis.builder.AuthorMapper"/> <mapper class="org.mybatis.builder.BlogMapper"/> <mapper class="org.mybatis.builder.PostMapper"/> </mappers>
<mappers> <mapper class="org.mybatis.builder.AuthorMapper"/> <mapper class="org.mybatis.builder.BlogMapper"/> <mapper class="org.mybatis.builder.PostMapper"/> </mappers>
将包内的映射器接口实现全部注册为映射器
xml<mappers> <package name="org.mybatis.builder"/> </mappers>
<mappers> <package name="org.mybatis.builder"/> </mappers>
XML 映射文件
XML 映射文件用来编写数据库 SQL 语句,以及进行结构集与 Java 对象的映射。
SQL 映射文件只有很少的几个顶级元素(按照应被定义的顺序列出):
cache
– 该命名空间的缓存配置。cache-ref
– 引用其它命名空间的缓存配置。resultMap
– 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。sql
– 可被其它语句引用的可重用语句块。insert
– 映射插入语句。update
– 映射更新语句。delete
– 映射删除语句。select
– 映射查询语句。
示例(SysUserMapper.xml):
<?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="tech.alexchen.mybatis.mapper.SysUserMapper">
<resultMap id="userMap" type="User">
<!-- column 为数据库字段名,property 为实体类属性名 -->
<result column="user_id" property="userId"/>
<result column="username" property="username"/>
<result column="phone" property="phone"/>
<result column="tenant_id" property="tenantId"/>
</resultMap>
</mapper>
<?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="tech.alexchen.mybatis.mapper.SysUserMapper">
<resultMap id="userMap" type="User">
<!-- column 为数据库字段名,property 为实体类属性名 -->
<result column="user_id" property="userId"/>
<result column="username" property="username"/>
<result column="phone" property="phone"/>
<result column="tenant_id" property="tenantId"/>
</resultMap>
</mapper>
namespace
命名空间用来绑定 Mapper 接口(定义数据访问方法的接口),从项目分层的角度来讲,SysUserMapper.xml
可以认为是 SysUserMapper 的实现,它定义了接口方法的具体实现。
命名空间应该是全局唯一的。
select
查询语句是 MyBatis 中最常用的元素之一——光能把数据存到数据库中价值并不大,还要能重新取出来才有用,多数应用也都是查询比修改要频繁。
示例:
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
这个语句名为 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。
其中,#{id}
表示创建预处理语句,在 JDBC 中,这样的一个参数在 SQL 中会由一个 ?
来标识,并被传递到一个新的预处理语句中,最后使用方法的输入参数 id
进行填充,就像这样:
String selectPerson = "SELECT * FROM PERSON WHERE ID = ?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
String selectPerson = "SELECT * FROM PERSON WHERE ID = ?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
select
有很多的属性:
属性 | 描述 |
---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType | 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。 |
resultType | 期望从这条语句中返回结果的类全限定名或别名。注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。resultType 和 resultMap 之间只能同时使用一个。 |
resultMap | 对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。resultType 和 resultMap 之间只能同时使用一个。 |
flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:false。 |
useCache | 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。 |
fetchSize | 这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。默认值为未设置(unset)(依赖驱动)。 |
statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
resultSetType | FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset)中的一个,默认值为 unset(依赖数据库驱动)。 |
databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。 |
resultOrdered | 这个设置仅针对嵌套结果 select 语句:如果为 true,则假设结果集以正确顺序(排序后)执行映射,当返回新的主结果行时,将不再发生对以前结果行的引用。这样可以减少内存消耗。默认值:false 。 |
resultSets | 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。 |
一个全部属性的 select 标签示例:
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
insert, update 和 delete
Insert, Update, Delete 元素的属性:
属性 | 描述 |
---|---|
id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
parameterType | 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。 |
parameterMap | 用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。 |
flushCache | 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。 |
timeout | 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。 |
statementType | 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。 |
useGeneratedKeys | (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false。 |
keyProperty | (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset )。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
keyColumn | (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。 |
databaseId | 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。 |
在进行数据插入时,常常需要借助数据库自动生成主键,Mybatis 提供了配置使用自动生成 key 的设置 useGeneratedKeys
和指定自动生成主键属性的配置 keyProperty
,例如:
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
<insert id="insertAuthor" useGeneratedKeys="true"
keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
假设这个方法传入一个对象,那么它还会将数据库生成的主键填充到输入对象的指定属性,例如这里的 id
。
另外可以通过配置 selectKey
用来在执行插入前,生产主键值并填充到参数中,具体见官方文档。
SQL
这个元素可以用来定义可重用的 SQL 代码片段,以便在其它语句中使用。参数可以静态地(在加载的时候)确定下来,并且可以在不同的 include 元素中定义不同的参数值。比如:
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
这个 SQL 片段可以在其它语句中使用,例如:
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>
参数
在 CRUD 的四个标签中,都有需要传参的场景,通过 parameterType
可以指定方法的输入参数。参数传入有几种情况:
原始类型或简单数据类型(比如
Integer
和String
)因为没有其它属性,会用它们的值来作为参数,因此可以省略parameterType
配置;参数对一个复杂对象,必须指定
parameterType
;可以在 SQL 中指定参数类型,例如:
xml#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
- 注意:当某个属性可以传输 null 并存储到数据库时,必须指定
jdbcType
; - 当参数为一个
HashMap
时,需要显式指定javaType
来确保正确的类型处理器(TypeHandler
)被使用。
- 注意:当某个属性可以传输 null 并存储到数据库时,必须指定
字符串替换
默认情况下,使用 #{}
参数语法时,MyBatis 会创建 PreparedStatement
参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。
这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中直接插入一个不转义的字符串。比如 ORDER BY 子句,这时候你可以:
ORDER BY ${columnName}
ORDER BY ${columnName}
以 ${}
包围的字符串,不会被预处理为 ?
,而是直接拼接到 SQL 中;这种做法虽然有时很方便,但是会有 SQL 注入到风险,例如:在 where id = ${id}
中,传入 1 or 1=1
,可以将 id=?
这个过滤条件跳过,查询出全部的数据。
因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数。
结果映射
映射 Map
配置 resultType
属性为 map,即可将结果以键值对的形式映射到 map 中:
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
映射 bean
同理,设置 resultType
属性为 bean 的全限定名,即可通过属性名称,将结果映射到 java 对象中:
<select id="selectUsers" resultType="com.someapp.model.User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
<select id="selectUsers" resultType="com.someapp.model.User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
当然,也可以通过 ResultMap
标签,定义复杂的映射关系,例如:
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
设置 resultType
属性为定义出来的 userResultMap
即可将映射到自定义的结构中。
ResultMap
ResultMap
用来定义复杂的结构映射,它的属性如下:
属性 | 描述 |
---|---|
id | 当前命名空间中的一个唯一标识,用于标识一个结果映射。 |
type | 类的完全限定名,或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。 |
autoMapping | 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。 |
它包含了如下子标签:
constructor
- 用于在实例化类时,注入结果到构造方法中,通过 bean 的构造函数设置属性;idArg
- ID 参数;标记出作为 ID 的结果可以帮助提高整体性能,可以理解为构造函数参数中的主键arg
- 将被注入到构造方法的一个普通结果,可以理解为构造函数参数中的普通属性
id
– 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能,可以理解为主键result
– 注入到字段或 JavaBean 属性的普通结果,可以理解为普通字段association
– 一个复杂类型的关联;许多结果将包装成这种类型,可以认为是另一个类collection
– 一个其他类型数据结构的集合,可以理解为Collection<T>
- discriminator
– 使用结果值来决定使用哪个
resultMap`case
– 基于某些值的结果映射;嵌套结果映射 –case
也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
下面按照属性的类别或注入方式,依次介绍:
1. 普通数据类型的 Id 和 result
id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。它们的共同的属性:
属性 | 描述 |
---|---|
property | 映射到列结果的字段或属性。如果 JavaBean 有这个名字的属性(property),会先使用该属性。否则 MyBatis 将会寻找给定名称的字段(field)。无论是哪一种情形,你都可以使用常见的点式分隔形式进行复杂属性导航。比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。 |
column | 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 |
javaType | 一个 Java 类的全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。 |
jdbcType | JDBC 类型,所支持的 JDBC 类型参见这个表格之后的“支持的 JDBC 类型”。只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可以为空值的列指定这个类型。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。 |
2. 构造器映射的 constructor
有些 Java 对象的属性是私有的且不暴露公有的方法,只能通过构造函数初始化,constructor 元素就是为此而生的,示例:
// java 类
public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}
// java 类
public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}
构造函数注入:
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
<arg column="age" javaType="_int"/>
</constructor>
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
<arg column="age" javaType="_int"/>
</constructor>
从版本 3.4.3 开始,可以在指定参数名称的前提下,以任意顺序编写 arg 元素。
constructor
的属性:
属性 | 描述 |
---|---|
column | 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。 |
jdbcType | JDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可能存在空值的列指定这个类型。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。 |
select | 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性中指定的列检索数据,作为参数传递给此 select 语句。具体请参考关联元素。 |
resultMap | 结果映射的 ID,可以将嵌套的结果集映射到一个合适的对象树中。它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet 。这样的 ResultSet 将会将包含重复或部分数据重复的结果集。为了将结果集正确地映射到嵌套的对象树中,MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。想了解更多内容,请参考下面的关联元素。 |
name | 构造方法形参的名字。从 3.4.3 版本开始,通过指定具体的参数名,你可以以任意顺序写入 arg 元素。参看上面的解释。 |
3. 关联其他 Bean
当 Java 类的属性为一个其他的 Bean 时,通过 association
标签,可以在当前映射嵌套另一个 Bean。
关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:
- 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
- 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
首先,先让我们来看看这个元素的属性。你将会发现,和普通的结果映射相比,它只在 select 和 resultMap 属性上有所不同。
属性 | 描述 |
---|---|
property | 映射到列结果的字段或属性。如果用来匹配的 JavaBean 存在给定名字的属性,那么它将会被使用。否则 MyBatis 将会寻找给定名称的字段。无论是哪一种情形,你都可以使用通常的点式分隔形式进行复杂属性导航。比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(关于内置的类型别名,可以参考上面的表格)。如果你映射到一个 JavaBean,MyBatis 通常可以推断类型。然而,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。 |
jdbcType | JDBC 类型,所支持的 JDBC 类型参见这个表格之前的“支持的 JDBC 类型”。只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。这是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 编程,你需要对可能存在空值的列指定这个类型。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是一个类型处理器实现类的完全限定名,或者是类型别名。 |
3.1 关联的嵌套 Select 查询(不推荐)
属性 | 描述 |
---|---|
column | 数据库中的列名,或者是列的别名。一般情况下,这和传递给 resultSet.getString(columnName) 方法的参数一样。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。 |
select | 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。具体请参考下面的例子。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。 |
fetchType | 可选的。有效值为 lazy 和 eager 。指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled ,使用属性的值。 |
示例:
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
<resultMap id="blogResult" type="Blog">
<association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectAuthor" resultType="Author">
SELECT * FROM AUTHOR WHERE ID = #{id}
</select>
我们有两个 select 查询语句:selectBlog
用来加载博客(Blog),selectAuthor
用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor
语句加载它的 author 属性。
其它所有的属性将会被自动加载,只要它们的列名和属性名相匹配。
这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。概括地讲,N+1 查询问题是这样子的:
- 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
- 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。
3.2 关联的嵌套结果映射(推荐)
属性 | 描述 |
---|---|
resultMap | 结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet 。这样的 ResultSet 有部分数据是重复的。为了将结果集正确地映射到嵌套的对象树中,MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。使用嵌套结果映射的一个例子在表格以后。 |
columnPrefix | 当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。详细说明请参考后面的例子。 |
notNullColumn | 默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列中任意一列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。 |
autoMapping | 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。这个属性会覆盖全局的属性 autoMappingBehavior。注意,本属性对外部的结果映射无效,所以不能搭配 select 或 resultMap 元素使用。默认值:未设置(unset)。 |
关联的嵌套 Select 查询在执行时需要多次连接数据库查询,十分影响性能;而关联的嵌套结果映射则通过数据库层面的表连接 SQL 语句,先一次性查询出结果,再通过 resultMap
构造复杂的对象和结果做映射,减少数据库连接次数,提高性能,因此这种方式更加推荐。
示例:
<!-- resultMap -->
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>
<!-- select -->
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>
<!-- resultMap -->
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
<result property="password" column="author_password"/>
<result property="email" column="author_email"/>
<result property="bio" column="author_bio"/>
</resultMap>
<!-- select -->
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
A.id as author_id,
A.username as author_username,
A.password as author_password,
A.email as author_email,
A.bio as author_bio
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>
在上面的例子中,你可以看到,博客(Blog)作者(author)的关联元素委托名为“authorResult”的结果映射来加载作者对象的实例。
非常重要: id
元素在嵌套结果映射中扮演着非常重要的角色。你应该总是指定一个或多个可以唯一标识结果的属性。虽然,即使不指定这个属性,MyBatis 仍然可以工作,但是会产生严重的性能问题。只需要指定可以唯一标识结果的最少属性。显然,你可以选择主键(复合主键也可以)。
4. 关联的多结果集(ResultSet)
涉及存储过程,不常用,见官方文档。
5. 映射集合
collection
标签可以表示,该属性映射一个集合的元素,通过 ofType
指定集合中的元素的类型,元素内部属性和 resultMap
一样。示例:
<collection property="posts" ofType="domain.blog.Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
<collection property="posts" ofType="domain.blog.Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
集合的映射同上述 Java Bean 的映射基本一样,具体可见 官方文档。
6. 鉴别器
鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句。一个鉴别器的定义需要指定 column 和 javaType 属性。column 指定了 MyBatis 查询被比较值的地方。例如:
<resultMap id="vehicleResult" type="Vehicle">
<id property="id" column="id" />
<result property="vin" column="vin"/>
<result property="year" column="year"/>
<result property="make" column="make"/>
<result property="model" column="model"/>
<result property="color" column="color"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultMap="carResult"/>
<case value="2" resultMap="truckResult"/>
<case value="3" resultMap="vanResult"/>
<case value="4" resultMap="suvResult"/>
</discriminator>
</resultMap>
<resultMap id="vehicleResult" type="Vehicle">
<id property="id" column="id" />
<result property="vin" column="vin"/>
<result property="year" column="year"/>
<result property="make" column="make"/>
<result property="model" column="model"/>
<result property="color" column="color"/>
<discriminator javaType="int" column="vehicle_type">
<case value="1" resultMap="carResult"/>
<case value="2" resultMap="truckResult"/>
<case value="3" resultMap="vanResult"/>
<case value="4" resultMap="suvResult"/>
</discriminator>
</resultMap>
在这个示例中,MyBatis 会从结果集中得到每条记录,然后比较它的 vehicle type 值。如果它匹配任意一个鉴别器的 case,就会使用这个 case 指定的结果映射。这个过程是互斥的,也就是说,剩余的结果映射将被忽略。
自动映射
正如你在前面一节看到的,在简单的场景下,MyBatis 可以为你自动映射查询结果。但如果遇到复杂的场景,你需要构建一个结果映射。
自动映射的工作原理:
当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。
通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将
mapUnderscoreToCamelCase
设置为 true。甚至在提供了结果映射后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。在下面的例子中,id 和 userName 列将被自动映射,hashed_password 列将根据配置进行映射。
xml<select id="selectUsers" resultMap="userResultMap"> select user_id as "id", user_name as "userName", hashed_password from some_table where id = #{id} </select> <resultMap id="userResultMap" type="User"> <result property="password" column="hashed_password"/> </resultMap>
<select id="selectUsers" resultMap="userResultMap"> select user_id as "id", user_name as "userName", hashed_password from some_table where id = #{id} </select> <resultMap id="userResultMap" type="User"> <result property="password" column="hashed_password"/> </resultMap>
有三种自动映射等级,默认是 PARTIAL
:
NONE
- 禁用自动映射。仅对手动映射的属性进行映射。PARTIAL
- 对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射FULL
- 自动映射所有属性。
缓存
Mybatis 有两种缓存:
一级缓存:又称为本地缓存,它只在 SqlSession 的生命周期中有效,同一个 SqlSession 对象,在参数和 SQL 完全一样的情况下,只执行一次 SQL 语句(前提缓存没有过期);
二级缓存:Mybatis 的二级缓存是指 mapper 映射文件。二级缓存的作用域是同一个 namespace 下 的 mapper 映射文件内容,多个 SqlSession 共享。可以通过在 Mapper 映射文件中添加
<cache/>
开启二级缓存,此时该 Mapper 中所有方法都支持缓存。在全局设置中,还有一个
cacheEnabled
配置用来全局性地开启或关闭所有映射器配置文件中已配置的任何缓存,默认为 true。
当使用 Mybatis 查询数据时,查询的结果会先缓存到 SqlSession 中,当 SqlSession 关闭或者提交事务时,如果开启了二级缓存,则会将数据缓存到 二级缓存中;当用户调用方法查询数据时,首先会去二级缓存查找,如果有缓存则直接返回;没有再去一级缓存中查询,如果有缓存则直接返回,否则去连接数据库执行 SQL 查询。
注意:缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。
二级缓存的效果如下:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
cache
标签的属性:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有:
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
默认的清除策略是 LRU。
flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。
readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。速度上会慢一些,但是更安全,因此默认值是 false。
二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
Mybatis 还支持自定义缓存,关于自定义缓存或者引用另一个 Mapper 的缓存,可以见官方文档
动态 SQL
在使用 Java 代码编写 SQL 语句时,常常需要判断参数,拼接不同的 SQL 语句,同时还要处理拼接中的空格、引号和格式化;Mybatis 提供了在 XML 中动态拼接 SQL 的标签:
- if
- choose (when, otherwise)
- trim (where, set)
- foreach
if
使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
<select id="findActiveBlogWithTitleLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
</select>
这条语句提供了可选的查找文本功能。如果不传入“title”,那么所有处于“ACTIVE”状态的 BLOG 都会返回;如果传入了“title”参数,那么就会对“title”一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title”的参数值需要包含查找掩码或通配符字符)。
如果希望通过“title”和“author”两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
choose、when、otherwise
有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。
还是上面的例子,但是策略变为:传入了“title”就按“title”查找,传入了“author”就按“author”查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员精选的 Blog)。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose>
<when test="title != null">
AND title like #{title}
</when>
<when test="author != null and author.name != null">
AND author_name like #{author.name}
</when>
<otherwise>
AND featured = 1
</otherwise>
</choose>
</select>
trim、where、set
前面几个例子已经方便地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的“if”示例,这次我们将“state = ‘ACTIVE’”设置成动态条件,看看会发生什么。
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
WHERE
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</select>
如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:
SELECT * FROM BLOG
WHERE
SELECT * FROM BLOG
WHERE
这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’
SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’
这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。
MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG
<where>
<if test="state != null">
state = #{state}
</if>
<if test="title != null">
AND title like #{title}
</if>
<if test="author != null and author.name != null">
AND author_name like #{author.name}
</if>
</where>
</select>
where 元素只会在子元素返回任何内容的情况下才插入“WHERE”子句。而且,若子句的开头为“AND”或“OR”,where 元素也会将它们去除。
如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
<trim prefix="WHERE" prefixOverrides="AND |OR ">
...
</trim>
prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。
用于动态更新语句的类似解决方案叫做 set。set 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
<update id="updateAuthorIfNecessary">
update Author
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id=#{id}
</update>
这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。
或者,你可以通过使用trim元素来达到同样的效果:
<trim prefix="SET" suffixOverrides=",">
...
</trim>
<trim prefix="SET" suffixOverrides=",">
...
</trim>
注意,我们覆盖了后缀值设置,并且自定义了前缀值。
foreach
动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where>
<foreach item="item" index="index" collection="list"
open="ID in (" separator="," close=")" nullable="true">
#{item}
</foreach>
</where>
</select>
foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!
提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
至此,我们已经完成了与 XML 配置及映射文件相关的讨论。下一章将详细探讨 Java API,以便你能充分利用已经创建的映射配置。
script
要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。比如:
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);
@Update({"<script>",
"update Author",
" <set>",
" <if test='username != null'>username=#{username},</if>",
" <if test='password != null'>password=#{password},</if>",
" <if test='email != null'>email=#{email},</if>",
" <if test='bio != null'>bio=#{bio}</if>",
" </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);
bind
bind
元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>