Mapper XML 文件

MyBatis 的真正强大之处在于映射语句。这是魔力发生的地方。对于它们的所有功能,Mapper XML 文件相对简单。当然,如果你将它们与等效的 JDBC 代码进行比较,你会立即看到代码节省了 95%。MyBatis 旨在专注于 SQL,并尽力不碍事。

Mapper XML 文件只有几个一级元素(按应定义的顺序排列)

  • cache – 给定命名空间的缓存配置。
  • cache-ref – 对另一个命名空间的缓存配置的引用。
  • resultMap – 最复杂且功能最强大的元素,用于描述如何从数据库结果集中加载对象。
  • parameterMap – 已弃用!映射参数的旧方法。首选内联参数,此元素将来可能会被删除。此处未记录。
  • sql – 其他语句可以引用的可重用 SQL 块。
  • insert – 映射的 INSERT 语句。
  • update – 映射的 UPDATE 语句。
  • delete – 映射的 DELETE 语句。
  • select – 映射的 SELECT 语句。

下一节将详细描述这些元素中的每一个,从语句本身开始。

select

select 语句是你将在 MyBatis 中使用的最流行的元素之一。将数据放入数据库中并不是特别有价值,直到你将其取回,因此大多数应用程序查询的频率远远高于修改数据。对于每个 insert、update 或 delete,可能有很多 select。这是 MyBatis 的基本原则之一,也是如此关注查询和结果映射的原因。对于简单的情况,select 元素非常简单。例如

<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

此语句称为 selectPerson,采用类型为 int(或 Integer)的参数,并返回一个 HashMap,其键为映射到行值的列名。

注意参数符号

#{id}

这会指示 MyBatis 创建 PreparedStatement 参数。使用 JDBC 时,此类参数会通过传递给新 PreparedStatement 的 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 元素具有更多属性,允许您配置每个语句应如何执行的详细信息。

<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 此命名空间中的唯一标识符,可用于引用此语句。
parameterType 将传递到此语句的参数的全限定类名或别名。此属性是可选的,因为 MyBatis 可以根据传递到语句的实际参数计算要使用的 TypeHandler。默认值为 unset
parameterMap 这是引用外部 parameterMap 的弃用方法。使用内联参数映射和 parameterType 属性。
resultType 此语句将返回的预期类型的全限定类名或别名。请注意,对于集合,这应该是集合包含的类型,而不是集合本身的类型。使用 resultTyperesultMap,不能同时使用。
resultMap 对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的功能,并且如果充分了解它们,可以解决许多困难的映射案例。使用 resultMapresultType,不能同时使用。
flushCache 将其设置为 true 将导致在每次调用此语句时刷新本地和二级缓存。默认值:对于 select 语句为 false
useCache 将其设置为 true 将导致此语句的结果缓存在二级缓存中。默认值:对于 select 语句为 true
timeout 这设置了在抛出异常之前,驱动程序将等待数据库从请求返回的秒数。默认值为 unset(取决于驱动程序)。
fetchSize 这是一个驱动程序提示,它将尝试使驱动程序以等于此设置大小的行批次返回结果。默认值为 unset(取决于驱动程序)。
statementType STATEMENTPREPAREDCALLABLE 中的任何一个。这导致 MyBatis 分别使用 StatementPreparedStatementCallableStatement。默认值:PREPARED
resultSetType FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE|DEFAULT(与 unset 相同)中的任何一个。默认值为 unset(取决于驱动程序)。
databaseId 如果配置了 databaseIdProvider,MyBatis 将加载所有没有 databaseId 属性或具有与当前属性匹配的 databaseId 的语句。如果找到具有和不具有 databaseId 的相同语句,则将丢弃后者。
resultOrdered 这仅适用于嵌套结果选择语句:如果为 true,则假定嵌套结果包含在一起或分组在一起,以便在返回新的主结果行时,不再出现对先前结果行的任何引用。这允许以更节省内存的方式填充嵌套结果。默认值:false
resultSets 这仅适用于多个结果集。它列出了语句将返回的结果集,并为每个结果集命名。名称以逗号分隔。
affectData 在编写返回数据的 INSERT、UPDATE 或 DELETE 语句时将其设置为 true,以便正确控制事务。另请参阅 事务控制方法。默认值:false(自 3.5.12 起)

insert、update 和 delete

数据修改语句 insert、update 和 delete 在其实现中非常相似

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">
插入、更新和删除属性
属性 说明
id 此命名空间中的唯一标识符,可用于引用此语句。
parameterType 将传递到此语句的参数的全限定类名或别名。此属性是可选的,因为 MyBatis 可以根据传递到语句的实际参数计算要使用的 TypeHandler。默认值为 unset
parameterMap 这是一种不推荐使用的方法,用于引用外部 parameterMap。使用内联参数映射和 parameterType 属性。
flushCache 将其设置为 true 将导致在调用此语句时刷新二级缓存和本地缓存。默认值:对于 insert、update 和 delete 语句为 true
timeout 这设置了在抛出异常之前,驱动程序将等待数据库从请求返回的最大秒数。默认值为 unset(取决于驱动程序)。
statementType STATEMENTPREPAREDCALLABLE 中的任何一个。这导致 MyBatis 分别使用 StatementPreparedStatementCallableStatement。默认值:PREPARED
useGeneratedKeys (仅限插入和更新)这告诉 MyBatis 使用 JDBC getGeneratedKeys 方法来检索数据库内部生成的键(例如 MySQL 或 SQL Server 等 RDBMS 中的自增字段)。默认值:false
keyProperty (仅插入和更新)标识 MyBatis 将设置由 getGeneratedKeys 或插入语句的 selectKey 子元素返回的关键值的目标属性。默认值:unset。如果预期有多个生成列,则可以是属性名称的逗号分隔列表。
keyColumn (仅插入和更新)设置具有生成键的表中列的名称。仅在某些数据库(如 PostgreSQL)中需要此项,当键列不是表中的第一列时。如果预期有多个生成列,则可以是列名称的逗号分隔列表。
databaseId 如果配置了 databaseIdProvider,MyBatis 将加载所有没有 databaseId 属性或具有与当前属性匹配的 databaseId 的语句。如果找到具有和不具有 databaseId 的相同语句,则将丢弃后者。

以下是插入、更新和删除语句的一些示例。

<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>

如前所述,插入语句更丰富一些,因为它有一些额外的属性和子元素,允许它以多种方式处理键生成。

首先,如果你的数据库支持自动生成键字段(例如 MySQL 和 SQL Server),那么你可以简单地设置 useGeneratedKeys="true" 并将 keyProperty 设置为目标属性,然后就完成了。例如,如果上面的 Author 表对 id 使用了自动生成列类型,则语句将修改如下

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

如果你的数据库还支持多行插入,你可以传递 Author 的列表或数组并检索自动生成的键。

<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>

MyBatis 还有另一种方法来处理不支持自动生成列类型或可能还不支持自动生成键的 JDBC 驱动程序的数据库的键生成。

这里有一个简单的(愚蠢的)示例,它将生成一个随机 ID(你可能永远不会这样做,但这展示了灵活性以及 MyBatis 如何真正地不在意)

<insert id="insertAuthor">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
  </selectKey>
  insert into Author
    (id, username, password, email,bio, favourite_section)
  values
    (#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>

在上面的示例中,selectKey 语句将首先运行,Author id 属性将被设置,然后将调用插入语句。这为你提供了与数据库中的自动生成键类似的行为,而不会使你的 Java 代码复杂化。

selectKey 元素的描述如下

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
selectKey 属性
属性 说明
keyProperty 应设置 selectKey 语句结果的目标属性。如果预期有多个生成列,则可以是属性名称的逗号分隔列表。
keyColumn 与属性匹配的返回结果集中的列名称。如果预期有多个生成列,则可以是列名称的逗号分隔列表。
resultType 结果的类型。MyBatis 通常可以弄清楚这一点,但添加它以确保无妨。MyBatis 允许将任何简单类型用作键,包括字符串。如果你预期有多个生成列,那么你可以使用包含预期属性的对象或 Map。
order 这可以设置为 BEFOREAFTER。如果设置为 BEFORE,则它将首先选择键,设置 keyProperty,然后执行插入语句。如果设置为 AFTER,则它将运行插入语句,然后运行 selectKey 语句 - 这在可能在插入语句中嵌入序列调用的 Oracle 等数据库中很常见。
statementType 与上述相同,MyBatis 支持 STATEMENTPREPAREDCALLABLE 语句类型,它们分别映射到 StatementPreparedStatementCallableStatement

作为不规则的情况,某些数据库允许 INSERT、UPDATE 或 DELETE 语句返回结果集(例如 PostgreSQL 和 MariaDB 的 RETURNING 子句或 MS SQL Server 的 OUTPUT 子句)。此类语句必须写为 <select> 以映射返回的数据。

<select id="insertAndGetAuthor" resultType="domain.blog.Author"
      affectData="true" flushCache="true">
  insert into Author (username, password, email, bio)
  values (#{username}, #{password}, #{email}, #{bio})
  returning id, username, password, email, bio
</select>

sql

此元素可用于定义可包含在其他语句中的 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>

属性值也可以用在包含 refid 属性或包含子句中的属性值中,例如

<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>

参数

在所有过去的语句中,您都看到了简单参数的示例。参数是 MyBatis 中非常强大的元素。对于简单的情况,可能 90% 的情况下,它们并没有什么作用,例如

<select id="selectUsers" resultType="User">
  select id, username, password
  from users
  where id = #{id}
</select>

上面的示例演示了一个非常简单的命名参数映射。parameterType 设置为 int,因此参数可以命名为任何内容。IntegerString 等原始或简单数据类型没有相关属性,因此将完全替换参数的全部值。但是,如果您传入一个复杂对象,那么行为会稍有不同。例如

<insert id="insertUser" parameterType="User">
  insert into users (id, username, password)
  values (#{id}, #{username}, #{password})
</insert>

如果将 User 类型的参数对象传递到该语句中,则会查找 id、username 和 password 属性,并将它们的值传递给 PreparedStatement 参数。

将参数传递到语句中非常简单。但参数映射还有许多其他功能。

首先,与 MyBatis 的其他部分一样,参数可以指定更具体的数据类型。

#{property,javaType=int,jdbcType=NUMERIC}

与 MyBatis 的其他部分一样,javaType 几乎总是可以从参数对象中确定,除非该对象是 HashMap。然后应指定 javaType 以确保使用正确的 TypeHandler

注意 如果将 null 作为值传递,则 JDBC 要求 JDBC 具有所有可空列的类型。您可以通过阅读 PreparedStatement.setNull() 方法的 JavaDocs 来自己调查这一点。

为了进一步自定义类型处理,您还可以指定特定的 TypeHandler 类(或别名),例如

#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

因此,它似乎已经变得冗长,但事实是你很少会设置其中任何一个。

对于数字类型,还有一个 numericScale 用于确定有多少位小数是相关的。

#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}

最后,mode 属性允许你指定 INOUTINOUT 参数。如果一个参数是 OUTINOUT,参数对象属性的实际值将被更改,就像你调用输出参数时所期望的那样。如果 mode=OUT(或 INOUT)并且 jdbcType=CURSOR(即 Oracle REFCURSOR),你必须指定一个 resultMap 来将 ResultSet 映射到参数的类型。请注意,javaType 属性在这里是可选的,如果将其留空,并且 jdbcTypeCURSOR,它将自动设置为 ResultSet

#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

MyBatis 还支持更高级的数据类型,例如结构,但你必须在注册输出参数时告诉语句类型名称。例如(同样,不要在实践中这样断行)

#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}

尽管有所有这些强大的选项,但大多数时候你只需指定属性名称,MyBatis 就会找出其余部分。最多,你将为可空列指定 jdbcType

#{firstName}
#{middleInitial,jdbcType=VARCHAR}
#{lastName}

字符串替换

默认情况下,使用 #{} 语法将导致 MyBatis 生成 PreparedStatement 属性,并将值安全地设置为 PreparedStatement 参数(例如 ?)。虽然这更安全、更快,并且几乎总是首选,但有时你只想直接将未修改的字符串注入到 SQL 语句中。例如,对于 ORDER BY,你可以使用类似这样的内容

ORDER BY ${columnName}

在这里,MyBatis 不会修改或转义字符串。

当 SQL 语句中的元数据(即表名或列名)是动态的时,字符串替换非常有用,例如,如果你想通过其任何一列从表中 select,而不是编写类似这样的代码

@Select("select * from user where id = #{id}")
User findById(@Param("id") long id);

@Select("select * from user where name = #{name}")
User findByName(@Param("name") String name);

@Select("select * from user where email = #{email}")
User findByEmail(@Param("email") String email);

// and more "findByXxx" method

你可以只写

@Select("select * from user where ${column} = #{value}")
User findByColumn(@Param("column") String column, @Param("value") String value);

其中 ${column} 将被直接替换,而 #{value} 将被“准备”。因此,你可以通过以下方式完成相同的工作

User userOfId1 = userMapper.findByColumn("id", 1L);
User userOfNameKid = userMapper.findByColumn("name", "kid");
User userOfEmail = userMapper.findByColumn("email", "noone@nowhere.com");

这个想法也可以应用于替换表名。

注意以这种方式接受来自用户的输入并将其提供给语句是不安全的。这会导致潜在的 SQL 注入攻击,因此你应该禁止在这些字段中输入用户,或者始终执行自己的转义和检查。

结果映射

resultMap 元素是 MyBatis 中最重要、最强大的元素。它允许你消除 JDBC 从 ResultSet 中检索数据所需的 90% 的代码,并且在某些情况下允许你执行 JDBC 甚至不支持的操作。事实上,为复杂语句的联接映射编写等效代码可能需要跨越数千行代码。ResultMap 的设计使得简单语句根本不需要显式结果映射,而更复杂的语句只需要描述关系所绝对必需的内容。

您已经看到没有显式 resultMap 的简单映射语句的示例。例如

<select id="selectUsers" resultType="map">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

此类语句仅仅导致所有列自动映射到 HashMap 的键,如 resultType 属性所指定。虽然在许多情况下很有用,但 HashMap 并不是一个很好的领域模型。您的应用程序更有可能使用 JavaBean 或 POJO(普通旧 Java 对象)作为领域模型。MyBatis 支持两者。考虑以下 JavaBean

package com.someapp.model;
public class User {
  private int id;
  private String username;
  private String hashedPassword;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getHashedPassword() {
    return hashedPassword;
  }
  public void setHashedPassword(String hashedPassword) {
    this.hashedPassword = hashedPassword;
  }
}

根据 JavaBean 规范,上述类具有 3 个属性:id、username 和 hashedPassword。这些与 select 语句中的列名完全匹配。

此类 JavaBean 可以像 HashMap 一样轻松地映射到 ResultSet

<select id="selectUsers" resultType="com.someapp.model.User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

请记住,类型别名是您的朋友。使用它们,这样您不必继续输入类的完全限定路径。例如

<!-- In 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>

在这些情况下,MyBatis 会自动在后台创建一个 ResultMap,以根据名称将列自动映射到 JavaBean 属性。如果列名不完全匹配,您可以在列名上使用选择子句别名(标准 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 的样子,因为这是解决列名不匹配的另一种方法。

<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 属性来执行此操作(请注意我们删除了 resultType 属性)。例如

<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>

如果世界总是那么简单就好了。

高级结果映射

MyBatis 是基于一个理念创建的:数据库并不总是您想要或需要的。虽然我们希望每个数据库都是完美的第 3 范式或 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>

您可能希望将其映射到一个智能对象模型,该模型由一位作者编写的博客组成,并有许多帖子,每个帖子可能包含零个或多个评论和标签。以下是复杂 ResultMap 的完整示例(假设 Author、Blog、Post、Comments 和 Tags 都是类型别名)。看看它,但不要担心,我们将逐步进行。虽然乍一看可能令人生畏,但它实际上非常简单。

<!-- 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>

resultMap 元素具有许多子元素,其结构值得讨论。以下是 resultMap 元素的概念视图。

resultMap

  • constructor - 用于在实例化时将结果注入到类的构造函数中
    • idArg - ID 参数;将结果标记为 ID 将有助于提高整体性能
    • arg - 注入到构造函数中的普通结果
  • id – ID 结果;将结果标记为 ID 将有助于提高整体性能
  • result – 注入到字段或 JavaBean 属性中的普通结果
  • association – 复杂类型关联;许多结果将汇总到此类型
    • 嵌套结果映射 – 关联本身就是 resultMap,或可以引用一个关联
  • collection – 复杂类型的集合
    • 嵌套结果映射 – 集合本身就是 resultMap,或可以引用一个集合
  • discriminator – 使用结果值来确定要使用哪个 resultMap
    • case – case 是基于某个值的结果映射
      • 嵌套结果映射 – case 本身也是一个结果映射,因此可以包含许多相同元素,或者可以引用外部 resultMap。
ResultMap 属性
属性 说明
id 此命名空间中可用于引用此结果映射的唯一标识符。
类型 完全限定的 Java 类名称或类型别名(有关内置类型别名的列表,请参见上表)。
自动映射 如果存在,MyBatis 将启用或禁用此 ResultMap 的自动映射。此属性会覆盖全局 autoMappingBehavior。默认值:未设置。

最佳实践始终增量构建 ResultMap。单元测试在此处确实有帮助。如果你尝试一次性构建像上面那样的巨大 resultMap,你很可能会出错,并且难以处理。从简单开始,一次执行一步。并进行单元测试!使用框架的缺点是它们有时有点像黑匣子(无论是否开源)。确保实现预期行为的最佳方法是编写单元测试。在提交错误时,它们也有帮助。

下一节将更详细地介绍每个元素。

id 和 result

<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

这些是最基本的结果映射。idresult 都将单个列值映射到简单数据类型(字符串、整数、双精度、日期等)的单个属性或字段。

两者之间的唯一区别是,id 将把结果标记为在比较对象实例时要使用的标识符属性。这有助于提高一般性能,但特别有助于缓存和嵌套结果映射(即联接映射)的性能。

每个都有多个属性

Id 和结果属性
属性 说明
property 将列结果映射到的字段或属性。如果给定名称存在匹配的 JavaBeans 属性,则将使用该属性。否则,MyBatis 将查找给定名称的字段。在这两种情况下,你都可以使用通常的点表示法进行复杂属性导航。例如,你可以映射到像 username 这样简单的内容,或映射到像 address.street.number 这样更复杂的内容。
column 数据库中的列名或别名列标签。这是通常传递给 resultSet.getString(columnName) 的相同字符串。
javaType 一个完全限定的 Java 类名或类型别名(有关内置类型别名的列表,请参见上表)。如果要映射到 JavaBean,MyBatis 通常可以弄清楚类型。但是,如果要映射到 HashMap,则应显式指定 javaType 以确保所需的行为。
jdbcType 此表后面的受支持类型列表中的 JDBC 类型。插入、更新或删除时,仅对可空列需要 JDBC 类型。这是 JDBC 要求,而不是 MyBatis 要求。因此,即使你直接编写 JDBC,也需要指定此类型——但仅适用于可空值。
typeHandler 我们在本文档中之前讨论过默认类型处理程序。使用此属性,你可以逐个映射地覆盖默认类型处理程序。该值要么是 TypeHandler 实现的完全限定类名,要么是类型别名。

支持的 JDBC 类型

供将来参考,MyBatis 通过包含的 JdbcType 枚举支持以下 JDBC 类型。

BIT FLOAT CHAR TIMESTAMP OTHER UNDEFINED
TINYINT REAL VARCHAR BINARY BLOB NVARCHAR
SMALLINT DOUBLE LONGVARCHAR VARBINARY CLOB NCHAR
INTEGER NUMERIC DATE LONGVARBINARY BOOLEAN NCLOB
BIGINT DECIMAL TIME NULL CURSOR ARRAY

构造函数

虽然属性适用于大多数数据传输对象 (DTO) 类型类,并且可能适用于您的大部分领域模型,但在某些情况下,您可能希望使用不可变类。通常,包含引用或查找数据(这些数据很少或从不更改)的表适合不可变类。构造函数注入允许您在实例化时设置类上的值,而无需公开方法。MyBatis 还支持私有属性和私有 JavaBeans 属性来实现此目的,但有些人更喜欢构造函数注入。构造函数元素启用了此功能。

考虑以下构造函数

public class User {
   //...
   public User(Integer id, String username, int age) {
     //...
  }
//...
}

为了将结果注入构造函数,MyBatis 需要以某种方式识别构造函数。在以下示例中,MyBatis 搜索声明了三个参数的构造函数:java.lang.Integerjava.lang.Stringint(按此顺序)。

<constructor>
   <idArg column="id" javaType="int"/>
   <arg column="username" javaType="String"/>
   <arg column="age" javaType="_int"/>
</constructor>

当您处理具有许多参数的构造函数时,维护 arg 元素的顺序容易出错。从 3.4.3 开始,通过指定每个参数的名称,您可以按任何顺序编写 arg 元素。要按名称引用构造函数参数,您可以向它们添加 @Param 注释,或使用“ -parameters”编译器选项编译项目并启用 useActualParamName(此选项默认启用)。以下示例对于相同的构造函数有效,即使第二个和第三个参数的顺序与声明的顺序不匹配也是如此。

<constructor>
   <idArg column="id" javaType="int" name="id" />
   <arg column="age" javaType="_int" name="age" />
   <arg column="username" javaType="String" name="username" />
</constructor>

如果存在具有相同名称和类型的可写属性,则可以省略 javaType

其余属性和规则与常规 id 和 result 元素相同。

属性 说明
column 数据库中的列名或别名列标签。这是通常传递给 resultSet.getString(columnName) 的相同字符串。
javaType 一个完全限定的 Java 类名或类型别名(有关内置类型别名的列表,请参见上表)。如果要映射到 JavaBean,MyBatis 通常可以弄清楚类型。但是,如果要映射到 HashMap,则应显式指定 javaType 以确保所需的行为。
jdbcType JDBC 类型来自下表中列出的受支持类型列表。仅在插入、更新或删除时才需要 JDBC 类型以支持空值列。这是 JDBC 要求,而不是 MyBatis 要求。因此,即使您直接编码 JDBC,也需要指定此类型 - 但仅适用于空值。
typeHandler 我们之前在本文档中讨论了默认类型处理器。使用此属性,您可以逐个映射地覆盖默认类型处理器。该值要么是 TypeHandler 实现的完全限定类名,要么是类型别名。
select 另一个映射语句的 ID,该语句将加载此属性映射所需的复杂类型。在 column 属性中指定的列中检索的值将作为参数传递给目标 select 语句。请参阅关联元素了解更多信息。
resultMap 这是可以将此参数的嵌套结果映射到适当对象图的 ResultMap 的 ID。这是使用对另一个选择语句的调用的替代方法。它允许您将多个表联接到单个 ResultSet 中。这样的 ResultSet 将包含重复的数据组,需要对其进行分解并正确映射到嵌套对象图。为了方便这一点,MyBatis 允许您将结果映射“链接”在一起,以处理嵌套结果。有关详细信息,请参阅下面的关联元素。
name 构造函数参数的名称。指定名称允许您按任何顺序编写 arg 元素。请参阅上面的说明。自 3.4.3 起。

association

<association property="author" javaType="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
</association>

association 元素处理“has-one”类型关系。例如,在我们的示例中,一个 Blog 有一个 Author。关联映射的工作方式与任何其他结果类似。您指定目标属性、属性的 javaType(MyBatis 大多数时候可以弄清楚)、必要的 jdbcType 以及要覆盖结果值检索的 typeHandler。

关联的不同之处在于,您需要告诉 MyBatis 如何加载关联。MyBatis 可以通过两种不同的方式做到这一点

  • 嵌套选择:通过执行另一个映射的 SQL 语句,该语句返回所需的复杂类型。
  • 嵌套结果:通过使用嵌套结果映射来处理联接结果的重复子集。

首先,让我们检查元素的属性。正如您将看到的,它仅通过 select 和 resultMap 属性与普通结果映射不同。

属性 说明
property 将列结果映射到的字段或属性。如果给定名称存在匹配的 JavaBeans 属性,则将使用该属性。否则,MyBatis 将查找给定名称的字段。在这两种情况下,你都可以使用通常的点表示法进行复杂属性导航。例如,你可以映射到像 username 这样简单的内容,或映射到像 address.street.number 这样更复杂的内容。
javaType 完全限定的 Java 类名称或类型别名(有关内置类型别名的列表,请参阅上表)。如果您映射到 JavaBean,MyBatis 通常可以弄清楚类型。但是,如果您映射到 HashMap,则应显式指定 javaType 以确保所需的行为。
jdbcType JDBC 类型来自下表中列出的受支持类型列表。仅在插入、更新或删除时才需要 JDBC 类型以支持空值列。这是 JDBC 要求,而不是 MyBatis 要求。因此,即使您直接编码 JDBC,也需要指定此类型 - 但仅适用于空值。
typeHandler 我们在本文档中之前讨论过默认类型处理程序。使用此属性,你可以逐个映射地覆盖默认类型处理程序。该值要么是 TypeHandler 实现的完全限定类名,要么是类型别名。

关联的嵌套选择

属性 说明
column 数据库中的列名或别名列标签,该标签保存将作为输入参数传递给嵌套语句的值。这是通常传递给 resultSet.getString(columnName) 的相同字符串。注意:要处理复合键,您可以使用语法 column="{prop1=col1,prop2=col2}" 指定要传递给嵌套选择语句的多个列名。这将导致 prop1prop2 设置为目标嵌套选择语句的参数对象。
select 加载此属性映射所需的复杂类型时,另一个映射语句的 ID。从列属性中指定的列中检索的值将作为参数传递到目标 select 语句。此表后附有详细示例。注意:要处理复合键,可以使用语法 column="{prop1=col1,prop2=col2}" 指定要传递到嵌套 select 语句的多个列名。这将导致 prop1prop2 设置为目标嵌套 select 语句的参数对象。
fetchType 可选。有效值是 lazyeager。如果存在,它将取代此映射的全局配置参数 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>

就是这样。我们有两个 select 语句:一个用于加载博客,另一个用于加载作者,博客的 resultMap 描述 selectAuthor 语句应用于加载其作者属性。

将自动加载所有其他属性,假设它们的列名和属性名匹配。

虽然此方法很简单,但对于大型数据集或列表,它不会很好地执行。此问题称为“N+1 选择问题”。简而言之,N+1 选择问题是由以下原因引起的

  • 执行单个 SQL 语句以检索记录列表(“+1”)。
  • 对于返回的每条记录,执行 select 语句以加载每个记录的详细信息(“N”)。

此问题可能导致执行数百或数千条 SQL 语句。这并不总是可取的。

好处是 MyBatis 可以延迟加载此类查询,因此您可能不必一次承担所有这些语句的成本。但是,如果您加载这样的列表,然后立即遍历它以访问嵌套数据,您将调用所有延迟加载,因此性能可能会非常差。

所以,还有另一种方法。

关联的嵌套结果

属性 说明
resultMap 这是 ResultMap 的 ID,它可以将此关联的嵌套结果映射到适当的对象图中。这是使用调用另一个 select 语句的替代方法。它允许您将多个表联接到单个 ResultSet 中。这样的 ResultSet 将包含重复的数据组,需要将这些数据组分解并正确映射到嵌套对象图。为了实现这一点,MyBatis 允许您将结果映射“链接”在一起,以处理嵌套结果。示例将更容易理解,此表后附有一个示例。
columnPrefix 联接多个表时,您必须使用列别名来避免 ResultSet 中的重复列名。指定 columnPrefix 允许您将此类列映射到外部 resultMap。请参阅本节后面解释的示例。
notNullColumn 默认情况下,仅当映射到子属性的列中至少有一个非空时,才会创建子对象。使用此属性,您可以通过指定哪些列必须具有值来更改此行为,这样 MyBatis 仅当这些列中的任何一个不为空时才会创建子对象。可以使用逗号作为分隔符指定多个列名。默认值:未设置。
自动映射 如果存在,MyBatis 在将结果映射到此属性时将启用或禁用自动映射。此属性会覆盖全局 autoMappingBehavior。请注意,它对外部 resultMap 无效,因此与 selectresultMap 属性一起使用毫无意义。默认值:未设置。

您已经看到了上面嵌套关联的一个非常复杂的示例。以下是演示其工作原理的一个更简单的示例。我们不会执行单独的语句,而是将 Blog 和 Author 表连接在一起,如下所示

<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 id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="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>

在上面的示例中,您可以在 Blog 的“author”关联中看到委托给“authorResult”resultMap 以加载 Author 实例。

非常重要 id 元素在嵌套结果映射中扮演着非常重要的角色。您应始终指定一个或多个可用于唯一标识结果的属性。事实上,即使您将其省略,MyBatis 仍将起作用,但会严重影响性能。选择尽可能少的属性来唯一标识结果。主键是一个显而易见的选择(即使是复合主键)。

现在,上面的示例使用外部 resultMap 元素来映射关联。这使得 Author resultMap 可重用。但是,如果您不需要重用它,或者您只是更喜欢将结果映射合并到一个描述性 resultMap 中,则可以嵌套关联结果映射。以下是使用此方法的相同示例

<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 来映射合著者结果。

<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>

关联的多个 ResultSet

属性 说明
column 使用多个结果集时,此属性指定将与 foreignColumn 关联的列(以逗号分隔),以标识关系的父项和子项。
foreignColumn 标识包含外键的列的名称,其值将与父类型 column 属性中指定的列的值进行匹配。
resultSet 标识将从中加载此复杂类型的结果集的名称。

从 3.2.3 版本开始,MyBatis 提供了另一种解决 N+1 问题的方法。

一些数据库允许存储过程返回多个结果集或一次执行多个语句并为每个语句返回一个结果集。这可用于仅访问数据库一次并返回相关数据,而无需使用连接。

在示例中,存储过程执行以下查询并返回两个结果集。第一个将包含博客,第二个将包含作者。

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM AUTHOR WHERE ID = #{id}

每个结果集必须通过向映射语句添加一个 resultSets 属性(其中包含用逗号分隔的名称列表)来指定一个名称。

<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
  {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

现在,我们可以指定用于填充“作者”关联的数据位于“作者”结果集中

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="email" column="email"/>
    <result property="bio" column="bio"/>
  </association>
</resultMap>

您已在上面了解了如何处理“has one”类型关联。但是“has many”呢?这是下一部分的主题。

集合

<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<Post> posts;

若要将一组嵌套结果映射到这样的列表,我们使用集合元素。就像关联元素一样,我们可以使用嵌套选择或联接中的嵌套结果。

集合的嵌套选择

首先,让我们看看如何使用嵌套选择来加载博客的帖子。

<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="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

您会立即注意到一些事情,但大部分内容看起来与我们上面了解的关联元素非常相似。首先,您会注意到我们正在使用集合元素。然后您会注意到有一个新的“ofType”属性。此属性对于区分 JavaBean(或字段)属性类型和集合包含的类型是必需的。因此,您可以这样读取以下映射

<collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>

读取为:“Post 类型 ArrayList 中的帖子集合”。

javaType 属性实际上是不必要的,因为 MyBatis 在大多数情况下会为您解决这个问题。因此,您通常可以将其缩短为

<collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>

集合的嵌套结果

到此为止,您可能已经猜到集合的嵌套结果将如何工作,因为它与关联完全相同,但增加了相同的 ofType 属性。

首先,让我们看看 SQL

<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 元素的重要性,或者如果您还没有阅读,请阅读上面的关联部分。

此外,如果您更喜欢允许更多重用结果映射的长格式,则可以使用以下备用映射

<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>

集合的多个结果集

对于关联,我们可以调用执行两个查询并返回两个结果集的存储过程,一个包含博客,另一个包含帖子

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM POST WHERE BLOG_ID = #{id}

每个结果集必须通过向映射语句添加一个 resultSets 属性(其中包含用逗号分隔的名称列表)来指定一个名称。

<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
  {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>

我们指定“posts”集合将从名为“posts”的结果集中填充数据

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
    <id property="id" column="id"/>
    <result property="subject" column="subject"/>
    <result property="body" column="body"/>
  </collection>
</resultMap>

注意您映射的关联和集合的深度、广度或组合没有限制。映射时应牢记性能。对应用程序进行单元测试和性能测试有助于发现最适合您应用程序的方法。MyBatis 的优点在于,它允许您稍后改变主意,对代码的影响非常小(如果有的话)。

高级关联和集合映射是一个深奥的主题。文档只能帮助您到此为止。经过一些练习,所有内容都会很快变得清晰。

鉴别器

<discriminator javaType="int" column="draft">
  <case value="1" resultType="DraftPost"/>
</discriminator>

有时,单个数据库查询可能会返回许多不同(但希望有点相关)数据类型的结果集。鉴别器元素旨在处理这种情况以及其他情况,包括类继承层次结构。鉴别器很容易理解,因为它在很大程度上类似于 Java 中的 switch 语句。

鉴别器定义指定 column 和 javaType 属性。column 是 MyBatis 将查找要比较的值的位置。javaType 是必需的,以确保执行适当的相等性测试(尽管 String 可能适用于几乎任何情况)。例如

<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 将从结果集中检索每条记录并比较其车辆类型值。如果它与任何鉴别器用例匹配,那么它将使用该用例指定的 resultMap。这是排他性执行的,换句话说,其他 resultMap 将被忽略(除非它被扩展,我们稍后会讨论)。如果没有任何用例匹配,那么 MyBatis 只是使用鉴别器块外部定义的 resultMap。因此,如果 carResult 声明如下

<resultMap id="carResult" type="Car">
  <result property="doorCount" column="door_count" />
</resultMap>

那么只有 doorCount 属性会被加载。这样做是为了允许完全独立的鉴别器用例组,即使这些组与父 resultMap 无关。当然,在这种情况下,我们知道汽车和车辆之间存在关系,因为汽车是车辆。因此,我们希望加载其他属性。对 resultMap 进行一个简单的更改,我们就可以开始了。

<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 将自动为您匹配列和属性。因此,这些示例中的大多数都比实际需要更冗长。也就是说,大多数数据库都比较复杂,我们不太可能在所有情况下都依赖它。

自动映射

正如您在前几节中已经看到的,在简单的情况下,MyBatis 可以自动为您映射结果,而在其他情况下,您需要构建结果映射。但正如您将在本节中看到的那样,您还可以混合这两种策略。让我们更深入地了解自动映射的工作原理。

在自动映射结果时,MyBatis 将获取列名并查找具有相同名称的属性,而忽略大小写。这意味着如果找到名为 ID 的列和名为 id 的属性,MyBatis 将使用 ID 列值设置 id 属性。

通常,数据库列使用大写字母和单词之间的下划线命名,而 Java 属性通常遵循驼峰命名约定。若要启用它们之间的自动映射,请将设置 mapUnderscoreToCamelCase 设置为 true。

即使有特定的结果映射,自动映射也会起作用。当这种情况发生时,对于每个结果映射,结果集中存在的所有尚未手动映射的列都将自动映射,然后将处理手动映射。在以下示例中,iduserName 列将自动映射,而 hashed_password 列将被映射。

<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>
<resultMap id="blogResult" type="Blog">
  <association property="author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <result property="username" column="author_username"/>
</resultMap>

使用此结果映射,BlogAuthor 都将自动映射。但请注意,Author 有一个 id 属性,并且结果集中有一个名为 id 的列,因此 Author 的 id 将填充 Blog 的 id,而这不是您所期望的。因此,请谨慎使用 FULL 选项。

无论配置了哪种自动映射级别,您都可以通过向特定 ResultMap 添加属性 autoMapping 来启用或禁用自动映射

<resultMap id="userResultMap" type="User" autoMapping="false">
  <result property="password" column="hashed_password"/>
</resultMap>

缓存

MyBatis 包含一个功能强大的事务查询缓存功能,该功能具有很强的可配置性和可定制性。MyBatis 3 缓存实现中进行了许多更改,使其功能更强大,配置也更简单。

默认情况下,仅启用本地会话缓存,该缓存仅用于在会话期间缓存数据。若要启用全局二级缓存,您只需向 SQL 映射文件添加一行即可

<cache/>

从字面上看就是这样。此简单语句的效果如下

  • 映射语句文件中的所有选择语句的结果都将被缓存。
  • 映射语句文件中的所有插入、更新和删除语句都将刷新缓存。
  • 缓存将使用最近最少使用 (LRU) 算法进行驱逐。
  • 缓存不会在任何基于时间的计划上刷新(即没有刷新间隔)。
  • 缓存将存储 1024 个对列表或对象的引用(无论查询方法返回什么)。
  • 缓存将被视为读/写缓存,这意味着检索到的对象不会被共享,并且可以由调用者安全地修改,而不会干扰其他调用者或线程的潜在修改。

注意缓存仅适用于位于缓存标记的映射文件中的语句。如果您将 Java API 与 XML 映射文件结合使用,则默认情况下不会缓存伴随接口中声明的语句。您需要使用 @CacheNamespaceRef 注释来引用缓存区域。

所有这些属性都可以通过缓存元素的属性进行修改。例如

<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 的插入/删除/更新时,它将更新。

使用自定义缓存

除了以这些方式自定义缓存之外,还可以通过实现自己的缓存或创建其他第三方缓存解决方案的适配器来完全覆盖缓存行为。

<cache type="com.domain.something.MyCustomCache"/>

此示例演示如何使用自定义缓存实现。type 属性中指定的类必须实现 org.apache.ibatis.cache.Cache 接口,并提供一个获取 String id 作为参数的构造函数。此接口是 MyBatis 框架中比较复杂的接口之一,但考虑到它的作用,它很简单。

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}

要配置缓存,只需向 Cache 实现添加公共 JavaBeans 属性,并通过缓存元素传递属性,例如,以下内容将在 Cache 实现上调用名为 setCacheFile(String file) 的方法

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

可以使用所有简单类型的 JavaBeans 属性,MyBatis 将进行转换。并且可以指定占位符(例如 ${cache.file})以替换在 配置属性 中定义的值。

自 3.4.2 起,MyBatis 支持在设置所有属性后调用初始化方法。如果您想使用此功能,请在您的自定义缓存类中实现 org.apache.ibatis.builder.InitializingObject 接口。

public interface InitializingObject {
  void initialize() throws Exception;
}

注意 在使用自定义缓存时,上面部分中的缓存设置(如逐出策略、读写等)不适用。

记住,缓存配置和缓存实例与 SQL 映射文件的命名空间绑定。因此,与缓存位于同一命名空间中的所有语句都受其约束。语句可以通过在逐语句基础上使用两个简单属性来修改它们与缓存交互的方式,或完全排除它们。默认情况下,语句配置如下

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

由于这是默认设置,您显然不应该显式地以这种方式配置语句。相反,仅当您想更改默认行为时才设置 flushCache 和 useCache 属性。例如,在某些情况下,您可能希望将特定选择语句的结果从缓存中排除,或者您可能希望选择语句刷新缓存。同样,您可能有一些更新语句,它们在执行时不需要刷新缓存。

cache-ref

从上一节中回忆一下,对于同一命名空间内的语句,只会使用或刷新此特定命名空间的缓存。可能有时您希望在命名空间之间共享相同的缓存配置和实例。在这种情况下,您可以使用 cache-ref 元素引用另一个缓存。

<cache-ref namespace="com.someone.application.data.SomeMapper"/>