【MyBatis系列4】一對一,一對多,多對多查詢及延遲加載(N+1問題)分析 前言 固定參數的查詢 動態參數的查詢 一對一查詢 一對多查詢 多對多查詢 延遲加載(解決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執行流程,緩存,參數和結果集映射等功能的實現原理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章