Mybatis 高级结果映射 ResultMap Association Collection

高级结果映射

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



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





























 

这个resultMap 的元素的子元素比较多,讨论起来比较宽泛。下面我们从概念上概览一下这个resultMap的元素。

 

resultMap

·constructor实例化的时候通过构造器将结果集注入到类中

oidArg– ID 参数; 将结果集标记为ID,以方便全局调用

oarg注入构造器的结果集

·id结果集ID,将结果集标记为ID,以方便全局调用

·result注入一个字段或者javabean属性的结果

·association复杂类型联合;许多查询结果合成这个类型

o嵌套结果映射– associations能引用自身,或者从其它地方引用

·collection复杂类型集合

o嵌套结果映射– collections能引用自身,或者从其它地方引用

·discriminator使用一个结果值以决定使用哪个resultMap

ocase基于不同值的结果映射

§嵌套结果映射–case也能引用它自身, 所以也能包含这些同样的元素。它也可以从外部引用resultMap

 

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

 

下一节,我们一步步地查看这些细节。

id, result元素

 

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

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

 

Idresult属性如下:

 

Attribute

Description

property

映射数据库列的字段或属性。如果JavaBean 的属性与给定的名称匹配,就会使用匹配的名字。否则,MyBatis 将搜索给定名称的字段。两种情况下您都可以使用逗点的属性形式。比如,您可以映射到“username”,也可以映射到“address.street.number”

column

数据库的列名或者列标签别名。与传递给resultSet.getString(columnName)的参数名称相同。

javaType

完整java类名或别名(参考上面的内置别名列表)。如果映射到一个JavaBean,那MyBatis 通常会自行检测到。然而,如果映射到一个HashMap,那您应该明确指定javaType 来确保所需行为。

jdbcType

这张表下面支持的JDBC类型列表列出的JDBC类型。这个属性只在insertupdatedelete 的时候针对允许空的列有用。JDBC 需要这项,但MyBatis 不需要。如果您直接编写JDBC代码,在允许为空值的情况下需要指定这个类型。

typeHandler

我们已经在文档中讨论过默认类型处理器。使用这个属性可以重写默认类型处理器。它的值可以是一个TypeHandler实现的完整类名,也可以是一个类型别名。

支持的JDBC类型

MyBatis支持如下的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

 

 

Constructor元素

 

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

 

考虑下面的构造器:

 

public class User {

//…

public User(int id, String username) {

//…

}

//…

}

 

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

 

 

其它的属性与规则与idresult元素的一样。

 

Attribute

Description

column

数据库的列名或者列标签别名。与传递给resultSet.getString(columnName)的参数名称相同。

javaType

完整java类名或别名(参考上面的内置别名列表)。如果映射到一个JavaBean,那MyBatis 通常会自行检测到。然而,如果映射到一个HashMap,那您应该明确指定javaType 来确保所需行为。

jdbcType

支持的JDBC类型列表中列出的JDBC类型。这个属性只在insert,updatedelete 的时候针对允许空的列有用。JDBC 需要这项,但MyBatis 不需要。如果您直接编写JDBC代码,在允许为空值的情况下需要指定这个类型。

typeHandler

我们已经在文档中讨论过默认类型处理器。使用这个属性可以重写默认类型处理器。它的值可以是一个TypeHandler实现的完整类名,也可以是一个类型别名。

Association元素

Association元素处理“has-one”(一对一)这种类型关系。比如在我们的例子中,一个Blog有一个Author。联合映射与其它的结果集映射工作方式差不多,指定propertycolumnjavaType(通常MyBatis会自动识别)、jdbcType(如果需要)、typeHandler

不同的地方是您需要告诉MyBatis 如何加载一个联合查询。MyBatis使用两种方式来加载:

·Nested Select:通过执行另一个返回预期复杂类型的映射SQL语句(即引用外部定义好的SQL语句块)。

·Nested Results:通过嵌套结果映射(nested result mappings)来处理联接结果集(joined results)的重复子集。

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

Attribute

Description

property

映射数据库列的字段或属性。如果JavaBean 的属性与给定的名称匹配,就会使用匹配的名字。否则,MyBatis 将搜索给定名称的字段。两种情况下您都可以使用逗点的属性形式。比如,您可以映射到”username”,也可以映射到更复杂点的”address.street.number”

column

数据库的列名或者列标签别名。与传递给resultSet.getString(columnName)的参数名称相同。

注意: 在处理组合键时,您可以使用column= “{prop1=col1,prop2=col2}”这样的语法,设置多个列名传入到嵌套查询语句。这就会把prop1prop2设置到目标嵌套选择语句的参数对象中。

javaType

完整java类名或别名(参考上面的内置别名列表)。如果映射到一个JavaBean,那MyBatis 通常会自行检测到。然而,如果映射到一个HashMap,那您应该明确指定javaType 来确保所需行为。

jdbcType

支持的JDBC类型列表中列出的JDBC类型。这个属性只在insert,updatedelete 的时候针对允许空的列有用。JDBC 需要这项,但MyBatis 不需要。如果您直接编写JDBC代码,在允许为空值的情况下需要指定这个类型。

typeHandler

我们已经在文档中讨论过默认类型处理器。使用这个属性可以重写默认类型处理器。它的值可以是一个TypeHandler实现的完整类名,也可以是一个类型别名。

联合嵌套选择(Nested Select for Association

select

通过这个属性,通过ID引用另一个加载复杂类型的映射语句。从指定列属性中返回的值,将作为参数设置给目标select 语句。表格下方将有一个例子。注意:在处理组合键时,您可以使用column={prop1=col1,prop2=col2}”这样的语法,设置多个列名传入到嵌套语句。这就会把prop1prop2设置到目标嵌套语句的参数对象中。

 例如:  




 

 

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

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

 

译者注:

上面的例子,首先执行selectBlog”>查出的数据都会自动赋值给”blogResult”的与列名匹配的属性,这时blog_idtitle等就被赋值了。同时“blogResult”还有一个关联属性"Author",执行嵌套查询select=”selectAuthor”后,Author对象的属性idusernamepasswordemailbio也被赋于数据库匹配的值。

 

Blog

{

blog_id;

title;

Author author

{

id;

username;

password;

email;

bio;

 

}

 

}

 

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

·您执行单条SQL语句去获取一个列表的记录( “+1”)

·对列表中的每一条记录,再执行一个联合select 语句来加载每条记录更加详细的信息(“N”)

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

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

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

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

resultMap

一个可以映射联合嵌套结果集到一个适合的对象视图上的ResultMap 。这是一个替代的方式去调用另一个select 语句。它允许您去联合多个表到一个结果集里。这样的结果集可能包括冗余的、重复的需要分解和正确映射到一个嵌套对象视图的数据组。简言之,MyBatis 让您把结果映射‘链接’到一起,用来处理嵌套结果。举个例子会更好理解,例子在表格下方。

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

 

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






 






 

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

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

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











 

在上面的例子中您已经看到如果处理“一对一”(“has one”)类型的联合查询。但是对于“一对多”(“has many”)的情况如果处理呢?这个问题在下一节讨论。

Collection元素 






collection元素的作用差不多和association元素的作用一样。事实上,它们非常相似,以至于再对相似点进行描述会显得冗余,因此我们只关注它们的不同点。

继续我们上面的例子,一个Blog只有一个Author。但一个Blog有许多帖子(文章)。在Blog类中,会像下面这样定义相应属性: 

private List posts;

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

集合嵌套选择(Nested Select for Collection

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




 

 
 

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

 

ofType="Post" select=”selectPostsForBlog”/>

è理解为:“一个名为posts,类型为PostArrayList集合(A collection of posts in an ArrayList of type Post)” 。

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

select=”selectPostsForBlog”/>



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

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

让我们看下这个SQL 


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









再次强调一下,id 元素是非常重要的。如果您忘了或者不知道id 元素的作用,请先读一下上面association 一节。

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






 




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

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

Discriminator元素

 

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

 

Discriminator定义要指定columnjavaType属性。列是MyBatis将要取出进行比较的值,javaType用来确定适当的测试是否正确运行(虽然String在大部分情况下都可以工作),例:  















在这个例子中,MyBatis将会从结果集中取出每条记录,然后比较它的vehicle type的值。如果匹配任何discriminator中的case,它将使用由case指定的resultMap。这是排它性的,换句话说,其它的caseresultMap将会被忽略(除非使用我们下面说到的extended)。如果没有匹配到任何caseMyBatis只是简单的使用定义在discriminator块外面的resultMap。所以,如果carResult像下面这样定义:

 

 

那么,只有doorCount属性会被加载。这样做是为了与识别器cases群组完全独立开来,哪怕它与上一层的resultMap 一点关系都没有。在刚才的例子里我们当然知道carsvehicles的关系,a Car is-a Vehicle。因此,我们也要把其它属性加载进来。我们要稍稍改动一下resultMap

 

extends=”vehicleResult”>

 

现在,vehicleResultcarResult的所有属性都会被加载。

可能有人会认为这样扩展映射定义有一点单调了,所以还有一种可选的更加简单明了的映射风格语法。例如:























 

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

Cache元素

MyBatis包含一个强大的、可配置、可定制的查询缓存机制。MyBatis 3 的缓存实现有了许多改进,使它更强大更容易配置。默认的情况,缓存是没有开启,除了会话缓存以外,它可以提高性能,且能解决循环依赖。开启二级缓存,您只需要在SQL映射文件中加入简单的一行:

 

 

这句简单的语句作用如下:

·所有映射文件里的select语句的结果都会被缓存。

·所有映射文件里的insertupdatedelete语句执行都会清空缓存

·缓存使用最近最少使用算法(LRU)来回收

·缓存不会被设定的时间所清空。

·每个缓存可以存储1024 个列表或对象的引用(不管查询方法返回的是什么)。

·缓存将作为“读/写”缓存,意味着检索的对象不是共享的且可以被调用者安全地修改,而不会被其它调用者或者线程干扰。

所有这些特性都可以通过cache元素进行修改。例如:

eviction="FIFO"

flushInterval="60000"

size="512"

readOnly="true"/>

 

这种高级的配置创建一个每60秒刷新一次的FIFO 缓存,存储512个结果对象或列表的引用,并且返回的对象是只读的。因此在不用的线程里的调用者修改它们可能会引用冲突

 

可用的回收算法如下:

·LRU最近最少使用:移出最近最长时间内都没有被使用的对象。

·FIFO先进先出:移除最先进入缓存的对象。

·SOFT软引用: 基于垃圾回收机制和软引用规则来移除对象(空间内存不足时才进行回收)。

·WEAK弱引用:基于垃圾回收机制和弱引用规则(垃圾回收器扫描到时即进行回收)。

默认使用LRU

flushInterval:设置任何正整数,代表一个以毫秒为单位的合理时间。默认是没有设置,因此没有刷新间隔时间被使用,在语句每次调用时才进行刷新。

Size属性可以设置为一个正整数,您需要留意您要缓存对象的大小和环境中可用的内存空间。默认是1024

readOnly属性可以被设置为truefalse。只读缓存将对所有调用者返回同一个实例。因此这些对象都不能被修改,这可以极大的提高性能。可写的缓存将通过序列化来返回一个缓存对象的拷贝。这会比较慢,但是比较安全。所以默认值是false

 

使用自定义缓存

除了上面已经定义好的缓存方式,您能够通过您自己的缓存实现来完全重写缓存行为,或者通过创建第三方缓存解决方案的适配器。

这个例子演示了如果自定义缓存实现。由type指定的类必须实现org.mybatis.cache.Cache接口。这个接口是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();

ReadWriteLock getReadWriteLock();

}

 

要配置您的缓存,简单地添加一个公共的JavaBeans 属性到您的缓存实现中,然后通过cache 元素设置属性进行传递,下面示例,将在您的缓存实现上调用一个setCacheFile(String file)方法。

 

 

您可以使用所有简单的JavaBeans属性,MyBatis会自动进行转换。

需要牢记的是一个缓存配置和缓存实例都绑定到一个SQL Map 文件命名空间。因此,所有的这个相同命名空间的语句也都和这个缓存绑定。语句可以修改如何与这个缓存相匹配,或者使用两个简单的属性来完全排除它们自己。默认情况下,语句像下面这样来配置: