Mybatis之Mapper映射文件
一、映射文件
MyBatis的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的XML文件就显得相对简单。如果拿它跟具有相同功能的JDBC代码进行对比,你会立即发现省掉了将近95%的代码。MyBatis就是针对SQL构建的,并且比普通的方法做的更好。
SQL映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):
cache– 给定命名空间的缓存配置。cache-ref– 其他命名空间缓存配置的引用。resultMap– 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。sql– 可被其他语句引用的可重用语句块。insert– 映射插入语句update– 映射更新语句delete– 映射删除语句select– 映射查询语句
二、主要元素
| 元素名称 | 描述 | 备注 |
|---|---|---|
| mapper | 映射文件的根节点,只有 namescape 一个属性 | namescape 作用如下:
|
| select | 查询语句,最常用、最复杂的元素之一 | 可以自定义参数,返回结果集等 |
| insert | 插入语句 | 执行后返回一个整数,代表插入的条数 |
| update | 更新语句 | 执行后返回一个整数,代表更新的条数 |
| delete | 删除语句 | 执行后返回一个整数,代表删除的条数 |
| parameterMap | 定义参数映射关系 | 即将被删除的元素,不建议使用 |
| sql | 允许定义一部分的 SQL,然后在各个地方引用它 | 例如,一张表列名,我们可以一次定义,在多个 SQL 语句中使用 |
| resultMap | 用来描述数据库结果集与对象的对应关系,它是最复杂、最强大的元素 | 提供映射规则 |
| cache | 配置给定命名空间的缓存 | - |
| cache-ref | 其它命名空间缓存配置的引用 | - |
2.1 mapper
mapper元素是用来定义一个映射器(Mapper)的,它是MyBatis的核心配置之一。一个映射器文件通常包含SQL语句和映射规则,这些映射规则将数据库表中的列与Java对象属性进行关联。通过这种方式,MyBatis可以执行各种数据库操作,比如插入、更新、删除和查询。
一个简单的UserMapper.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="com.test.mapper.UserMapper">
<!-- 定义一个查询所有用户的操作 -->
<select id="selectAllUsers" resultType="com.test.User">
SELECT id, username, email FROM users
</select>
</mapper>
namespace属性指定了Mapper接口的全限定名。
2.2 select
查询语句是MyBatis中最常用的元素之一,光能把数据存到数据库中价值并不大,如果还能重新取出来才有用,多数应用也都是查询比修改要频繁。对每个插入、更新或删除操作,通常对应多个查询操作。这是MyBatis的基本原则之一,也是将焦点和努力放到查询和结果映射的原因。简单查询的select元素是非常简单的。比如:
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
这个语句被称作selectPerson,接受一个int(或Integer)类型的参数,并返回一个HashMap类型的对象,其中的键是列名,值便是结果行中的对应值。
注意参数符号: #{id}
这就告诉MyBatis创建一个预处理语句参数,通过JDBC,这样的一个参数在SQL中会由一个"?"来标识,并被传递到一个新的预处理语句中,就像这样:
// Similar JDBC code, NOT MyBatis…
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);
当然,这需要很多单独的JDBC的代码来提取结果并将它们映射到对象实例中,这就是MyBatis节省你时间的地方。我们需要深入了解参数和结果映射,细节部分我们下面来了解。
select元素有很多属性允许你配置,来决定每条语句的作用细节。
<mapper namespace="com.test.mapper.PersonMapper">
<select id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
</select>
</mapper>
| 属性 | 描述 |
|---|---|
| id | 在命名空间中唯一的标识符,可以被用来引用这条语句。 |
| parameterType | 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为MyBatis可以通过TypeHandler推断出具体传入语句的参数,默认值为unset。 |
| resultType | 从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用resultType或resultMap,但不能同时使用。 |
| resultMap | 外部resultMap的命名引用。结果集的映射是MyBatis最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用resultMap或resultType,但不能同时使用。 |
| 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中的一个,默认值为unset(依赖驱动)。 |
| databaseId | 如果配置了databaseIdProvider,MyBatis会加载所有的不带databaseId或匹配当前databaseId的语句;如果带或者不带的语句都有,则不带的会被忽略。 |
| resultOrdered | 这个设置仅针对嵌套结果select语句适用:如果为true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。 |
| resultSets | 这个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称是逗号分隔的。 |
2.3 insert,update和delete
数据变更语句insert,update和delete的实现非常接近:
<mapper namespace="com.test.mapper.AuthorMapper">
<insert id="insertAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20">
</insert>
<update id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
</update>
<delete id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
</delete>
</mapper>
Insert,Update和Delete的属性
| 属性 | 描述 |
|---|---|
| id | 命名空间中的唯一标识符,可被用来代表这条语句。 |
| parameterType | 将要传入语句的参数的完全限定类名或别名。这个属性是可选的,因为MyBatis可以通过TypeHandler推断出具体传入语句的参数,默认值为unset。 |
| flushCache | 将其设置为true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值: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的语句;如果带或者不带的语句都有,则不带的会被忽略。 |
下面就是insert,update和delete语句的示例:
<mapper namespace="com.test.mapper.AuthorMapper">
<insert id="insertAuthor">
insert into Author (id,username,password,email,bio)
values (#{id},#{username},#{password},#{email},#{bio})
</insert>
<update id="updateAuthor">
update Author
set
username = #{username},
password = #{password},
email = #{email},
bio = #{bio}
where id = #{id}
</update>
<delete id="deleteAuthor">
delete from Author where id = #{id}
</delete>
</mapper>
如前所述,插入语句的配置规则更加丰富,在插入语句里面有一些额外的属性和子元素用来处理主键的生成,而且有多种生成方式。
首先,如果你的数据库支持自动生成主键的字段(比如MySQL和SQL Server),那么你可以设置useGeneratedKeys=”true”,然后再把keyProperty设置到目标属性上就OK了。例如,如果上面的Author表已经对id使用了自动生成的列类型,那么语句可以修改为:
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
insert into Author (username,password,email,bio)
values (#{username},#{password},#{email},#{bio})
</insert>
如果你的数据库还支持多行插入,你也可以传入一个Authors数组或集合,并返回自动生成的主键。
<insert id="insertAuthor" useGeneratedKeys="true" keyProperty="id">
insert into Author (username, password, email, bio) values
<foreach item="item" collection="list" separator=",">
(#{item.username}, #{item.password}, #{item.email}, #{item.bio})
</foreach>
</insert>
在上面的示例中,selectKey元素将会首先运行,Author的id会被设置,然后插入语句会被调用。这给你了一个和数据库中来处理自动生成的主键类似的行为,避免了使Java代码变得复杂。
selectKey元素描述如下:
<selectKey keyProperty="id"
resultType="int"
order="BEFORE"
statementType="PREPARED">
</selectKey>
selectKey的属性
| 属性 | 描述 |
|---|---|
| keyProperty | selectKey语句结果应该被设置的目标属性。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 |
| keyColumn | 匹配属性的返回结果集中的列名称。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。 |
| resultType | 结果的类型。MyBatis通常可以推算出来,但是为了更加确定写上也不会有什么问题。MyBatis允许任何简单类型用作主键的类型,包括字符串。如果希望作用于多个生成的列,则可以使用一个包含期望属性的Object或一个Map。 |
| order | 这可以被设置为BEFORE或AFTER。如果设置为BEFORE,那么它会首先选择主键,设置keyProperty然后执行插入语句。如果设置为AFTER,那么先执行插入语句,然后是selectKey元素 - 这和像Oracle的数据库相似,在插入语句内部可能有嵌入索引调用。 |
| statementType | 与前面相同,MyBatis支持STATEMENT,PREPARED和CALLABLE语句的映射类型,分别代表PreparedStatement和CallableStatement类型。 |
2.4 sql
这个元素可以被用来定义可重用的SQL代码段,可以包含在其他语句中。它可以静态地(在加载阶段)参数化。不同的属性值可以在include实例中有所不同。比如:
<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>
属性值也可以用于include refid属性或include子句中的属性值,例如:
<mapper namespace="com.test.mapper.XXXMapper">
<sql id="someTable">
${prefix}Table
</sql>
<sql id="someInclude">
from
<include refid="${include_target}"/>
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someInclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="someTable"/>
</include>
</select>
</mapper>
三、parameterType
parameterType属性用于指定SQL语句中的参数类型,表示传递给SQL语句的参数对象类型。它通常与映射器接口方法的参数类型相对应。例如,在<insert>、<update>和<select>等元素中,parameterType属性定义了要传递给SQL语句的参数对象的类型。使用#{}占位符时,MyBatis会根据parameterType指定的类型来设置对应的参数值。
比如:
<insert id="insertUser" parameterType="User">
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>
如果User类型的参数对象传递到了语句中,id、username和password属性将会被查找,然后将它们的值传入预处理语句的参数中。这点对于向语句中传参是比较好的而且又简单,不过参数映射的功能远不止于此。首先,像MyBatis的其他部分一样,参数也可以指定一个特殊的数据类型。
#{property,javaType=int,jdbcType=NUMERIC}
像MyBatis的剩余部分一样,javaType通常可以从参数对象中来去确定,前提是只要对象不是一个HashMap。那么javaType应该被确定来保证使用正确类型处理器。NOTE如果null被当作值来传递,对于所有可能为空的列,JDBCType是需要的。你可以自己通过阅读预处理语句的setNull()方法的JavaDocs文档来研究这种情况。为了以后定制类型处理方式,你也可以指定一个特殊的类型处理器类(或别名),比如:
#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}
尽管看起来配置变得越来越繁琐,但实际上是很少去设置它们。对于数值类型,还有一个小数保留位数的设置,来确定小数点后保留的位数。
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}
最后,mode属性允许你指定IN,OUT或INOUT参数。如果参数为OUT或INOUT,参数对象属性的真实值将会被改变,就像你在获取输出参数时所期望的那样。如果mode为OUT(或INOUT),而且jdbcType为CURSOR(也就是Oracle的REFCURSOR),你必须指定一个resultMap来映射结果集到参数类型。要注意这里的javaType属性是可选的,如果左边的空白是jdbcType的CURSOR类型,它会自动地被设置为结果集。
#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}
MyBatis也支持很多高级的数据类型,比如结构体,但是当注册out参数时你必须告诉它语句类型名称。比如(再次提示,在实际中要像这样不能换行):
#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}
尽管所有这些强大的选项很多时候你只简单指定属性名,其他的事情MyBatis会自己去推断,最多你需要为可能为空的列名指定jdbcType。
#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}
字符串替换
默认情况下,使用#{}格式的语法会导致MyBatis创建预处理语句属性并安全地设置值(比如?)。这样做更安全,更迅速,通常也是首选做法,不过有时你只是想直接在SQL语句中插入一个不改变的字符串。比如,像ORDER BY,你可以这样来使用:
ORDER BY ${columnName}
这里MyBatis不会修改或转义字符串。NOTE以这种方式接受从用户输出的内容并提供给语句中不变的字符串是不安全的,会导致潜在的SQL注入攻击,因此要么不允许用户输入这些字段,要么自行转义并检验。
四、resultType
resultType属性用于指定SQL语句的返回结果类型,表示从数据库查询返回的结果对象类型。它通常与映射器接口方法的返回类型相对应。例如,在<select>元素中,resultType属性定义了查询结果的类型。MyBatis会根据resultType指定的类型将查询结果映射为相应的Java对象resultType属性适用于简单的结果映射,对于复杂的结果映射,可以使用resultMap进行更灵活的映射配置。
比如:
<select id="selectUsers" resultType="map">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
这样一个语句简单作用于所有列被自动映射到HashMap的键上,这由resultType属性指定。这在很多情况下是有用的,但是HashMap不能很好描述一个领域模型。那样你的应用程序将会使用JavaBeans或POJOs(Plain Old Java Objects,普通Java对象)来作为领域模型。MyBatis对两者都支持。看看下面这个JavaBean:
@Data
public class User {
private int id;
private String username;
private String hashedPassword;
}
基于JavaBean的规范,上面这个类有3个属性:id,username和hashedPassword。这些在select语句中会精确匹配到列名。这样的一个JavaBean可以被映射到结果集,就像映射到HashMap一样简单。
<select id="selectUsers" resultType="com.someapp.model.User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
要记住类型别名。使用它们你可以不用输入类的全路径。比如:
<mapper namespace="com.test.mapper.XXXMapper">
<!-- In mybatis-config.xml file -->
<typeAlias type="com.someapp.model.User" alias="User"/>
<!-- In SQL Mapping XML file -->
<select id="selectUsers" resultType="User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
</mapper>
这些情况下,MyBatis会在幕后自动创建一个ResultMap,基于属性名来映射列到JavaBean的属性上。如果列名没有精确匹配,可以在列名上使用select字句的别名(一个基本的SQL特性)来匹配标签。比如:
<select id="selectUsers" resultType="User">
select
user_id as id,
user_name as userName,
hashed_password as hashedPassword
from some_table
where id = #{id}
</select>
五、resultMap
resultMap用于自定义复杂的结果映射关系。它可以定义如何将数据库查询结果映射到Java对象中的字段或属性。resultMap元素通常在<select>元素中使用,并配合<result>子元素来定义每个字段或属性的映射关系。通过使用resultMap,可以灵活地进行字段名和属性名之间的映射、关联查询的结果映射等。
相比于简单的resultType,resultMap提供了更细粒度的结果映射配置。
resultMap元素有很多子元素和一个值得讨论的结构。下面是resultMap元素的概念视图
constructor- 类在实例化时,用来注入结果到构造方法中idArg- ID参数;标记结果作为ID可以帮助提高整体效能arg- 注入到构造方法的一个普通结果
id– 一个ID结果;标记结果作为ID可以帮助提高整体效能result– 注入到字段或JavaBean属性的普通结果association– 一个复杂的类型关联;许多结果将包成这种类型- 嵌入结果映射 – 结果映射自身的关联,或者参考一个
collection– 复杂类型的集- 嵌入结果映射 – 结果映射自身的集,或者参考一个
discriminator– 使用结果值来决定使用哪个结果映射case– 基于某些值的结果映射- 嵌入结果映射 – 这种情形结果也映射它本身,因此可以包含很多相同的元素,或者它可以参照一个外部的结果映射。
| 属性 | 描述 |
|---|---|
id |
此名称空间中的唯一标识符,可用于引用此结果映射。 |
type |
一个完全特定的Java类名,或者一个类型别名(参见上表中的内置类型别名列表)。 |
autoMapping |
如果存在,MyBatis将启用或禁用这个ResultMap的自动操作。此属性覆盖全局autoMappingBehavior。默认值:未设置的。 |
最佳实践通常逐步建立结果映射。单元测试的真正帮助在这里。如果你尝试创建一次创建一个向上面示例那样的巨大的结果映射,那么可能会有错误而且很难去控制它来工作。开始简单一些,一步一步的发展。而且要进行单元测试!使用该框架的缺点是它们有时是黑盒(是否可见源代码)。你确定你实现想要的行为的最好选择是编写单元测试。它也可以你帮助得到提交时的错误。
5.1 id & result
id和result都映射一个单独列的值到简单数据类型(字符串,整型,双精度浮点数,日期等)的单独属性或字段。
<resultMap type="" id="BaseResultMap">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
</resultMap>
这两者之间的唯一不同是id表示的结果将是当比较对象实例时用到的标识属性。这帮助来改进整体表现,特别是缓存和嵌入结果映射(也就是联合映射)。
每个都有一些属性:
| 属性 | 描述 |
|---|---|
property |
映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同的JavaBeans的属性,那么就会使用。否则MyBatis将会寻找给定名称property的字段。这两种情形你可以使用通常点式的复杂属性导航。比如,你可以这样映射一些东西:"username",或者映射到一些复杂的东西:"address.street.number"。 |
column |
从数据库中得到的列名,或者是列名的重命名标签。这也是通常和会传递给resultSet.getString(columnName)方法参数中相同的字符串。 |
javaType |
一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列表) 。如果你映射到一个JavaBean,MyBatis通常可以断定类型。 然而,如果你映射到的是HashMap,那么你应该明确地指定javaType来保证所需的行为。 |
jdbcType |
在这个表格之后的所支持的JDBC类型列表中的类型。JDBC类型是仅仅需要对插入,更新和删除操作可能为空的列进行处理。这是JDBC jdbcType的需要,而不是MyBatis的。如果你直接使用JDBC编程,你需要指定这个类型-但仅仅对可能为空的值。 |
typeHandler |
我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。这个属性值是类的完全限定名或者是一个类型处理器的实现,或者是类型别名。 |
为了未来的参考,MyBatis通过包含的jdbcType枚举型,支持下面的JDBC类型。
| bit | float | char | timestamp | other | undefined |
|---|---|---|---|---|---|
| tinyint | real | varchar | binary | blog | nvarchar |
| smallint | double | longvarchar | varbinary | clob | nchar |
| integer | numeric | date | longvarbinary | boolean | nclob |
| bigint | decimal | time | null | cursor | array |
5.2 构造方法
对于大多数数据传输对象(Data Transfer Object,DTO)类型,属性可以起作用,而且像你绝大多数的领域模型,指令也许是你想使用一成不变的类的地方。通常包含引用或查询数据的表很少或基本不变的话对一成不变的类来说是合适的。构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis也支持私有属性和私有JavaBeans属性来达到这个目的,但是一些人更青睐构造方法注入。构造方法元素支持这个。
看看下面这个构造方法:
public class User {
private //...
public User(int id, String username) {
//...
}
//...
}
为了向这个构造方法中注入结果,MyBatis需要通过它的参数的类型来标识构造方法。Java没有自查(反射)参数名的方法。所以当创建一个构造方法元素时,保证参数是按顺序排列的,而且数据类型也是确定的。
<constructor>
<idArg column="id" javaType="int"/>
<arg column="username" javaType="String"/>
</constructor>
剩余的属性和规则和固定的id和result元素是相同的。
| 属性 | 描述 |
|---|---|
column |
来自数据库的类名,或重命名的列标签。这和通常传递给resultSet.getString(columnName)方法的字符串是相同的。 |
javaType |
一个Java类的完全限定名,或一个类型别名(参考上面内建类型别名的列表)。 如果你映射到一个JavaBean,MyBatis通常可以断定类型。然而,如果你映射到的是HashMap,那么你应该明确地指定javaType来保证所需的行为。 |
jdbcType |
在这个表格之前的所支持的JDBC类型列表中的类型。JDBC类型是仅仅需要对插入,更新和删除操作可能为空的列进行处理。这是JDBC的需要,jdbcType而不是MyBatis的。如果你直接使用JDBC编程,你需要指定这个类型-但仅仅对可能为空的值。 |
typeHandler |
我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是类的完全限定名或者是一个类型处理器的实现,或者是类型别名。 |
select |
另一个映射语句的ID,该语句将加载此属性映射所需的复杂类型。从列属性中指定的列检索的值将作为参数传递给目标select语句。更多信息请参见关联元素。 |
resultMap |
这是ResultMap的ID,它可以将此参数的嵌套结果映射到适当的对象图中。这是对另一个select语句调用的替代方法。它允许您将多个表连接到一个结果集中。这样的ResultSet将包含重复的、重复的数据组,需要对这些数据进行分解并正确地映射到嵌套的对象图中。为了实现这一点,MyBatis允许将结果映射“链”在一起,以处理嵌套的结果。 |
5.3 关联
关联元素处理"有一个"类型的关系。比如,在我们的示例中,一个博客有一个用户。关联映射就工作于这种结果之上。你指定了目标属性,来获取值的列,属性的java类型(很多情况下MyBatis可以自己算出来),如果需要的话还有jdbc类型,如果你想覆盖或获取的结果值还需要类型控制器。
<association property="author" column="blog_author_id" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>
关联中不同的是你需要告诉MyBatis如何加载关联。MyBatis在这方面会有两种不同的方式:
- 嵌套查询:通过执行另外一个
SQL映射语句来返回预期的复杂类型。 - 嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集。首先,然让我们来查看这个元素的属性。所有的你都会看到,它和普通的只由
select和resultMap属性的结果映射不同。
| 属性 | 描述 |
|---|---|
property |
映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同的property JavaBeans的属性,那么就会使用。否则MyBatis将会寻找给定名称的字段。这两种情形你可以使用通常点式的复杂属性导航。比如,你可以这样映射一些东西:"username",或者映射到一些复杂的东西:"address.street.number"。 |
javaType |
一个Java类的完全限定名,或一个类型别名(参考上面内建类型别名的列表)。如果你映射到一个JavaBean,MyBatis通常可以断定类型。然而,如javaType果你映射到的是HashMap,那么你应该明确地指定javaType来保证所需的行为。 |
jdbcType |
在这个表格之前的所支持的JDBC类型列表中的类型。JDBC类型是仅仅需要对插入,更新和删除操作可能为空的列进行处理。这是JDBC的需要,jdbcType而不是MyBatis的。如果你直接使用JDBC编程,你需要指定这个类型-但仅仅对可能为空的值。 |
typeHandler |
我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的typeHandler类型处理器。这个属性值是类的完全限定名或者是一个类型处理器的实现,或者是类型别名。 |
5.3.1 关联的嵌套查询
| 属性 | 描述 |
|---|---|
column |
来自数据库的类名,或重命名的列标签。这和通常传递给resultSet.getString(columnName)方法的字符串是相同的。column注意:要处理复合主键,你可以指定多个列名通过 column="{prop1=col1,prop2=col2}"这种语法来传递给嵌套查询语句。这会引起prop1和prop2以参数对象形式来设置给目标嵌套查询语句。 |
select |
另外一个映射语句的ID,可以加载这个属性映射需要的复杂类型。获取的在列属性中指定的列的值将被传递给目标 select语句作为参数。表格后面有一个详细的示例。select注意:要处理复合主键,你可以指定多个列名通过 column="{prop1=col1,prop2=col2}"这种语法来传递给嵌套查询语句。这会引起prop1和prop2以参数对象形式来设置给目标嵌套查询语句。 |
fetchType |
可选的。有效值是lazy的和eager的。 如果存在,它将替代此映射的全局配置参数 lazyLoadingEnabled。 |
示例:
<mapper namespace="com.test.mapper.BlogMapper">
<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>
</mapper>
我们有两个查询语句:一个来加载博客,另外一个来加载作者,而且博客的结果映射描述了"selectAuthor"语句应该被用来加载它的author属性。
其他所有的属性将会被自动加载,假设它们的列和属性名相匹配。
这种方式很简单,但是对于大型数据集合和列表将不会表现很好。问题就是我们熟知的"N+1查询问题"。概括地讲,N+1查询问题可以是这样引起的:
- 你执行了一个单独的SQL语句来获取结果列表(就是"+1")。
- 对返回的每条记录,你执行了一个查询语句来为每个加载细节(就是"N")。
这个问题会导致成百上千的SQL语句被执行。这通常不是期望的。
MyBatis能延迟加载这样的查询就是一个好处,因此你可以分散这些语句同时运行的消耗。然而,如果你加载一个列表,之后迅速迭代来访问嵌套的数据,你会调用所有的延迟加载,这样的行为可能是很糟糕的。
所以还有另外一种方法。
5.3.2 关联的嵌套结果
| 属性 | 描述 |
|---|---|
resultMap |
这是结果映射的ID,可以映射关联的嵌套结果到一个合适的对象图中。这是一种替代方法来调用另外一个查询语句。这允许你联合多个表来合成到resultMap一个单独的结果集。这样的结果集可能包含重复,数据的重复组需要被分解,合理映射到一个嵌套的对象图。为了使它变得容易,MyBatis让你"链接"结果映射,来处理嵌套结果。一个例子会很容易来仿照,这个表格后面也有一个示例。 |
columnPrefix |
在连接多个表时,必须使用列别名以避免结果集中的重复列名。指定columnPrefix允许您将这些列映射到外部resultMap。请参阅本节后面解释的示例。 |
notNullColumn |
默认情况下,只有在映射到子对象属性的至少一个列非空时,才会创建子对象。有了这个属性,您可以通过指定哪些列必须有一个值来改变这种行为,这样MyBatis将只在这些列中的任何一列不为空时创建子对象。可以使用逗号作为分隔符指定多个列名。默认值:未设置的。 |
autoMapping |
如果存在,MyBatis将在将结果映射到此属性时启用或禁用自动映射。此属性覆盖全局autoMappingBehavior。注意,它对外部resultMap没有影响,因此与select或resultMap属性一起使用是没有意义的。默认值:未设置的。 |
在上面你已经看到了一个非常复杂的嵌套关联的示例。下面这个是一个非常简单的示例来说明它如何工作。代替了执行一个分离的语句,我们联合博客表和作者表在一起,就像:
<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>
注意这个联合查询,以及采取保护来确保所有结果被唯一而且清晰的名字来重命名。这使得映射非常简单。现在我们可以映射这个结果:
<mapper namespace="com.test.mapper.BlogMapper">
<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>
</mapper>
在上面的示例中你可以看到博客的作者关联代表着"authorResult"结果映射来加载作者实例。
非常重要:在嵌套据诶过映射中
id元素扮演了非常重要的角色。应应该通常指定一个或多个属性,它们可以用来唯一标识结果。
实际上就是如果你离开她了,但是有一个严重的性能问题时MyBatis仍然可以工作。选择的属性越少越好,它们可以唯一地标识结果。主键就是一个显而易见的选择(尽管是联合主键)。
现在,上面的示例用了外部的结果映射元素来映射关联。这使得Author结果映射可以重用。然而,如果你不需要重用它的话,或者你仅仅引用你所有的结果映射合到一个单独描述的结果映射中。你可以嵌套结果映射。这里给出使用这种方式的相同示例:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author" javaType="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"/>
</association>
</resultMap>
如果博客有一个共同作者呢?select语句如下:
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
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,
CA.id as co_author_id,
CA.username as co_author_username,
CA.password as co_author_password,
CA.email as co_author_email,
CA.bio as co_author_bio
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Author CA on B.co_author_id = CA.id
where B.id = #{id}
</select>
回想一下,Author的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>
由于结果中的列名与resultMap中定义的列不同,因此您需要指定columnPrefix来重新使用resultMap,以便映射合Author的结果。
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<association property="author"
resultMap="authorResult" />
<association property="coAuthor"
resultMap="authorResult"
columnPrefix="co_" />
</resultMap>
上面你已经看到了如何处理"有一个"类型关联。但是"有很多个"是怎样的?下面这个部分就是来讨论这个主题的。
5.4 集合
集合元素的作用几乎和关联是相同的。实际上,它们也很相似,文档的异同是多余的。所以我们更多关注于它们的不同。我们来继续上面的示例,一个博客只有一个作者。但是博客有很多文章。在博客类中,这可以由下面这样的写法来表示:
<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>
private List posts;
要映射嵌套结果集合到List中,我们使用集合元素。就像关联元素一样,我们可以从连接中使用嵌套查询,或者嵌套结果。
5.4.1 集合的嵌套查询
首先,让我们看看使用嵌套查询来为博客加载文章。
<mapper namespace="com.test.mapper.BlogMapper">
<resultMap id="blogResult" type="Blog">
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>
<select id="selectBlog" resultMap="blogResult">
SELECT * FROM BLOG WHERE ID = #{id}
</select>
<select id="selectPostsForBlog" resultType="Blog">
SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>
</mapper>
这里你应该注意很多东西,但大部分代码和上面的关联元素是非常相似的。首先,你应该注意我们使用的是集合元素。然后要注意那个新的"ofType"属性。这个属性用来区分JavaBean(或字段)属性类型和集合包含的类型来说是很重要的。所以你可以读出下面这个映射:
<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
读作:"在Post类型的ArrayList中的posts的集合。"
javaType属性是不需要的,因为MyBatis在很多情况下会为你算出来。所以你可以缩短写法:
<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>
5.4.2 集合的嵌套结果
至此,你可以猜测集合的嵌套结果是如何来工作的,因为它和关联完全相同,除了它应用了一个"ofType"属性
<select id="selectBlog" resultMap="blogResult">
select
B.id as blog_id,
B.title as blog_title,
B.author_id as blog_author_id,
P.id as post_id,
P.subject as post_subject,
P.body as post_body,
from Blog B
left outer join Post P on B.id = P.blog_id
where B.id = #{id}
</select>
我们又一次联合了博客表和文章表,而且关注于保证特性,结果列标签的简单映射。现在用文章映射集合映射博客,可以简单写为:
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<result property="body" column="post_body"/>
</collection>
</resultMap>
同样,要记得id元素的重要性,如果你不记得了,请阅读上面的关联部分。同样,如果你引用更长的形式允许你的结果映射的更多重用,你可以使用下面这个替代的映射:
<mapper namespace="com.test.mapper.BlogMapper">
<resultMap id="blogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title"/>
<collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>
<resultMap id="blogPostResult" type="Post">
<id property="id" column="id"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
</resultMap>
</mapper>
注意:这个对你所映射的内容没有深度,广度或关联和集合相联合的限制。当映射它们时你应该在大脑中保留它们的表现。你的应用在找到最佳方法前要一直进行的单元测试和性能测试。好在myBatis让你后来可以改变想法,而不对你的代码造成很小(或任何)影响。
高级关联和集合映射是一个深度的主题。文档只能给你介绍到这了。加上一点联系,你会很快清楚它们的用法。
5.5 鉴别器
有时一个单独的数据库查询也许返回很多不同(但是希望有些关联)数据类型的结果集。鉴别器元素就是被设计来处理这个情况的,还有包括类的继承层次结构。鉴别器非常容易理解,因为它的表现很像Java语言中的switch语句。
定义鉴别器指定了column和javaType属性。列是MyBatis查找比较值的地方。JavaType是需要被用来保证等价测试的合适类型(尽管字符串在很多情形下都会有用)。比如:
<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类型的值。如果它匹配任何一个鉴别器的实例,那么就使用这个实例指定的结果映射。换句话说,这样做完全是剩余的结果映射被忽略(除非它被扩展,这在第二个示例中讨论)。如果没有任何一个实例相匹配,那么MyBatis仅仅使用鉴别器块外定义的结果映射。所以,如果carResult按如下声明:
<resultMap id="carResult" type="Car">
<result property="doorCount" column="door_count" />
</resultMap>
那么只有doorCount属性会被加载。这步完成后完整地允许鉴别器实例的独立组,尽管和父结果映射可能没有什么关系。这种情况下,我们当然知道cars和vehicles之间有关系,如Car是一个Vehicle实例。因此,我们想要剩余的属性也被加载。我们设置的结果映射的简单改变如下。
<resultMap id="carResult" type="Car" extends="vehicleResult">
<result property="doorCount" column="door_count" />
</resultMap>
现在vehicleResult和carResult的属性都会被加载了。尽管曾经有些人会发现这个外部映射定义会多少有一些令人厌烦之处。因此还有另外一种语法来做简洁的映射风格。比如:
<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" resultType="carResult">
<result property="doorCount" column="door_count"/>
</case>
<case value="2" resultType="truckResult">
<result property="boxSize" column="box_size"/>
<result property="extendedCab" column="extended_cab"/>
</case>
<case value="3" resultType="vanResult">
<result property="powerSlidingDoor" column="power_sliding_door"/>
</case>
<case value="4" resultType="suvResult">
<result property="allWheelDrive" column="all_wheel_drive"/>
</case>
</discriminator>
</resultMap>
要记得这些都是结果映射,如果你不指定任何结果,那么MyBatis将会为你自动匹配列和属性。所以这些例子中的大部分是很冗长的,而其实是不需要的。也就是说,很多数据库是很复杂的,我们不太可能对所有示例都能依靠它。
5.6 自动映射
在简单的场景下,MyBatis可以替你自动映射查询结果。如果遇到复杂的场景,你需要构建一个resultmap。可以混合使用这两种策略。当自动映射查询结果时,MyBatis会获取sql返回的列名并在java类中查找相同名字的属性(忽略大小写)。这意味着如果Mybatis发现了ID列和id属性,Mybatis会将ID的值赋给id。通常数据库列使用大写单词命名,单词间用下划线分隔;而java属性一般遵循驼峰命名法。
为了在这两种命名方式之间启用自动映射,需要将mapUnderscoreToCamelCase设置为true。自动映射甚至在特定的resultmap下也能工作。在这种情况下,对于每一个resultmap,所有的ResultSet提供的列,如果没有被手工映射,则将被自动映射。自动映射处理完毕后手工映射才会被处理。在接下来的例子中,id和userName列将被自动映射,hashedpassword列将根据配置映射。
<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>
有三个自动映射级别:
NONE- 禁用自动映射,只有手动映射属性才会被设置。PARTIAL- 将自动映射结果,除了那些嵌套结果映射(连接)内的结果。FULL- 自动映射一切
默认值是PARTIAL,这是有原因的。使用FULL时,将在处理连接结果时执行自动映射,并且连接会检索同一行中的多个不同实体的数据,因此可能会导致不需要的映射。要了解风险,请查看下面的示例:
<select id="selectBlog" resultMap="blogResult">
select
B.id,
B.title,
A.username
from Blog B left outer join Author A on B.author_id = A.id
where B.id = #{id}
</select>
<mapper namespace="com.test.mapper.BlogMapper">
<resultMap id="blogResult" type="Blog">
<association property="author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<result property="username" column="author_username"/>
</resultMap>
</mapper>
在结果中Blog和Author均将自动映射。但是注意Author有一个id属性,在ResultSet中有一个列名为id,所以Author的id将被填充为Blog的id,这不是你所期待的。
所以需要谨慎使用FULL。通过添加autoMapping属性可以忽略自动映射等级配置,你可以启用或者禁用自动映射指定的ResultMap。
<resultMap id="userResultMap" type="User" autoMapping="false">
<result property="password" column="hashed_password"/>
</resultMap>
六、案例-高级结果映射
MyBatis创建的一个想法:数据库不用永远是你想要的或需要它们是什么样的。而我们最喜欢的数据库最好是第三范式或BCNF模式,但它们有时不是。
如果可能有一个单独的数据库映射,所有应用程序都可以使用它,这是非常好的,但有时也不是。结果映射就是MyBatis提供处理这个问题的答案。比如,我们如何映射下面这个语句?
<!-- Very Complex Statement -->
<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
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,
A.favourite_section as author_favourite_section,
P.id as post_id,
P.blog_id as post_blog_id,
P.author_id as post_author_id,
P.created_on as post_created_on,
P.section as post_section,
P.subject as post_subject,
P.draft as draft,
P.body as post_body,
C.id as comment_id,
C.post_id as comment_post_id,
C.name as comment_name,
C.comment as comment_text,
T.id as tag_id,
T.name as tag_name
from Blog B
left outer join Author A on B.author_id = A.id
left outer join Post P on B.id = P.blog_id
left outer join Comment C on P.id = C.post_id
left outer join Post_Tag PT on PT.post_id = P.id
left outer join Tag T on PT.tag_id = T.id
where B.id = #{id}
</select>
你可能想把它映射到一个智能的对象模型,包含一个作者写的博客,有很多的博文,每篇博文有零条或多条的评论和标签。下面是一个完整的复杂结果映射例子(假设作者,博客,博文,评论和标签都是类型的别名)。
<!-- Very Complex Result Map -->
<resultMap id="detailedBlogResultMap" type="Blog">
<constructor>
<idArg column="blog_id" javaType="int"/>
</constructor>
<result property="title" column="blog_title"/>
<association property="author" javaType="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"/>
<result property="favouriteSection" column="author_favourite_section"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
<association property="author" javaType="Author"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
</collection>
<collection property="tags" ofType="Tag" >
<id property="id" column="tag_id"/>
</collection>
<discriminator javaType="int" column="draft">
<case value="1" resultType="DraftPost"/>
</discriminator>
</collection>
</resultMap>

浙公网安备 33010602011771号