高级结果映射

高级结果映射

MyBatis 的创建基于这样一个思想:数据库并不是您想怎样就怎样的。虽然我们希望所有的数据库遵守第三范式或 BCNF(修正的第三范式),但它们不是。如果有一个数据库能够完美映射到所有应用数据模型,也将是非常棒的,但也没有。结果集映射就是 MyBatis 为解决这些问题而提供的解决方案。例如,我们如何映射下面这条语句?

<select id="selectBlogDetails" parameterType="int" 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>

您可能想要把它映射到一个智能的对象模型,包括由一个作者写的一个博客,有许多文章(Post,帖子),每个文章由0个或者多个评论和标签。下面是一个复杂 ResultMap 的完整例子(假定作者、博客、文章、评论和标签都是别名)。仔细看看这个例子,但是不用太担心,我们会一步步地来分析,一眼看上去可能让人沮丧,但是实际上非常简单的。

<resultMap id="detailedBlogResultMap" type="Blog">
    <constructor>
        <idArg column="blog_id" javaType="int"/>
    </constructor>
    <result property="title" column="blog_title"/>
    <association property="author" column="blog_author_id" 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" column="post_author_id" javaType="Author"/>
        <collection property="comments" column="post_id" ofType="Comment">
            <idproperty="id"column="comment_id"/>
        </collection>
        <collection property="tags" column="post_id" 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:复杂类型联合;许多查询结果合成这个类型
    • 嵌套结果映射:associations 能引用自身,或者从其它地方引用
  • collection:复杂类型集合
    • 嵌套结果映射:collections 能引用自身,或者从其它地方引用
  • discriminator:使用一个结果值以决定使用哪个 resultMap
    • case:基于不同值的结果映射
    • 嵌套结果映射:case 也能引用它自身,所以也能包含这些同样的元素。它也可以从外部引用 resultMap

最佳实践:逐步地生成 resultMap,单元测试对此非常有帮助。如果您尝试一下子就生成像上面这样巨大的 resultMap,可能会出错,并且工作起来非常吃力。从简单地开始,再一步步地扩展,并且进行单元测试。使用框架开发有一个缺点,它们有时像是一个黑盒子。为了确保达到您所预想的行为,最好的方式就是进行单元测试。这对提交 bugs 也非常有用。

我们一步步地查看这些细节

一、id, result元素

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

这是最基本的结果集映射。id 和 result 将列映射到属性或简单的数据类型字段(String, int, double, Date 等)。

这两者唯一不同的是,在比较对象实例时 id 作为结果集的标识属性。这有助于提高总体性能,特别是应用缓存和嵌套结果映射的时候。

id、result属性如下:

高级结果映射

高级结果映射

 支持的 JDBC 类型

 MyBatis 支持如下的 JDBC 类型

 高级结果映射

二、Constructor元素

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

当属性与 DTO,或者与您自己的域模型一起工作的时候,许多场合要用到不变类。通常,包含引用,或者查找的数据很少或者数据不会改变的的表,适合映射到不变类中。构造器注入允许您在类实例化后给类设值,这不需要通过 public 方法。MyBatis 同样也支持 private 属性和 JavaBeans 的私有属性达到这一点,但是一些用户可能更喜欢使用构造器注入。构造器元素可以做到这点。

考虑下面的构造器:

public class User{
    //…
    public User(int id,String username){
        //…
    }
    //…
}    

为了将结果注入构造器,MyBatis 需要使用它的参数类型来标记构造器。Java 没有办法通过参数名称来反射获得。因此当创建 constructor 元素,确保参数是按顺序的并且指定了正确的类型

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

其它的属性与规则与id、result元素的一样。

高级结果映射

三、Association元素

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

Association 元素处理 "has-one"(一对一)这种类型关系。比如在我们的例子中,一个 Blog 有一个 Author。联合映射与其它的结果集映射工作方式差不多,指定 property、column、 javaType(通常MyBatis会自动识别)、jdbcType(如果需要)、typeHandler。 不同的地方是您需要告诉 MyBatis 如何加载一个联合查询。MyBatis 使用两种方式来加载:

  • Nested Select:通过执行另一个返回预期复杂类型的映射SQL语句(即引用外部定义好 的SQL语句块)。
  • Nested Results:通过嵌套结果映射(nested result mappings)来处理联接结果集 (joined results)的重复子集。

首先,让我们检查一下元素属性。正如您看到的,它不同于普通只有 select 和 resultMap 属性的结果映射。

高级结果映射

高级结果映射

1、联合嵌套选择(Nested Select for Association)

高级结果映射

例如:

<resultMap id="blogResult" type="Blog">
    <association property="author" column="blog_author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectBlog" parameterType="int" resultMap="blogResult"> SELECT * FROM BLOG WHERE ID=#{id} </select>

<select id="selectAuthor" parameterType="int" resultType="Author"> SELECT * FROM AUTHOR WHERE ID=#{id} </select>

我们使用两个 select 语句:一个用来加载 Blog,另一个用来加载 Author。Blog 的 resultMap 描述了使用 "selectAuthor" 语句来加载 author 的属性。

如果列名和属性名称相匹配的话,所有匹配的属性都会自动加载。

注意:上面的例子,首先执行 <select 后,Author对象的属性 id,username,password,email,bio 也被赋于与数据库匹配的值。

Blog
{
    blog_id;
    title;
    Author author
    {
        id;
        username;
        password;
        email;
        bio;
    }
}

建议不要使用 Mybatis 的自动赋值,这样不能够清晰地知道要映射哪些属性,并且有时候还不能保证正确地映射数据库检索结果。

虽然这个方法简单,但是对于大数据集或列表查询,就不尽如人意了。这个问题被称为 "N+1" 选择问题(N+1 Selects Problem)。概括地说,N+1 选择问题是这样产生的:

  • 您执行单条 SQL 语句去获取一个列表的记录("+1")。
  • 对列表中的每一条记录,再执行一个联合 select 语句来加载每条记录更加详细的信息("N")。

如:执行一条 SQL 语句获得了10条记录,这10条记录的每一条再执行一条 SQL 语句去加载更详细的信息,这就执行了10+1次查询。

这个问题会导致成千上万的 SQL 语句的执行,因此并非总是可取的。

上面的例子,MyBatis 可以使用延迟加载这些查询,因此这些查询立马可节省开销。然而,如果您加载一个列表后立即迭代访问嵌套的数据,这将会调用所有的延迟加载,因此性能会变得非常糟糕。

鉴于此,这有另外一种方式。

2、联合嵌套结果集(Nested Results for Association)

高级结果映射

您已经在上面看到了一个非常复杂的嵌套联合的例子,接下的演示的例子会更简单一些。我们把 Blog 和 Author 表联接起来查询,而不是执行分开的查询语句:

<select id="selectBlog" parameterType="int" 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>

注意到这个连接(join),要确保所有的别名都是唯一且无歧义的。这使映射容易多了,现在我们来映射结果集:

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

在上面的例子中,您会看到 Blog 的作者("author")联合一个 "authorResult" 结果映射来加载 Author 实例。

重点提示:id 元素在嵌套结果映射中扮演了非常重要的角色,您应该总是指定一个或多个属性来唯一标识这个结果集。事实上,如果您没有那样做,MyBatis 也会工作,但是会导致严重性能开销。选择尽量少的属性来唯一标识结果,而使用主键是最明显的选择(即使是复合主键)。

上面的例子使用一个扩展的 resultMap 元素来联合映射。这可使 Author 结果映射可重复使用。然后,如果您不需要重用它,您可以直接嵌套这个联合结果映射。下面例子就是使用这样的方式:

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

在上面的例子中您已经看到如果处理 "一对一"("has one")类型的联合查询。但是对于 "一对多"("hasmany")的情况如果处理呢?请接着往下看。

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

collection 元素的作用差不多和 association 元素的作用一样。事实上,它们非常相似,以至于再对相似点进行描述会显得冗余,因此我们只关注它们的不同点。继续我们上面的例子,一个 Blog 只有一个 Author。但一个 Blog 有许多帖子(文章)。在 Blog 类中,会像下面这样定义相应属性:

private List<Post> posts;

映射一个嵌套结果集到一个列表,我们使用 collection 元素。就像 association 元素那样,我们使用嵌套查询,或者从连接中嵌套结果集。

1、集合嵌套选择(Nested Select for Collection)

首先我们使用嵌套选择来加载 Blog 的文章。

<resultMap id="blogResult" type="Blog">
    <collection property="posts" javaType="ArrayList" column="blog_id"
        ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" parameterType="int" resultMap="blogResult">
    SELECT * FROM BLOG WHERE ID=#{id}
</select>

<select id="selectPostsForBlog" parameterType="int" resultType="Author">
   SELECT *FROM POST WHERE BLOG_ID=#{id}
</select>

 一看上去这有许多东西需要注意,但大部分看起与我们在 association 元素中学过的相似。首先,您会注意到我们使用了 collection 元素,然后会注意到一个新的属性 "ofType"。这个元素是用来区别 JavaBean 属性(或者字段)类型和集合所包括的类型。因此您会读到下面这段代码。

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

理解为:一个名为posts,类型为 Post 的 ArrayList 集合。

javaType 属性不是必须的,通常 MyBatis 会自动识别,所以您通常可以简略地写成:

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

2、集合的嵌套结果集(Nested Results for Collection)

这时候,您可能已经猜出嵌套结果集是怎样工作的了,因为它与 association 非常相似,只不过多了一个属性 "ofType"。

让我们看下这个 SQL:

<select id="selectBlog" parameterType="int" 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>

同样,我们把 Blog 和 Post 两张表连接在一起,并且也保证列标签名在映射的时候是唯一且无歧义的。现在将 Blog 和 Post 的集合映射在一起是多么简单:

<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 元素是非常重要的。如果您忘了或者不知道 id 元素的作用,请先读一下上面 association 一节。

如果希望结果映射有更好的可重用性,您可以使用下面的方式:

<resultMap id="blogResult" type="Blog">
    <id property="id" column="blog_id"/>
    <result property="title" column="blog_title"/>
    <collection property="posts" ofType="Post" resultMap="blogPostResult"/>
</resultMap>

<resultMap id="blogPostResult" type="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
</resultMap>

Note:在您的映射中没有深度、宽度、联合和集合数目的限制。但应该谨记,在进行映射的时候也要考虑性能的因素。应用程序的单元测试和性能测试帮助您发现最好的方式可能要花很长时间。但幸运的是,MyBatis允许您以后可以修改您的想法,这时只需要修改少量代码就行了。

关于高级联合和集合映射是一个比较深入的课题,文档只能帮您了解到这里,多做一些实践,一切将很快变得容易理解。

五、Discriminator元素

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

有时候一条数据库查询可能会返回包括各种不同的数据类型的结果集。Discriminator(识别器)元素被设计来处理这种情况,以及其它像类继承层次情况。识别器非常好理解,它就像 java 里的 switch 语句。

Discriminator 定义要指定 column 和 javaType 属性。列是 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 将会从结果集中取出每条记录,然后比较它的 vehicle_type 的值。如果匹配任何 discriminator 中的 case,它将使用由 case 指定的 resultMap。这是排它性的,换句话说,其它的 case 的 resultMap 将会被忽略(除非使用我们下面说到的extended)。如果没有匹配到任何 case,MyBatis 只是简单的使用定义在 discriminator 块外面的 resultMap。所以,如果 carResult 像下面这样定义:

<resultMap id="carResult" type="Car">
    <resul tproperty="doorCount" column="door_count"/>
</resultMap>

那么,只有 doorCount 属性会被加载。这样做是为了与识别器 cases 群组完全独立开来,哪怕它与上一层的 resultMap 一点关系都没有。在刚才的例子里我们当然知道 cars 和 vehicles 的关系,a Car is a Vehicle。因此,我们也要把其它属性加载进来。我们要稍稍改动一下 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>
        <casevalue="4"resultType="suvResult">
            <result property="allWheelDrive" column="all_wheel_drive"/>
        </case>
  </discriminator>
</resultMap>

记住:对于这么多的结果映射,如果您不指定任何的结果集,那么 MyBatis 会自动地将列名与属性相匹配。所以上面所举的例子比实际中需要的要详细。尽管如此,大部分数据库有点复杂,并且它并不是所有情况都是完全可以适用的。