【MyBatis系列4】一对一,一对多,多对多查询及延迟加载(N+1问题)分析

前言

上一篇分析了MyBatis中的配置的使用,而MyBatis中动态标签功能也非常强大,本文不会介绍全部标签,主要是针对resultMap来介绍复杂查询该如何利用sql标签来配置动态sql。

固定参数的查询

首先我们来看一个带有固定参数的查询语句该如何实现:
UserMapper.java中新增如下两个方法:

 List<LwUser> listUserByUserName(@Param("userName") String userName);

 List<LwUser> listUserByTable(@Param("tableName") String tableName);

对应UserMapper.xml中的sql语句为:

  <select id="listUserByUserName" resultType="lwUser">
        select user_id,user_name from lw_user where user_name=#{userName}
    </select>

    <select id="listUserByTable" resultType="lwUser">
        select user_id,user_name from ${tableName}
    </select>

然后执行查询:

package com.lonelyWolf.mybatis;

import com.alibaba.fastjson.JSONObject;
import com.lonelyWolf.mybatis.mapper.UserMapper;
import com.lonelyWolf.mybatis.model.LwUser;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class MyBatisQueryByParam {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        //读取mybatis-config配置文件
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //创建SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //创建SqlSession对象
        SqlSession session = sqlSessionFactory.openSession();

        /**
         * 相比较于session.selectList("com.xxx.UserMapper.listAllUser")来实现查询,
         * 下面这种通过先获取mapper再挑用mapper中方法的方式会更灵活
         */
        UserMapper userMapper = session.getMapper(UserMapper.class);
        List<LwUser> userList = userMapper.listUserByUserName("孤狼1号");

        System.out.println(null == userList ? "": JSONObject.toJSONString(userList));

        List<LwUser> userList2 = userMapper.listUserByTable("lw_user");
        System.out.println(null == userList2 ? "": JSONObject.toJSONString(userList2));
    }
}

查询结果输出如下:


在这里插入图片描述

#和$区别

从上面的输出sql语句截图可以看到,如果使用#的话,那么sql语句会先在sql语句中使用占位符,也就是预编译,对应JBDC中的PreparedStatement。而使用$,则会直接把参数拼到sql语句上,相当于JDBC中的Statement。

一般情况下不建议使用$,因为这种直接拼接的方式容易被sql注入攻击。
比如,上面的sql语句:

select user_id,user_name from ${tableName}

假如tableName传入的是:lw_user;delete from lw_user;那么这时候执行的sql语句就会变成:

select user_id,user_name from lw_user;delete from lw_user;

这时候整张表的数据都会被删除,而如果使用的是#{tableName},最终执行的是如下sql:

select user_id,user_name from 'lw_user;delete from lw_user;'

产生的后果只是查询了一张不存在的表而已。

动态参数的查询

上面的例子中参数是固定的,那么假如我们参数不固定呢?比如有2个参数,但是我可能一个都不用,也可能只用1个,或者2个都用。这种又该如何实现呢?
如下图所示,可以通过where和if标签结合使用,两个条件都写了and,这是因为Mybatis会帮我们处理掉多余的and关键字。

<select id="list" parameterType="com.lonelyWolf.mybatis.model.LwUser" resultType="lwUser">
        select user_id,user_name from lw_user
        <where>
            <if test="userId !=null and userId !=''">
                and user_id=#{userId}
            </if>
            <if test="userName !=null and userName !=''">
                and user_name=#{userName}
            </if>
        </where>
    </select>

或者说我们对同一个参数需要进行不同取值拼接不同的sql,那么可以通过choose标签根据不同的参数拼接不同的sql

 select user_id,user_name from lw_user
        <where>
            <choose>
                <when test="userId ='1'">
                    and user_id=#{userId}
                </when>
                <when test="userId='2'">
                    and user_id=#{userId}
                </when>
                <otherwise>
                    and user_id=#{userId}
                </otherwise>
            </choose>
        </where>
    </select>

当然,Mybatis还提供了其他许多标签,用来处理更加复杂的组合,在这里就不举例说明了。

一对一查询

假如我们现在有两种表,是一对一关系,我们想同时查询出来,当然最简单的办法是再写一个类,把两张表的结果属性都放到一个类里面,但是这种方式无疑会造成了很多重复代码,而且体现不出层级关系,假如我们有一张表lw_user表,存储用户信息,另一张表lw_user_job存储了用户的工作经历,那么很明显,job对应类应该包含在user类内,这种应该怎么实现呢?

请看!

1、新建一个实体类UserJob来映射lw_user_job表属性:

package com.lonelyWolf.mybatis.model;

public class LwUserJob {
    private String id;
    private String userId; //用户id
    private String companyName; //公司名
    private String position; //职位

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getCompanyName() {
        return companyName;
    }

    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }

    public String getPosition() {
        return position;
    }

    public void setPosition(String position) {
        this.position = position;
    }
}

2、在原先的LwUser类增加一个引用属性来引用LwUserJob:

package com.lonelyWolf.mybatis.model;

public class LwUser {
    private String userId; //用户id
    private String userName; //用户名称

    private LwUserJob usreJobInfo;//用户工作信息

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public LwUserJob getUsreJobInfo() {
        return usreJobInfo;
    }

    public void setUsreJobInfo(LwUserJob usreJobInfo) {
        this.usreJobInfo = usreJobInfo;
    }
}

3、UserMapper.java中新增一个方法:

List<LwUser> listUserAndJob();

这时候UserMapper.xml需要自定义一个ResultMap:

<resultMap id="JobResultMap" type="lwUser">
        <result column="user_id" property="userId" jdbcType="VARCHAR" />
        <result column="user_name" property="userName" jdbcType="VARCHAR" />
        <!--这里的JavaType也可以定义别名;property对应LwUser类中的属性名 -->
        <association property="userJobInfo" javaType="com.lonelyWolf.mybatis.model.LwUserJob">
            <result column="id" property="id" jdbcType="VARCHAR" />
            <result column="company_Name" property="companyName" jdbcType="VARCHAR" />
            <result column="position" property="position" jdbcType="VARCHAR" />
        </association>
    </resultMap>

<!-- 这里需要修改为resultMap-->
 <select id="listUserAndJob" resultMap="JobResultMap">
        select * from lw_user u inner join lw_user_job j on u.user_id=j.user_id
    </select>

这时候执行查询就可以得到如下结果:

[{"userId":"1","userJobInfo":{"companyName":"自由职业","id":"11","position":"初级开发"},"userName":"孤狼1号"}]

一对多查询

假设用户信息和工作信息时1对多的关系,又该如何呢?
只需做2步简单的改造:
1、将LwUser中引用属性改为List:

private List<LwUserJob> userJobList;

2、Mapper中的ResultMap文件同时做出修改,通过collection标签代替association标签,同时javaType修改为ofType:

 <collection property="userJobList" ofType="com.lonelyWolf.mybatis.model.LwUserJob">
            <result column="id" property="id" jdbcType="VARCHAR" />
            <result column="company_Name" property="companyName" jdbcType="VARCHAR" />
            <result column="position" property="position" jdbcType="VARCHAR" />
        </collection>

再次执行查询得到如下结果:

[{"userId":"1","userJobList":[{"companyName":"自由职业","id":"11","position":"初级开发"}],"userName":"孤狼1号"}]

可以看到这时候的userJobList已经是一个数组了。

PS:记得之前有人问过属性的映射是不是必须把表里面所有的属性都映射才行,答案是否定的,需要几个就映射几个,不需要完全映射过来。

多对多查询

多对多其实和一对多差不多的原理,都是利用collection标签,就是在collection标签里面再嵌套collection标签就可以实现多对多的查询,在这里就不在举例了。

延迟加载(解决N+1问题)

我们先来看一下一对多的另一种写法,就是支持一种嵌套查询:

 <resultMap id="JobResultMap2" type="lwUser">
        <result column="user_id" property="userId" jdbcType="VARCHAR" />
        <result column="user_name" property="userName" jdbcType="VARCHAR" />

        <collection property="userJobList" ofType="com.lonelyWolf.mybatis.model.LwUserJob" column="user_id" select="selectJob">
        </collection>
    </resultMap>

<!-- 外部查询-->
  <select id="selectUserAndJob" resultMap="JobResultMap2">
        select * from lw_user
    </select>

<!-- 嵌套查询-->
    <select id="selectJob" resultType="com.lonelyWolf.mybatis.model.LwUserJob">
        select * from lw_user_job where user_id=#{userId}
    </select>

上面的collection内部并没有定义属性,但是collection上面定义了两个标签,代表的含义是将当前查询结果的值user_id传递到查询selectJob中去。我们定义方法来执行一下这个外部查询selectUserAndJob看看会有什么结果:


在这里插入图片描述

可以看到外部查询有几条数据就会触发内部查询几次,这就是嵌套查询引发的N+1问题。(使用association标签也会存在这个问题)

这种在对性能要求比较高的场景中是不允许的,非常的浪费资源,MyBatis官方也不建议我们使用这种方式。

解决N+1问题

MyBatis虽然不建议我们使用这种嵌套查询,但是也提供了一种解决N+1问题的方式,那就是当我们执行外部查询的时候不会触发内部查询,仅仅当我们使用到了内部对象的时候才会触发内部查询来获取对应结果,这种就叫延迟加载

延迟加载需要通过全局属性来控制,默认是关闭的。
我们再mybatis-config.xml文件中开启延迟加载试试:

<setting name="lazyLoadingEnabled" value="true"/>

然后我们再来执行如下语句:

List<LwUser> userList = userMapper.selectUserAndJob();
        System.out.println(userList.size());//不会触发
        System.out.println(userList.get(0).getUserJobList());//触发
        System.out.println(null == userList ? "": JSONObject.toJSONString(userList));//触发

输出结果为:


在这里插入图片描述

延迟加载原理

延迟加载其实就是利用了动态代理来生成一个新的对象,默认用的是Javassist动态代理,可以通过参数来控制,支持切换为CGLIB:

<setting name="proxyFactory" value="CGLIB" />

总结

本文主要讲述了如何利用MyBatis实现一对一,一对多以及多对多的查询,并且讲解了如何利用延迟加载来解决嵌套查询中的N+1问题。MyBatis系列中前两篇相对基础,并没有深入分析实现原理,仅仅只是讲解了如何使用,下一篇开始,将会深入分析MyBatis的源码以及一些高级特性比如sqlSession执行流程,缓存,参数和结果集映射等功能的实现原理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342