Mybatis<collection>使用方法与N+1查询问题

目录

Mybatis标签嵌套Sql语句/嵌套结果映射

N+1查询问题

懒加载

Sql关键字In

ResultSet


Mybatis<association>标签嵌套Sql语句/嵌套结果映射

Mybatis的结果映射(resultMap)功能真是太强了,其能够帮助你有效率地将Sql查询结果转换为对应的Bean实体。

为了能够将复杂的Sql语句的查询结果映射至对应的Bean实体(可能是多个),需要使用MyBatis中的<association>标签和<collection>标签。

同时上述两个标签支持嵌套select查询或是嵌套resultMap查询。

正如文档所说:

  • 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
  • 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。

到底选择嵌套哪种取决于你采用的SQL语句: 如果你构造的是一句复杂的,带有Join关键字以连接多张表的Sql语句,那么应该嵌套结果映射。如果你想通过执行另一个SQL映射语句来加载另一张表中的关联信息,那么应该嵌套Select查询。

下面是一个简单的例子:

通用部分:

#desc book
id	int(11)	NO	PRI		auto_increment
name	varchar(128)	YES			
author	varchar(64)	YES			

#desc book_comment
id	int(11)	NO	PRI		auto_increment
book_id	int(11)	YES	MUL		
comment	varchar(255)	YES			
public class Book {
    private Integer id;
    private String name;
    private String author;
    //省略getter/setter
}

public class Comment {
    private String bookId;
    private Integer id;
    private String content;
    //省略getter/setter
}
# Model 层

# Service 层
@Service
public class BookService {
    @Autowired
    BookMapper bookMapper;
    
    //....
    public HashMap getBookById(Integer id) {
        return bookMapper.getBookById(id);
    }
    //.....
}

如果是“嵌套Select”语句写法,Mybatis的xml文件应该这样写。主要关注collection标签中的column属性,其作用是将"bookResultMap"中的列"id"的值传递给"selectCommentsForBook"中,进而构造了另一个Sql映射语句,来获取对应id的book的comments。

<select id="getBookById" parameterType="int" resultMap="bookResultMap">
        select
        * from book
        where id = #{id}
    </select>

    <select id="selectCommentsForBook" resultMap="commentResult">
        SELECT * FROM book_comment WHERE book_id = #{id}
    </select>

    <resultMap id="commentResult" type="org.sang.model.Comment">
        <id property="id" column="id"/>
        <result property="bookId" column="book_id"/>
        <result property="content" column="comment"/>
    </resultMap>

    <resultMap id="bookResultMap" type="hashmap">
        <id property="id" column="id" />
        <result property="name" column="name"/>
        <result property="author" column="author"/>
        <collection property="comments" javaType="ArrayList" ofType="org.sang.model.Comment" column="id" select="selectCommentsForBook"/>
    </resultMap>

如果是“嵌套映射结果”写法,则需要先构造一个Sql语句连接两张表。在结果映射中,Collection标签不需要嵌套Select语句,只需嵌套Comment类对应的结果映射:

<select id="getBookById" parameterType="int" resultMap="bookResultMap">
        select
        B.id as book_id,
        B.name as book_name,
        B.author as book_author,
        A.Id as comment_id,
        A.book_id as comment_book_id,
        A.Comment as comment_comment
        from Book B left outer join book_comment A on B.id=A.book_id
        where B.id = #{id}
    </select>

    <resultMap id="bookResultMap" type="hashmap">
        <id property="id" column="book_id" />
        <result property="name" column="book_name"/>
        <result property="author" column="book_author"/>
        <collection property="comments" javaType="ArrayList" ofType="org.sang.model.Comment">
            <id property="id" column="comment_id"/>
            <result property="bookId" column="comment_book_id"/>
            <result property="content" column="comment_comment"/>
        </collection>
    </resultMap>

N+1查询问题

Mybatis文档中指出:

虽然嵌套select的写法(对应上文的第一种写法)易于理解,但是它在大型数据库上表现不佳,造成这种现象的原因是N+1查询问题:

  • 你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”)。
  • 对列表返回的每条记录,你执行一个 select 查询语句来为每条记录加载详细信息(就是“N”)。

拿上面例子具体来讲的话,就是会先执行一条Sql语句获取满足条件的书本的列表,再对列表中的每个书本执行select查询选取对应的评论。虽说利用索引机制,这些Sql语句的执行耗费不会太大,但文档的观点是“这会导致成百上千的 SQL 语句被执行,在有些时候是不被希望的”。

为了对N+1查询问题的严重性有进一步了解,我查找相关资料。结果一搜发现这是个老生常谈的问题了。

解决方法包括:(1)在Sql语句中使用join语句连接多张表并进行查询,并使用上文的第一种方法构造结果映射,这里就不多提了;(2)使用懒加载技术,延迟“N查询”部分中各操作被执行的时间节点;(3)合并N查询为一个查询,通过使用Sql的关键字In可以达成这一点;(4)使用Mybatis3.2.3版本后的新标签ResultSets

懒加载

为了实验怎样的写法可以使懒加载生效,首先需要修改配置文件允许Mybatis输出日志文件,这一步骤在此省略。

首先原封不动,按照上文的写法,从Debug消息可看出N+1次执行均会被即时执行。

org.sang.mapper.BookMapper.getBookById   : ==>  Preparing: select * from book where id = ? 
o.s.m.BookMapper.selectCommentsForBook   : ====>  Preparing: SELECT * FROM book_comment WHERE book_id = ? 

接下来测试Lazy加载方式的有效性,即在<collection>标签中添加fetchType属性并赋值为lazy,测试函数被修改为如下样式:

@Test
	public void contextLoads() {
		HashMap hMap = (bookService.getBookById(1));
		System.out.println("I am going to visit comment instance for now.");
		System.out.println(hMap);
	}

 消息日志中的输出表明,Lazy加载方式确实是在用户需要访问Comment数据时,才会执行相应的Select语句。

org.sang.mapper.BookMapper.getBookById   : ==>  Preparing: select * from book where id = ? 
//.......
I am going to visit comment instance for now.
//.......
o.s.m.BookMapper.selectCommentsForBook   : ==>  Preparing: SELECT * FROM book_comment WHERE book_id = ? 
{comments=[Comment{bookId='1', id=2, content='Not bad'}], author=鲁迅, name=朝花夕拾, id=1}

但正如文档所说,“如果你加载记录列表之后立刻就遍历列表以获取嵌套的数据,就会触发所有的延迟加载查询,性能可能会变得很糟糕” ,在这种情况下延时加载等于没有发挥用场。

Sql关键字In

这个方法的思路很简单,使用In关键字将第一次查询获取的列表并入Sql查询语句即可。

explain select * from test_user  where username in ('username_1','username_2','username_3')

在构建对应索引的情况下该操作的花费不会太大。

下面链接中的文章对于In的执行效率进行了探讨,并将其与临时表写法的效率进行比较。(临时表的思路也很简单,就是为需要查询的Id的集合新建一张表,拿这张新表和对应的原有表进行连接)

得出的结论是在结果条目数更大的场合下,临时表写法效率更优。 

https://blog.youkuaiyun.com/LJFPHP/article/details/103656726

ResultSet

某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。

使用ResultSet的前提条件是数据库允许存储过程返回多个结果集或一次性执行多个语句&返回多个结果集,因此该方法不具备通用性,这里只是简单带过。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值