高级结果映射
MyBatis 创建时的一个思想是:数据库不可能永远是你所想或所需的那个样子。 我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们并不都是那样。 如果能有一种数据库映射模式,完美适配所有的应用程序,那就太好了,但可惜也没有。 而 ResultMap 就是 MyBatis 对这个问题的答案。
一、关于resultMap知识点讲解
我们如何映射下面这个语句?
<!-- 非常复杂的语句 -->
<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 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 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射
下一部分将详细说明每个元素。
这些元素是结果映射的基础。id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。id & result
这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候
两个元素都有一些属性:
支持的 JDBC 类型
为了以后可能的使用场景,MyBatis 通过内置的 jdbcType 枚举类型支持下面的 JDBC 类型。
构造方法
通过修改对象属性的方式,可以满足大多数的数据传输对象(Data Transfer Object, DTO)以及绝大部分领域模型的要求。但有些情况下你想使用不可变类。 一般来说,很少改变或基本不变的包含引用或数据的表,很适合使用不可变类。 构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。MyBatis 也支持私有属性和私有 JavaBean 属性来完成注入,但有一些人更青睐于通过构造方法进行注入。 constructor 元素就是为此而生的。
看看下面这个构造方法:
public class User {
//...
public User(Integer id, String username, int age) {
//...
}
//...
}
为了将结果注入构造方法,MyBatis 需要通过某种方式定位相应的构造方法。 在下面的例子中,MyBatis 搜索一个声明了三个形参的构造方法,参数类型以 java.lang.Integer
, java.lang.String
和 int
的顺序给出。
<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
元素中参数声明的顺序不匹配。
<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 元素是一样的。
关联
<association property="author" column="blog_author_id" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>
关联(association)元素处理“有一个”类型的关系。 比如,在我们的示例中,一个博客有一个用户。关联结果映射和其它类型的映射工作方式差不多。 你需要指定目标属性名以及属性的javaType
(很多时候 MyBatis 可以自己推断出来),在必要的情况下你还可以设置 JDBC 类型,如果你想覆盖获取结果值的过程,还可以设置类型处理器。
关联的不同之处是,你需要告诉 MyBatis 如何加载关联。MyBatis 有两种不同的方式加载关联:
嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。
首先,先让我们来看看这个元素的属性。你将会发现,和普通的结果映射相比,它只在 select 和 resultMap 属性上有所不同。
关联的嵌套 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 查询语句:一个用来加载博客(Blog),另外一个用来加载作者(Author),而且博客的结果映射描述了应该使用 selectAuthor 语句加载它的 author 属性。
其它所有的属性将会被自动加载,只要它们的列名和属性名相匹配。
这种方式虽然很简单,但在大型数据集或大型数据表上表现不佳。这个问题被称为“N+1 查询问题”。 概括地讲,N+1 查询问题是这样子的:
你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。
这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。
好消息是,MyBatis 能够对这样的查询进行延迟加载,因此可以将大量语句同时运行的开销分散开来。 然而,如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕。
所以还有另外一种方法。
关联的嵌套结果映射
之前,你已经看到了一个非常复杂的嵌套关联的例子。 下面的例子则是一个非常简单的例子,用于演示嵌套结果映射如何工作。 现在我们将博客表和作者表连接在一起,而不是执行一个独立的查询语句,就像这样:
<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" 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” 的结果映射来加载作者对象的实例。
非常重要: 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>
那如果博客(blog)有一个共同作者(co-author)该怎么办?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 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>
由于结果中的列名与结果映射中的列名不同。你需要指定 columnPrefix 以便重复使用该结果映射来映射 co-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>
关联的多结果集(ResultSet)
从版本 3.2.3 开始,MyBatis 提供了另一种解决 N+1 查询问题的方法。
某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。
在例子中,存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果。
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>
现在我们可以指定使用 “authors” 结果集的数据来填充 “author” 关联:
<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>
你已经在上面看到了如何处理“有一个”类型的关联。但是该怎么处理“有很多个”类型的关联呢?这就是我们接下来要介绍的。
集合
<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>
集合元素和关联元素几乎是一样的,它们相似的程度之高,以致于没有必要再介绍集合元素的相似部分。 所以让我们来关注它们的不同之处吧。
我们来继续上面的示例,一个博客(Blog)只有一个作者(Author)。但一个博客有很多文章(Post)。 在博客类中,这可以用下面的写法来表示:
private List<Post> posts;
要像上面这样,映射嵌套结果集合到一个 List 中,可以使用集合元素。 和关联元素一样,我们可以使用嵌套 Select 查询,或基于连接的嵌套结果映射集合。
集合的嵌套 Select 查询
首先,让我们看看如何使用嵌套 Select 查询来为博客加载文章。
<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"/>
读作: “posts 是一个存储 Post 的 ArrayList 集合”
在一般情况下,MyBatis 可以推断 javaType 属性,因此并不需要填写。所以很多时候你可以简略成:
<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>
集合的多结果集(ResultSet)
像关联元素那样,我们可以通过执行存储过程实现,它会执行两个查询并返回两个结果集,一个是博客的结果集,另一个是文章的结果集:
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>
有时候,一个数据库查询可能会返回多个不同的结果集(但总体上还是有一定的联系的)。 鉴别器(discriminator)元素就是被设计来应对这种情况的,另外也能处理其它情况,例如类的继承层次结构。 鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句。
一个鉴别器的定义需要指定 column
和 javaType
属性。column
指定了 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 type
值。 如果它匹配任意一个鉴别器的 case
,就会使用这个 case
指定的结果映射。 这个过程是互斥的,也就是说,剩余的结果映射将被忽略(除非它是扩展的,我们将在稍后讨论它)。 如果不能匹配任何一个 case
,MyBatis 就只会使用鉴别器块外定义的结果映射。 所以,如果 carResult 的声明如下:
<resultMap id="carResult" type="Car">
<result property="doorCount" column="door_count" />
</resultMap>
那么只有 doorCount 属性会被加载。这是为了即使鉴别器的 case 之间都能分为完全独立的一组,尽管和父结果映射可能没有什么关系。在上面的例子中,我们当然知道 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>
提示: 请注意,这些都是结果映射,如果你完全不设置任何的 result 元素,MyBatis 将为你自动匹配列和属性。所以上面的例子大多都要比实际的更复杂。 这也表明,大多数数据库的复杂度都比较高,我们不太可能一直依赖于这种机制。
自动映射
正如你在前面一节看到的,在简单的场景下,MyBatis 可以为你自动映射查询结果。但如果遇到复杂的场景,你需要构建一个结果映射。 但是在本节中,你将看到,你可以混合使用这两种策略。让我们深入了解一下自动映射是怎样工作的。
当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。
通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true。
甚至在提供了结果映射后,自动映射也能工作。在这种情况下,对于每一个结果映射,在 ResultSet 出现的列,如果没有设置手动映射,将被自动映射。在自动映射处理完毕后,再处理手动映射。 在下面的例子中,id 和 userName 列将被自动映射,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>
在该结果映射中,Blog 和 Author 均将被自动映射。但是注意 Author 有一个 id 属性,在 ResultSet 中也有一个名为 id 的列,所以 Author 的 id 将填入 Blog 的 id,这可不是你期望的行为。 所以,要谨慎使用 FULL
。
无论设置的自动映射等级是哪种,你都可以通过在结果映射上设置 autoMapping
属性来为指定的结果映射设置启用/禁用自动映射。
<resultMap id="userResultMap" type="User" autoMapping="false">
<result property="password" column="hashed_password"/>
</resultMap>
二、基于上面的知识点分别给例子
1、多对一处理
多对一的理解:
多个学生对应一个老师
如果对于学生这边,就是一个多对一的现象,即从学生这边关联一个老师!
如果懒得写get和set的方法这里推荐一个插件
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
用注解就可以
@Data //GET,SET,ToString,有参,无参构造
实体类:
public class Teacher {
private int id;
private String name;
}
public class Student {
private int id;
private String name;
//多个学生可以是同一个老师,即多对一
private Teacher teacher;
}
编写实体类对应的Mapper接口 【两个】
public interface StudentMapper {
}
public interface TeacherMapper {
}
5、编写Mapper接口对应的 mapper.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="Mapper.StudentMapper">
</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="Mapper.TeacherMapper">
</mapper>
按查询嵌套处理
1、给StudentMapper接口增加方法
//获取所有学生及对应老师的信息
public List<Student> getStudents();
2、编写对应的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="Mapper.StudentMapper">
<!--
1. 获取所有学生的信息
2. 根据获取的学生信息的老师ID->获取该老师的信息
1. 做一个结果集映射:StudentTeacher
2. StudentTeacher结果集的类型为 Student
3. 学生中老师的属性为teacher,对应数据库中为tid。
多个 [1,...)学生关联一个老师=> 一对一,一对多
4. association – 一个复杂类型的关联;使用它来处理关联查询
-->
<select id="getStudents" resultMap="StudentTeacher">
select * from student
</select>
<resultMap id="StudentTeacher" type="Student">
<!--association关联属性 property属性名 javaType属性类型 column在多的一方的表中的列名-->
<association property="teacher" column="tid" javaType="Teacher" select="getTeacher"/>
</resultMap>
<!--
这里传递过来的id,只有一个属性的时候,下面可以写任何值
association中column多参数配置:
column="{key=value,key=value}"
其实就是键值对的形式,key是传给下个sql的取值名称,value是片段一中sql查询的字段名。
-->
<select id="getTeacher" resultType="teacher">
select * from teacher where id = #{id}
</select>
</mapper>
3、编写完毕去Mybatis配置文件中,注册Mapper!
4、注意点说明:
<resultMap id="StudentTeacher" type="Student">
<!--association关联属性 property属性名 javaType属性类型 column在多的一方的表中的列名-->
<association property="teacher" column="{id=tid,name=tid}" javaType="Teacher" select="getTeacher"/>
</resultMap>
<!--
这里传递过来的id,只有一个属性的时候,下面可以写任何值
association中column多参数配置:
column="{key=value,key=value}"
其实就是键值对的形式,key是传给下个sql的取值名称,value是片段一中sql查询的字段名。
-->
<select id="getTeacher" resultType="teacher">
select * from teacher where id = #{id} and name = #{name}
</select>
5、测试
@Test
public void selectUser2() {
// 定义SqlSession变量
SqlSession sqlSession = null;
try {
// 创建SqlSession实例
sqlSession = MyFactroySqlsession.getSqlSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
for (Student student : mapper.getStudents()) {
System.out.println(student);
}
// 提交事务
sqlSession.commit();
} catch (Exception e) {
// 回滚事务
sqlSession.rollback();
e.printStackTrace();
}finally {
// 关闭SqlSession
if(sqlSession != null)
sqlSession.close();
}
}
按结果嵌套处理
1、接口方法编写
public List<Student> getStudents2();
2、编写对应的mapper文件
<!--
按查询结果嵌套处理
思路:
1. 直接查询出结果,进行结果集的映射
-->
<select id="getStudents2" resultMap="StudentTeacher2" >
select s.id sid, s.name sname , t.name tname
from student s,teacher t
where s.tid = t.id
</select>
<resultMap id="StudentTeacher2" type="Student">
<id property="id" column="sid"/>
<result property="name" column="sname"/>
<!--关联对象property 关联对象在Student实体类中的属性-->
<association property="teacher" javaType="Teacher">
<result property="name" column="tname"/>
</association>
</resultMap>
3、测试
按照查询进行嵌套处理就像SQL中的子查询
按照结果进行嵌套处理就像SQL中的联表查询
2、一对多处理
一对多的理解:
一个老师拥有多个学生
如果对于老师这边,就是一个一对多的现象,即从一个老师下面拥有一群学生(集合)
实体类:
public class Teacher {
private int id;
private String name;
//一个老师多个学生
private List<Student> students;
}
public class Student {
private int id;
private String name;
private int tid;
}
剩下的同上
按结果嵌套处理
1、TeacherMapper接口编写方法
//获取指定老师,及老师下的所有学生
public Teacher getTeacher(int id);
2、编写接口对应的Mapper配置文件
<mapper namespace="Mapper.TeacherMapper">
<!--
1. 从学生表和老师表中查出学生id,学生姓名,老师姓名
2. 对查询出来的操作做结果集映射
1. 集合的话,使用collection!
JavaType和ofType都是用来指定对象类型的
JavaType是用来指定pojo中属性的类型
ofType指定的是映射到list集合属性中pojo的类型。
-->
<select id="getTeacher" resultMap="TeacherStudent">
select s.id sid, s.name sname , t.name tname, t.id tid
from student s,teacher t
where s.tid = t.id and t.id=#{id}
</select>
<resultMap id="TeacherStudent" type="Teacher">
<result property="name" column="tname"/>
<collection property="students" ofType="Student">
<result property="id" column="sid" />
<result property="name" column="sname" />
<result property="tid" column="tid" />
</collection>
</resultMap>
</mapper>
3、将Mapper文件注册到MyBatis-config文件中
<mappers>
<mapper resource="mapper/TeacherMapper.xml"/>
</mappers>
4、测试
按查询嵌套处理
1、TeacherMapper接口编写方法
public Teacher getTeacher2(int id);
2、编写接口对应的Mapper配置文件
<select id="getTeacher2" resultMap="TeacherStudent2">
select * from teacher where id = #{id}
</select>
<resultMap id="TeacherStudent2" type="Teacher">
<!--column是一对多的外键 , 写的是一的主键的列名-->
<collection property="students" javaType="ArrayList" ofType="Student" column="id" select="getStudentByTeacherId"/>
</resultMap>
<select id="getStudentByTeacherId" resultType="Student">
select * from student where tid = #{id}
</select>
3、测试
3、多对多处理
比如:淘宝的订单,订单可以包含多个人,包含多个商品
实体类:
public class Article implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id; // 商品id,主键
private String name; // 商品名称
private Double price; // 商品价格
private String remark; // 商品描述
// 商品和订单是多对多的关系,即一种商品可以包含在多个订单中
private List<Order> orders;
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id; // 订单id,主键
private String code; // 订单编号
private Double total; // 订单总金额
// 订单和用户是多对一的关系,即一个订单只属于一个用户
private User user;
// 订单和商品是多对多的关系,即一个订单可以包含多种商品
private List<Article> articles;
public class User implements Serializable{
private static final long serialVersionUID = 1L;
private Integer id; // 用户id,主键
private String username; // 用户名
private String loginname; // 登录名
private String password; // 密码
private String phone; // 联系电话
private String address; // 收货地址
// 用户和订单是一对多的关系,即一个用户可以有多个订单
private List<Order> orders;
剩下的同上
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="Mapper.OrderMapper">
<resultMap type="domain.Order" id="orderResultMap">
<id property="id" column="oid"/>
<result property="code" column="code"/>
<result property="total" column="total"/>
<!-- 多对一关联映射:association -->
<association property="user" javaType="domain.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="loginname" column="loginname"/>
<result property="password" column="password"/>
<result property="phone" column="phone"/>
<result property="address" column="address"/>
</association>
<!-- 多对多映射的关键:collection -->
<collection property="articles" javaType="ArrayList"
column="oid" ofType="domain.Article"
select="Mapper.ArticleMapper.selectArticleByOrderId"
fetchType="lazy">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="price" column="price"/>
<result property="remark" column="remark"/>
</collection>
</resultMap>
<!-- 注意,如果查询出来的列同名,例如tb_user表的id和tb_order表的id都是id,同名,需要使用别名区分 -->
<select id="selectOrderById" parameterType="int" resultMap="orderResultMap">
SELECT u.*,o.id AS oid,CODE,total,user_id
FROM tb_user3 u,tb_order o
WHERE u.id = o.user_id
AND o.id = #{id}
</select>
<!-- 根据userid查询订单 -->
<select id="selectOrderByUserId" parameterType="int" resultType="domain.Order">
SELECT * FROM tb_order WHERE user_id = #{id}
</select>
</mapper>
3、测试
小结:
1、关联-association
2、集合-collection
3、所以association是用于一对一和多对一,而collection是用于一对多的关系
4、JavaType和ofType都是用来指定对象类型的
JavaType是用来指定pojo中属性的类型
ofType指定的是映射到list集合属性中pojo的类型。
注意说明:
1、保证SQL的可读性,尽量通俗易懂
2、根据实际要求,尽量编写性能更高的SQL语句
3、注意属性名和字段不一致的问题
4、注意一对多和多对一 中:字段和属性对应的问题
5、通过日志来查看自己的错误