前言
我们知道 Mybatis 的结果映射,如果遇到实体类的属性是自定义,事情就不会那么简单。可能会有一对多,多对一,多对多的情况产生。本文讲解一下多对一的情况。
多对一,指的是 A 表的多行对应 B 表的某一行,就是 A 的多个实例对应 B 的一个实例。比如我们有一张博客表,表里记录了作者,题目,时间,阅读量等信息。还有一张读者表。表里记录了作者姓名、地址等信息。 Blog 表中的多条记录,对应了 Author 表中的一条记录,这就是多对一。
多对一的结果集映射需要用到 <association> 元素,来表示一个类中的属性是一个复杂类。
下面我们先建两张表,来具体描述一下多对一的关系。
数据库环境
我们有两个 pojo 类,一个是博客类 Blog.java,另一个是作者类 Author.java。
一个 Blog 类 “有一个” Author,该类有以下属性:
public class Blog {
private String id;
private String title;
private Author author;
private String create_time;
}
Blog 类对应的数据表为(我这里特意让类的属性名和数据表的列名不同,为的是做结果映射的时候看让大家看的更清楚):
可以看到博客的作者是由 author_id (数字)表示的,一共有 4 位作者,每位作者都发表了至少一篇博客。
Author 类有以下属性:
public class Author {
private int id;
private String name;
private String address;
}
Author类对应的数据表为,这里记录的就不止是 Blog 表中出现的作者了,而是所有作者的信息,一共六位:
可以看到两张表是有关联的,可以通过作者的 id 建立联系,即 blog.author = author.id
多个 Blog 实体对应一个 Author 实体:
而且该问题复杂之处在哪里呢?在于 Blog 类的属性不全是基础类型,还包含了自定义的 Author 类,这使我们结果映射的时候要多做一些处理。
sql 操作
现在要求在 Blog 表中,传入作者 ID,查询储该作者写的所有博客的信息。
说到底,Mybatis 要操作的还是数据库,所以我们回归 MySQL,看看上面这个任务,用 SQL 怎么写。这个很简单,只需要执行:
select * from mybatis.blog where author_id = 315
就可以得到 author_id 为 315 的作者所有博客信息:
但是仅仅这样还不够,我想同时查出作者的信息,也就是在上面的结果后增加三列数据 id,name,address。这就涉及到多表查询了,两个表需要通过 blog.author_id = author.id 作为连接条件做查询。我有两种写法:
第一种写法——多表查询:
左连接查询
select
blog_id, blog_title, author_id, create_time, view_count, name, address
from Mybatis.Blog left join Mybatis.Author on Blog.author_id = Author.id
where Blog.author_id = 315;
或者也可以这样查询
select blog_id, blog_title, author_id, create_time, view_count, name, address
from mybatis.blog, mybatis.author
where author_id = id and author_id = 315;
第二种写法——嵌套select查询(性能糟糕):
# 先查询从 blog 表中查询 author_id = 315 的数据
select * from mybatis.blog where author_id = 315;
# 再从 author 数据表中查询 id = 315 的数据
select * from mybatis.author where id = 315;
# 将数据组合成最终数据
二者的查询结果是一样的:
嵌套select查询
select * from blog inner join author on (blog.author_id = author.id and author.id = 315);
应该怎么在 Mybatis 中操作该 sql 语句呢?
先在 BlogMapper 中定义查询接口:
public interface BlogMapper {
public List<Blog> queryBlogAndAuthorByAuthorID(int id);
}
再在 AuthorMapper 中定义查询接口:
public interface UserMapper {
public User selectUser(int id);
}
之后要在 BlogMapper.xml 文件中定义 sql 语句:
<select id="queryBlogAndAuthorByAuthorID" resultMap="BlogAuthor">
select * from mybatis.blog where author_id = #{id}
</select>
<resultMap id="BlogAuthor" type="Blog">
<result property="id" column="blog_id"></result>
<result property="title" column="blog_title"></result>
<result property="view" column="view_count"></result>
<association property="author" column="author_id" javaType="Author" select="selectAuthorById">
<result column="id" property="id" />
<result column="name" property="name" />
<result column="address" property="address" />
</association>
</resultMap>
<select id="selectAuthorById" resultType="Author">
select * from mybatis.author where id=#{id}
</select>
之后调用接口就可以完成查询,结果正是我们想要的:
这种方式在大型数据集和大型数据表上表现不佳。该问题被称为 “N + 1” 查询问题:
- 你执行了一个单独的 SQL 语句来获取结果的一个列表 (“+1”)
- 对列表返回的每条记录,你执行了一个 select 查询语句来为每条记录加载详细信息(“N”)
在这个例子中:
- 我们执行了一个单独的 SQL:“selectBlog”,先获得了一个Blog 列表 (“+1”)
- 对于列表 Blog 返回的每条记录,又执行了 “selectAuthor” 来为每条 blog 记录加载 Author 的详细信息(“N”)
来解释一下刚才发生了什么事。
我们有两个 select 查询语句:一个用来加载 Blog,一个用来加载 Author,而且博客的结果映射描述了应该使用 selectAuthorById 语句加载 它的 author 属性,就是这句:
<association property="author" column="author_id" javaType="Author" select="selectAuthorById">
用到的几个 association 属性的描述如下:
属性 | 描述 |
---|---|
property | 属性名:列结果===》实体类的属性名 |
column | 列名:实体类的属性名===》列名 |
javaType | 该属性的 Java 类型 |
select | 加载复杂类型属性的映射语句的 ID |
对于 Blog 类型,将 author 属性映射到了 Blog 数据表的 author_id 列中,由于 Author 对象不止有 author_id,这个属性,还有 name, address 属性,所以加载了复杂类型属性之后,还要在属性(Author)做结果映射:
<association property="author" column="author_id" javaType="Author" select="selectAuthorById">
<result column="id" property="id" />
<result column="name" property="name" />
<result column="address" property="address" />
</association>
因为 Author 的列名和属性名匹配,可以被自动加载,故上述语句可以简写为:
<association property="author" column="author_id" javaType="Author" select="selectAuthorById" />
扩展
假设我们现在要查询整个 Blog 表中的数据,每一行中增加作者的全部信息,也就是说,在 Blog 表后面增加作者数据三列数据 id, name, address。这回比起前面的需求,不要求搜索特定的作者了,只需要稍加改动即可。
先在 BlogMapper 中定义接口:
public interface BlogMapper {
public List<Blog> queryBlog();
}
再在AuthorMapper 中定义接口:
public interface UserMapper {
public User selectUser(int id);
}
之后在 BlogMapper 中定义 sql 语句:
<select id="queryBlog" resultMap="BlogAuthor">
select * from mybatis.blog
</select>
<resultMap id="BlogAuthor" type="Blog">
<result property="id" column="blog_id"></result>
<result property="title" column="blog_title"></result>
<result property="view" column="view_count"></result>
<association property="author" column="author_id" javaType="Author" select="selectAuthorById"></association>
</resultMap>
<select id="selectAuthorById" resultType="Author">
select * from mybatis.author where id=#{id}
</select>
查询结果如下:
关联的嵌套结果映射
我觉得这个 SQL 语句是比较符合直觉的,比较好写:
select
blog_id, blog_title, author_id, create_time, view_count, name, address
from Mybatis.Blog left join Mybatis.Author on Blog.author_id = Author.id
where Blog.author_id = 315;
就是拿 Blog 表作为驱动表,用 Blog.author_id = Author.id 作为连接条件,找到 Blog.author_id = #{id} 的 Blog + Author 数据。怎么在 Mybatis 中完成上述操作呢?
将 Blog 表和 Author 表连接在一起,而不是执行一个独立的查询语句,就像这样:
<select id="queryBlogAndAuthorByAuthorID" resultMap="BlogResult">
select
blog_id, blog_title, author_id, create_time, view_count, name, address
from Mybatis.Blog left join Mybatis.Author on Blog.author_id = Author.id
where Blog.author_id = #{id}
</select>
之后结果映射:
<resultMap id="BlogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title" />
<result property="create_time" column="create_time" />
<result property="view" column="view_count" />
<association property="author" column="author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id" />
<id property="name" column="name"/>
<id property="address" column="address"/>
</resultMap>
调用接口即可完成查询:
这个结果映射很简单,就是将结果映射到 Blog 类型,再将 Blog 属性的复杂类型 Author 映射到 author 数据表中。使用 assicia tion 元素来映射:
<association property="author" column="author_id" javaType="Author" resultMap="authorResult"/>
association 中使用到了 resultMap 属性,再次做结果映射,把 author 的返回结果映射到 authorResult 中,而 authorResult 其实就是 Author 类和 author 表之间的映射:
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id" />
<id property="name" column="name"/>
<id property="address" column="address"/>
</resultMap>
该结果映射可以写为:
<resultMap id="BlogResult" type="Blog">
<id property="id" column="blog_id" />
<result property="title" column="blog_title" />
<result property="create_time" column="create_time" />
<result property="view" column="view_count" />
<association property="author" javaType="Author">
<id property="id" column="author_id" />
<id property="name" column="name"/>
<id property="address" column="address"/>
</association>
</resultMap>
同理,上述的结果映射,如果使用下面的 SQL,查询效果是一样的:
<select id="queryBlogAndAuthorByAuthorID" resultMap="BlogResult">
select blog_id, blog_title, author_id, create_time, view_count, name, address
from Mybatis.Blog, Author
where Blog.author_id = Author.id and Blog.author_id = #{id}
</select>
查询结果如下: