1.首先,我们先看看一个常见的博客页面的组成,如下:
a.页面上能够展示的部分:正文,标题,日期,作者,评论正文,评论时间,评论人等等
b.页面之外的部分:用户名,用户id,用户密码,用户基本信息(电话,邮箱,地址,兴趣,特长,等等)
2.将我们页面上的信息从数据库中查出来的SQL语句转化为Mapper文件中的语句,可能是如下内容:
- <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>
其对应着非常复杂的结果集合,Mapper文件可能长这个样子,如下:
-
- <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>
对于初学者而言,看到这样的一份XML文件,我想内心一定是崩溃的!但是,不要担心,我们日常开发,很少能够遇到这样的场景,并且,相信通过我们一步一步的解释这个配置文档,以后各位也能够运用自如。
在上面的resultMap中存在很多的子元素,下面我们来逐一解释:
“constructor”:类在实例化时,用来注入结果到构造方法中。
“idArg”:ID参数,标记结果作为ID,可以帮助提高整体的效率。
“arg”:注入到构造方法的一个不同结果。
“id”:这个id,类似于数据库的主键,能够帮助提高整体的效率
“result”:即结果字段,其中包括java对象的属性值,和数据库列名
“association”:复杂类型的结果关联,结果映射能够关联自身,或者关联另一个结果集
“collection”:复杂类型的集合,结果映射自身,或者映射结果集
“discriminator”:使用结果值来决定使用哪个结果映射
“case”:基于某些值的结果映射。嵌入结果映射,这种情形也映射到它本身,因此,能够包含相同的元素,或者参照一个外部的结果映射。
对于resultMap标签,上文的基础用法中我们已经介绍了他的属性含义。但,在此之外,还有一个属性值为:
“autoMapping”:如果出现此配置,Mybatis将会启用或者禁用自动匹配resultMap的功能,这个属性将会在全局范围内覆盖自动匹配机制。默认情况下是没有这个配置的,因此,如果需要,请保持慎重。
下面,我们开始详细说明每一个元素,如果有心急的读者想使用前面增改删查功能,请读者一定按照单元测试的方法推进,千万不要一次性配置大量属性,以免影响学习兴趣。
a.构造方法
- <constructor>
- <idArg column="id" javaType="int"/>
- <arg column="username" javaType="String"/>
- </constructor>
尽管对于大部分的DTO对象,以及我们的domain模型,属性值都是能够起到相应的作用,但是,在某些情况下如我们想使用一些固定的类。如:通常情况下的表格中包括一些仅供浏览的数据或者很少改变的数据。Mybatis的构造函数注入功能允许我们在类初始化时就设置某些值,而不暴露其中的public方法。同时Mybatis也支持私有的属性与私有的JavaBeans属性来实现这个目的,尽管这样,一些开发者还是更愿意使用构造函数注入的方式。
例如,程序中我们存在这样一个实体类,如下:
- public class User {
- //...
- public User(int id, String username) {
- //...
- }
- //...
- }
在Mybatis中,为了向这个构造方法中注入结果,Mybatis需要通过它的参数的类型来表示构造方法。java中,没有反射参数名称的方法,因此,当创建一个构造方法的元素是,必须保证参数是按照顺序排列的,而且,数据类型也必须匹配!
b.关联
- <association property="author" column="blog_author_id" javaType="Author">
- <id property="id" column="author_id"/>
- <result property="username" column="author_username"/>
- </association>
关联元素用来处理数据模型中的“has-one”关系。如我们上文示例的一个博客有一个用户。关联映射大部分是基于这种应用场景。使用时,我们制定目标属性,可以选用javaType,jdbcType,typeHandler等属性来覆盖结果集合。
关联查询的不同之处是,我们必须告诉告诉Mybatis如何加载关联关系,这里有两种供我们选择的方法:
嵌套查询:即通过执行另一个预期返回复杂类型的SQL语句。
嵌套结果:使用嵌套结果映射来处理联合结果中重复的子集。
在正式的使用之前,我们先来看看这个的属性配置的具体含义,注意,这里的属性配置跟前文基本增改删查中的区别:
属性 | 描述 |
property | 映射到列结果的字段或属性。如果匹配的是存在的,和给定名称相同的 property JavaBeans 的属性, 那么就会使用。 否则 MyBatis 将会寻找给定名称的字段。 这两种情形你可以使用通常点式的复杂属性导航。比如,可以这样映射 :“ username ”, 或 者 映 射 到 一 些 复 杂 的属性 : “address.street.number” 。 |
javaType | 一个 Java 类的完全限定名,或一个类型别名(参考上面内建类型别名的列 表) 。如果你映射到一个 JavaBean,MyBatis 通常可以断定类型。然而,如 javaType 果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证所需的行为。 |
jdbcType | 在这个表格之前的所支持的 JDBC 类型列表中的类型。JDBC 类型是仅仅 需要对插入, 更新和删除操作可能为空的列进行处理。这是 JDBC 的需要, jdbcType 而不是 MyBatis 的。如果你直接使用 JDBC 编程,你需要指定这个类型-但 仅仅对可能为空的值。 |
typeHandler | 我们在前面讨论过默认的类型处理器。使用这个属性,你可以覆盖默认的 typeHandler 类型处理器。 这个属性值是类的完全限定名或者是一个类型处理器的实现, 或者是类型别名。 |
-------------------------------------------------------------------------------------------------------------------------------------
现在正式的介绍这两种方式:
1.嵌套查询
属性 | 描述 |
column | 这是来自数据库的类名,或重命名的列标签的值作为一个输入参数传递给嵌套语句,这和通常传递给 resultSet.getString(columnName)方法的字符串是相同的。 注 意 : 要 处 理 复 合 主 键 , 你 可 以 指 定 多 个 列 名 通 过 column= ” {prop1=col1,prop2=col2} ” 这种语法来传递给嵌套查询语 句。这会引起 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句 |
select | 另外一个映射语句的 ID,将会按照属性的映射来加载复杂类型。获取的在列属性中指定的列的值将被传递给目标 select 语句作为参数。表格后面 有一个详细的示例。 注 意 : 要 处 理 复 合 主 键 , 可 以 通 过使用 column= ” {prop1=col1,prop2=col2} ” 这种语法指定多个列名传递给嵌套查询语句。这会导致 prop1 和 prop2 以参数对象形式来设置给目标嵌套查询语句。 |
fetchType | 可选的,他的有效值是lazy,eager。如果存在的话,他将在当前映射关系中取代全局变量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>
我们有两个查询语句:一个来加载博客,另外一个来加载作者,而且博客的结果映射描 述了“selectAuthor”语句应该被用来加载它的 author 属性。
其他所有的属性将会被自动加载,前提假设它们的列和属性名相匹配。
这种方式很简单, 但是对于大型数据集合和列表将不会表现很好。 问题就是我们熟知的 “N+1 查询问题”。概括地讲,N+1 查询问题可以是这样引起的:
- 执行了一个单独的 SQL 语句来获取结果列表(就是“+1”)。
- 对返回的每条记录,执行了一个查询语句来为每个加载细节(就是“N”)。
这个问题会导致成百上千的 SQL 语句被执行。这通常不是期望的。
MyBatis 能延迟加载这样的查询就是一个好处,因此你可以分散这些语句同时运行的消耗。然而,如果你加载一个列表,之后迅速迭代来访问嵌套的数据,你会调用所有的延迟加载,这样的行为可能是很糟糕的。
所以还有另外一种方法。
2.嵌套结果:
首先,我们先来看看有哪些属性能够供我们使用:
属性 | 描述 |
resultMap | 这是结果映射的 ID,可以映射关联的嵌套结果到一个合适的对象图中。这是一种替代方法来调用另外一个查询语句。这允许你联合多个表来合成到 resultMap 一个单独的结果集。这样的结果集可能包含需要被分解的相同的,重复的数据组并且合理映射到一个嵌套的对象图。为了使它变得容易,MyBatis 让你“链接”结果映射,来处理嵌套结果。下面给予一个很容易来仿照例子。 |
columnPrefix | 当连接多个表时,你最好使用列的别名来避免在一个结果集合中出现的名称重复。对于制定的前缀,Mybatis允许我们映射列到外部集合中,具体用法请参照后面的例子 |
notNullColumn | 只有在至少有一个非空列映射到子对象的属性时,才创建一个默认的子对象。通过这个属性,我们可以设置哪一个列必须有值来改变这个行为,此时的Mybatis就会按照这个非空设置来创建一个子对象。多个列存在时,可以通过逗号作为分割符。默认情况下,该属性是不会被设置的,即unset |
autoMapping | 如果存在此属性的话,Mybatis会在映射到对应属性时启用或者禁用自动映射的功能。这个属性将会在全局范围内覆盖自动映射的功能。 注意:该属性没有对外部结果集造成影响。因此,在select或者结果集合中使用是没有意义的。默认情况下,它是不设置的,即unset |
上面的内容是不是让各位已经头晕目眩了?不要急,我们马上就给大家展示一个非常简单的例子来说明上面各个属性是怎么工作的。
- <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>
在上面的这个例子中,我们已经观察到博客的“author”关联另外一个“resultMap”结果映射,来加载“Author”实例
特别注意的是:在嵌套结果映射中,元素“id”扮演了一个非常重要的角色。我们应该特别指明一个或者多个属性来唯一的标识这个结果。在实际应用中,如果不指定这个“id”,Mybatis仍然能够继续运行,但是会产生很大的性能消耗。但是,也需要尽可能的少的选择这些属性,数据库的主键显然是一个非常好的选择!
上面的例子使用了外部结果集元素来映射关联。这样的做法,使得id为“Authot”的结果集能够被不断的重用。但是,假如我们没有重用的需求,或者,我们只是想简单的把我们的结果映射到一个单独描述的结果集合当中的话,就不再需要上面的方式书写了,直接嵌套关联结果映射就好。具体的做法如下:
- <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 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>
其对应的结果集合,如下:
- <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”来重用这个结果集,以此来映射“联合作者”的查询结果。具体写法如下:
- <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>
多结果集关联
属性 | 描述 |
column | 当时用多结果集的这个属性来指定被逗号分隔的列时,将会使得该列与foreignColumn 相关联,从而确定了关联列之间的父子关系 |
foreignColumn | 标识出包含foreing keys的列的名称。这个foreing keys的值将会和父类型中指定的列属性的值相匹配 |
resultSet | 标识这个将会从哪里加载的复杂类型数据的结果集合的名称 |
从3.2.3版本开始,Mybatis提供了另外一种方式来解决“N+1”问题
某些数据库允许在存储过程中返回多个结果集,或者同时执行多个语句并且每一个都返回一个结果集合。这就使得我们可以只用访问数据库一次,且不用使用join,就返回存在关联的数据。
举个例子,执行下面的语句将会返回两个结果集合。第一个返回博客文章的结果集合,第二个返回作者的结果集合
- 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>
现在,我们来指定:数据填充的“author”的集合包含在“authors”的结果集中
- <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”(先这么叫吧)类似的问题,鉴于篇幅的关系,我们将在下文中进行介绍,敬请期待!