前言

查询是数据库中使用频率最高的功能,在MyBatis中想要执行查询,需要在映射文件中配置<select>标签来编写查询SQL。光有查询还不够,还得完成查询结果与实体对象的映射,MyBatis提供了<resultMap>标签来提供强大的结果映射功能。

本篇文章将结合示例对<select>和<resultMap>标签进行学习,并对N+1问题进行分析。

MyBatis版本:3.5.6

正文

一. 示例工程搭建

首先通过如下SQL语句创建表,并插入数据。

CREATE TABLE people (
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    p_name VARCHAR(255) NOT NULL,
    p_age INT(11) NOT NULL
);

INSERT INTO people(p_name, p_age) VALUES ("Lee", 20);
INSERT INTO people(p_name, p_age) VALUES ("Arm", 25);

CREATE TABLE bookstore(
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    bs_name VARCHAR(255) NOT NULL,
    p_id INT(11) NOT NULL,
    FOREIGN KEY bookstore(p_id) REFERENCES people(id)
);

INSERT INTO bookstore (bs_name, p_id) VALUES ("XinHua", 1);
INSERT INTO bookstore (bs_name, p_id) VALUES ("SanYou", 2);

CREATE TABLE book(
    id INT(11) PRIMARY KEY AUTO_INCREMENT,
    b_name VARCHAR(255) NOT NULL,
    b_price FLOAT NOT NULL,
    bs_id INT(11) NOT NULL,
    FOREIGN KEY book(bs_id) REFERENCES bookstore(id)
);

INSERT INTO book (b_name, b_price, bs_id) VALUES ("Math", 20.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("English", 21.5, 1);
INSERT INTO book (b_name, b_price, bs_id) VALUES ("Water Margin", 30.5, 2)
复制代码

工程结构如下所示。

mybatis resultMap设置list属性 mybatis resultmap int_sql

MAVENpom文件内容如下所示。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lee.learn.mybatis</groupId>
    <artifactId>mybatis-select</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.0</version>
        </dependency>
    </dependencies>

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>

</project>
复制代码

配置文件mybatis-config.xml如下所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://192.168.101.7:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false"/>
                <property name="username" value="somdbdev"/>
                <property name="password" value="Km7HZP#aB"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.lee.learn.mybatis.dao"/>
    </mappers>
</configuration>
复制代码

实体对象如下所示。

public class Book {

    private int id;
    private String bookName;
    private float bookPrice;
    private int bookStoreId;

    // 省略get和set

}

public class BookStore {

    private int id;
    private String bookStoreName;
    private int peopleId;

    // 省略get和set

}

public class People {

    private int id;
    private String peopleName;
    private int peopleAge;

    // 省略get和set

}

public class BsBookCombine {

    private int id;
    private String bookStoreName;

    private List<Book> books;

    // 省略get和set

}

public class BsPeopleCombine {

    private int id;
    private String bookStoreName;

    private People boss;

    // 省略get和set

}

public class BsBkPeopleCombine {

    private int id;
    private String bookStoreName;

    private People boss;

    private List<Book> books;

    // 省略get和set

}
复制代码

映射接口BookMapper如下所示。

public interface BookMapper {

    /**
     * 根据书Id查询一本书。
     *
     * @param bookId 书Id。
     * @return {@link Book}。
     */
    Book queryBookByBookId(int bookId);

    /**
     * 根据书店Id查询多本书。
     *
     * @param bsId 书店Id。
     * @return {@link Book}。
     */
    List<Book> queryBooksByBsId(int bsId);

}
复制代码

映射文件BookMapper.xml如下所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.mybatis.dao.BookMapper">
    <resultMap id="bookResultMap" type="com.lee.learn.mybatis.entity.Book">
        <id property="id" column="id"/>
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
        <result property="bookStoreId" column="bs_id"/>
    </resultMap>

    <select id="queryBookByBookId" resultMap="bookResultMap">
        SELECT
            id,
            b_name,
            b_price,
            bs_id
        FROM book
        WHERE id=#{bookId}
    </select>

    <select id="queryBooksByBsId" resultMap="bookResultMap">
        SELECT
            id,
            b_name,
            b_price,
            bs_id
        FROM book
        WHERE bs_id=#{bsId}
    </select>

</mapper>
复制代码

映射接口PeopleMapper如下所示。

public interface PeopleMapper {

    /**
     * 根据人Id查询一个人。
     *
     * @param peopleId 人Id。
     * @return 以Map的形式返回。
     */
    Map<String, Object> simpleQueryPeopleByPeopleId(int peopleId);

    /**
     * 根据人Id查询一个人。
     *
     * @param peopleId 人id。
     * @return {@link People}。
     */
    People queryPeopleByPeopleId(int peopleId);

}
复制代码

映射文件PeopleMapper.xml如下所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.mybatis.dao.PeopleMapper">
    <resultMap id="peopleResultMap" type="com.lee.learn.mybatis.entity.People">
        <id property="id" column="id"/>
        <result property="peopleName" column="p_name"/>
        <result property="peopleAge" column="p_age"/>
    </resultMap>

    <select id="simpleQueryPeopleByPeopleId" resultType="Map">
        SELECT
            id,
            p_name,
            p_age
        FROM people
        WHERE id=#{id}
    </select>

    <select id="queryPeopleByPeopleId" resultMap="peopleResultMap">
        SELECT
            id,
            p_name,
            p_age
        FROM people
        WHERE id=#{id}
    </select>

</mapper>
复制代码

映射接口AssociatedMapper如下所示。

public interface AssociatedMapper {

    /**
     * 根据书店Id 关联的嵌套Select 查询{@link BsPeopleCombine}。
     *
     * @param bsId 书店Id。
     * @return {@link BsPeopleCombine}。
     */
    BsPeopleCombine queryBsPeopleCombineByBsIdAssociatedNestSelect(int bsId);

    /**
     * 根据书店Id 关联的嵌套结果 查询{@link BsPeopleCombine}。
     *
     * @param bsId 书店Id。
     * @return {@link BsPeopleCombine}。
     */
    BsPeopleCombine queryBsPeopleCombineByBsIdAssociatedNestResult(int bsId);

    /**
     * 根据书店Id 集合的嵌套Select 查询{@link BsBookCombine}。
     *
     * @param bsId 书店Id。
     * @return {@link BsBookCombine}。
     */
    BsBookCombine queryBsBookCombineByBsIdCollectionNestSelect(int bsId);

    /**
     * 根据书店Id 集合的嵌套结果 查询{@link BsBookCombine}。
     *
     * @param bsId 书店Id。
     * @return {@link BsBookCombine}。
     */
    BsBookCombine queryBsBookCombineByBsIdCollectionNestResult(int bsId);

    /**
     * 根据书店Id 嵌套Select 查询{@link BsBkPeopleCombine}。
     *
     * @param bsIs 书店Id。
     * @return {@link BsBkPeopleCombine}。
     */
    BsBkPeopleCombine queryBsBkPeopleCombineByBsIdNestSelect(int bsIs);

    /**
     * 根据书店Id 嵌套结果 查询{@link BsBkPeopleCombine}。
     *
     * @param bsIs 书店Id。
     * @return {@link BsBkPeopleCombine}。
     */
    BsBkPeopleCombine queryBsBkPeopleCombineByBsIdNestResult(int bsIs);

}
复制代码

映射文件AssociatedMapper.xml如下所示。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.mybatis.dao.AssociatedMapper">
    <resultMap id="bsPeopleCombineQueryMap" type="com.lee.learn.mybatis.entity.BsPeopleCombine">
        <id property="id" column="id"/>
        <result property="bookStoreName" column="bs_name"/>
        <association property="boss" column="p_id"
                     javaType="com.lee.learn.mybatis.entity.People"
                     select="com.lee.learn.mybatis.dao.PeopleMapper.queryPeopleByPeopleId"/>
    </resultMap>

    <resultMap id="bsPeopleCombineResultMap" type="com.lee.learn.mybatis.entity.BsPeopleCombine">
        <id property="id" column="bs_id"/>
        <result property="bookStoreName" column="bs_name"/>
        <association property="boss" javaType="com.lee.learn.mybatis.entity.People">
            <id property="id" column="p_id"/>
            <result property="peopleName" column="p_name"/>
            <result property="peopleAge" column="p_age"/>
        </association>
    </resultMap>

    <resultMap id="bsBookCombineQueryMap" type="com.lee.learn.mybatis.entity.BsBookCombine">
        <id property="id" column="id"/>
        <result property="bookStoreName" column="bs_name"/>
        <collection property="books" column="id"
                    javaType="ArrayList" ofType="com.lee.learn.mybatis.entity.Book"
                    select="com.lee.learn.mybatis.dao.BookMapper.queryBooksByBsId"/>
    </resultMap>

    <resultMap id="bsBookCombineResultMap" type="com.lee.learn.mybatis.entity.BsBookCombine">
        <id property="id" column="bs_Id"/>
        <result property="bookStoreName" column="bs_name"/>
        <collection property="books" ofType="com.lee.learn.mybatis.entity.Book">
            <id property="id" column="b_id"/>
            <result property="bookName" column="b_name"/>
            <result property="bookPrice" column="b_price"/>
            <result property="bookStoreId" column="bbs_id"/>
        </collection>
    </resultMap>

    <resultMap id="BsBkPeopleCombineQueryMap" type="com.lee.learn.mybatis.entity.BsBkPeopleCombine">
        <id property="id" column="id"/>
        <result property="bookStoreName" column="bs_name"/>
        <association property="boss" column="p_id"
                     javaType="com.lee.learn.mybatis.entity.People"
                     select="com.lee.learn.mybatis.dao.PeopleMapper.queryPeopleByPeopleId"/>
        <collection property="books" column="id"
                    ofType="com.lee.learn.mybatis.entity.Book"
                    select="com.lee.learn.mybatis.dao.BookMapper.queryBooksByBsId"/>
    </resultMap>
    
    <resultMap id="BsBkPeopleCombineResultMap" type="com.lee.learn.mybatis.entity.BsBkPeopleCombine">
        <id property="id" column="bs_id"/>
        <result property="bookStoreName" column="bs_name"/>
        <association property="boss" javaType="com.lee.learn.mybatis.entity.People">
            <id property="id" column="p_id"/>
            <result property="peopleName" column="p_name"/>
            <result property="peopleAge" column="p_age"/>
        </association>
        <collection property="books" ofType="com.lee.learn.mybatis.entity.Book">
            <id property="id" column="b_id"/>
            <result property="bookName" column="b_name"/>
            <result property="bookPrice" column="b_price"/>
            <result property="bookStoreId" column="bbs_id"/>
        </collection>
    </resultMap>

    <select id="queryBsPeopleCombineByBsIdAssociatedNestSelect" resultMap="bsPeopleCombineQueryMap">
        SELECT
            id,
            bs_name,
            p_id
        FROM bookstore
        WHERE id=#{bsId}
    </select>

    <select id="queryBsPeopleCombineByBsIdAssociatedNestResult" resultMap="bsPeopleCombineResultMap">
        SELECT
            bs.id AS bs_id,
            bs.bs_name AS bs_name,
            p.id AS p_id,
            p.p_name AS p_name,
            p.p_age AS p_age
        FROM bookstore bs
            LEFT OUTER JOIN people p ON bs.p_id=p.id
        WHERE bs.id=#{bsId}
    </select>

    <select id="queryBsBookCombineByBsIdCollectionNestSelect" resultMap="bsBookCombineQueryMap">
        SELECT
            id,
            bs_name
        FROM bookstore
        WHERE id=#{bsId}
    </select>

    <select id="queryBsBookCombineByBsIdCollectionNestResult" resultMap="bsBookCombineResultMap">
        SELECT
            bs.id AS bs_Id,
            bs.bs_name AS bs_name,
            b.id AS b_id,
            b.b_name AS b_name,
            b.b_price AS b_price,
            b.bs_id AS bbs_id
        FROM bookstore bs
            LEFT OUTER JOIN book b ON bs.id=b.bs_id
        WHERE bs.id=#{bsId}
    </select>

    <select id="queryBsBkPeopleCombineByBsIdNestSelect" resultMap="BsBkPeopleCombineQueryMap">
        SELECT
            id,
            bs_name,
            p_id
        FROM bookstore
        WHERE id=#{bsId}
    </select>

    <select id="queryBsBkPeopleCombineByBsIdNestResult" resultMap="BsBkPeopleCombineResultMap">
        SELECT
            bs.id AS bs_id,
            bs.bs_name AS bs_name,
            p.id AS p_id,
            p.p_name AS p_name,
            p.p_age AS p_age,
            b.id AS b_id,
            b.b_name AS b_name,
            b.b_price AS b_price,
            b.bs_id AS bbs_id
        FROM bookstore bs
            LEFT OUTER JOIN people p ON bs.p_id=p.id
            LEFT OUTER JOIN book b ON bs.id=b.bs_id
        WHERE bs.id=#{bsId}
    </select>

</mapper>
复制代码

最后就是单元测试类MybatisTest如下所示。

public class MybatisTest {

    private ObjectMapper objectMapper;

    private SqlSession sqlSession;

    @Before
    public void setUp() throws Exception {
        objectMapper = new ObjectMapper();

        String resource = "mybatis-config.xml";
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
                .build(Resources.getResourceAsStream(resource));
        sqlSession = sqlSessionFactory.openSession();
    }

    @After
    public void tearDown() {
        sqlSession.close();
    }

    @Test
    public void 根据人Id查询一个人并映射成Map() throws Exception {
        PeopleMapper peopleMapper = sqlSession.getMapper(PeopleMapper.class);
        Map<String, Object> peopleMap = peopleMapper.simpleQueryPeopleByPeopleId(1);
        System.out.println(objectMapper.writeValueAsString(peopleMap));
    }

    @Test
    public void 根据书Id查询一本书() throws Exception {
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
        Book book = bookMapper.queryBookByBookId(1);
        System.out.println(objectMapper.writeValueAsString(book));
    }

    @Test
    public void 根据人Id查询一个人() throws Exception {
        PeopleMapper peopleMapper = sqlSession.getMapper(PeopleMapper.class);
        People people = peopleMapper.queryPeopleByPeopleId(1);
        System.out.println(objectMapper.writeValueAsString(people));
    }

    @Test
    public void 关联的嵌套SELECT查询() throws Exception {
        AssociatedMapper associatedMapper = sqlSession
                .getMapper(AssociatedMapper.class);
        BsPeopleCombine bsPeopleCombine = associatedMapper
                .queryBsPeopleCombineByBsIdAssociatedNestSelect(1);
        System.out.println(bsPeopleCombine.getBookStoreName());
        People boss = bsPeopleCombine.getBoss();
        System.out.println(objectMapper.writeValueAsString(boss));
    }

    @Test
    public void 关联的嵌套结果查询() throws Exception {
        AssociatedMapper associatedMapper = sqlSession
                .getMapper(AssociatedMapper.class);
        BsPeopleCombine bsPeopleCombine = associatedMapper
                .queryBsPeopleCombineByBsIdAssociatedNestResult(1);
        System.out.println(objectMapper.writeValueAsString(bsPeopleCombine));
    }

    @Test
    public void 集合的嵌套SELECT查询() {
        AssociatedMapper associatedMapper = sqlSession
                .getMapper(AssociatedMapper.class);
        BsBookCombine bsBookCombine = associatedMapper
                .queryBsBookCombineByBsIdCollectionNestSelect(1);
        System.out.println(bsBookCombine.getBookStoreName());
        List<Book> books = bsBookCombine.getBooks();
        books.forEach(book -> System.out.println(book.getBookName()));
    }

    @Test
    public void 集合的嵌套结果查询() {
        AssociatedMapper associatedMapper = sqlSession
                .getMapper(AssociatedMapper.class);
        BsBookCombine bsBookCombine = associatedMapper
                .queryBsBookCombineByBsIdCollectionNestResult(1);
        System.out.println(bsBookCombine.getBookStoreName());
        List<Book> books = bsBookCombine.getBooks();
        books.forEach(book -> System.out.println(book.getBookName()));
    }

    @Test
    public void 关联和集合的嵌套SELECT查询() {
        AssociatedMapper associatedMapper = sqlSession
                .getMapper(AssociatedMapper.class);
        BsBkPeopleCombine bsBkPeopleCombine = associatedMapper
                .queryBsBkPeopleCombineByBsIdNestSelect(1);
        System.out.println(bsBkPeopleCombine.getBookStoreName());
        People boss = bsBkPeopleCombine.getBoss();
        System.out.println(boss.getPeopleName());
        List<Book> books = bsBkPeopleCombine.getBooks();
        books.forEach(book -> System.out.println(book.getBookName()));
    }

    @Test
    public void 关联和集合的嵌套结果查询() {
        AssociatedMapper associatedMapper = sqlSession
                .getMapper(AssociatedMapper.class);
        BsBkPeopleCombine bsBkPeopleCombine = associatedMapper
                .queryBsBkPeopleCombineByBsIdNestResult(1);
        System.out.println(bsBkPeopleCombine.getBookStoreName());
        People boss = bsBkPeopleCombine.getBoss();
        System.out.println(boss.getPeopleName());
        List<Book> books = bsBkPeopleCombine.getBooks();
        books.forEach(book -> System.out.println(book.getBookName()));
    }

}
复制代码

二. select标签

通过<select>标签,可以在映射文件中编写查询SQL语句,例如示例工程中,映射接口PeopleMappersimpleQueryPeopleByPeopleId() 方法对应的查询语句,如下所示。

<select id="simpleQueryPeopleByPeopleId" resultType="Map">
    SELECT
        id,
        p_name,
        p_age
    FROM people
    WHERE id=#{id}
</select>
复制代码

<select>标签的id属性是该查询语句在当前映射文件下的唯一标识,通常需要与映射接口中的方法名一样,而resultType属性告诉MyBatis应该将查询结果映射成什么对象类型,这里的对象类型需要与映射接口中的方法的返回类型一致,示例中是Map类型,那么表示需要将查询结果映射成一个Map对象,Mapkey就是列名,value就是记录值。

如果将<select>标签的resultType属性设置为某一个实体对象类型,只要列名与对象属性名一样或者满足驼峰命名规则且开启了自动映射机制(<setting name="mapUnderscoreToCamelCase" value="true"/>),MyBatis也能够完成查询结果到实体对象的映射。

但如果列名与对象属性名不一样,并且也不满足驼峰命名规则,此时要完成查询结果与实体对象的映射,需要借助<resultMap>标签,这里后面会详细说明。

<select>标签的常用属性配置可以参考select标签属性。 这里对一些常用的属性配置进行说明。

属性

配置

id

当前<select>标签在映射文件下的唯一标识。需要与映射接口的方法名一致

parameterType

传入SQL的参数的类型。例如上面例子中,就可以设置parameterType="int",但通常不需要设置,MyBatis能够自行推断出入参类型

resultType

告诉MyBatis需要将查询结果映射成什么类型。如果需要映射成集合类型,则resultType应该设置为集合的泛型类型。注意:resultTyperesultMap只能同时配置一个

resultMap

告诉MyBatis需要使用那个<resultMap>来做结果映射。需要配置为<resultMap>标签的id

flushCache

如果配置为true,则语句执行时清空一级缓存和二级缓存。<select>标签默认为false

useCache

如果配置为true,则语句执行结果会被缓存到二级缓存。<select>标签默认为true

三. resultMap标签

1. 认识标签和属性

如下是一个示例。

<!--
    id:表示resultMap在当前映射文件下的唯一标识,当前映射文件中通过id就可以引用该resultMap,其它映射
        文件中要引用该resultMap,需要通过当前映射文件的命名空间和resultMap的id来引用,即namesapce.id
    type:标识resultMap需要映射到那个实体对象
 -->
<resultMap id="BsBkPeopleCombineResultMap" type="com.lee.learn.mybatis.entity.BsBkPeopleCombine">
    <!--
        <id>标签:可以完成将一个列的值映射到实体对象的一个简单类型的属性值,<id>标签标记的
                 属性可以作为对象的唯一标识符,用于对象的快速比较
        property:实体对象的属性名
        column:表的列名
        javaType:指定实体对象的属性的Java类型,用于TypeHandler的推断
        jdbcType:指定列的Jdbc类型,用于TypeHandler的推断
        typeHandler:直接指定使用的TypeHandler
    -->
    <id property="id" column="bs_id"/>
    <!--
        <result>标签:可以完成将一个列的值映射到实体对象的一个简单类型的属性值
        property:实体对象的属性名
        column:表的列名
        javaType:指定实体对象的属性的Java类型,用于TypeHandler的推断
        jdbcType:指定列的Jdbc类型,用于TypeHandler的推断
        typeHandler:直接指定使用的TypeHandler
    -->
    <result property="bookStoreName" column="bs_name"/>
    <!--
        <association>标签:有两种工作方式
                            嵌套SELECT:将另外一个SELECT查询的结果映射为实体对象的某一个对象属性
                            嵌套结果:将查询出来的若干列映射为实体对象的某一个对象属性
                          本示例是嵌套结果的工作方式
        property:实体对象的属性名
        javaType:实体对象的属性的Java类型
        column:(嵌套SELECT)通过列名或者列别名指定列,指定列的值会作为参数传递给嵌套的select语句
        select:(嵌套SELECT)通过namespace.id的形式引用select语句
        fetchType:(嵌套SELECT)指定为lazy会让select引用的查询语句延迟执行,指定为eager会让
                    select引用的查询语句随着当前查询语句的执行而执行
    -->
    <association property="boss" javaType="com.lee.learn.mybatis.entity.People">
        <id property="id" column="p_id"/>
        <result property="peopleName" column="p_name"/>
        <result property="peopleAge" column="p_age"/>
    </association>
    <!--
        <collection>标签:有两种工作方式
                            嵌套SELECT:将另外一个SELECT查询的结果映射为实体对象的某一个集合属性
                            嵌套结果:将查询出来的若干列映射为实体对象的某一个集合属性中的一个对象
                         本示例是嵌套结果的工作方式
        property:实体对象的属性名
        javaType:实体对象的属性的集合类型,通常可以省略
        ofType:集合的泛型的类型
        column:(嵌套SELECT)通过列名或者列别名指定列,指定列的值会作为参数传递给嵌套的select语句
        select:(嵌套SELECT)通过namespace.id的形式引用select语句
        fetchType:(嵌套SELECT)指定为lazy会让select引用的查询语句延迟执行,指定为eager会让
                    select引用的查询语句随着当前查询语句的执行而执行
    -->
    <collection property="books" ofType="com.lee.learn.mybatis.entity.Book">
        <id property="id" column="b_id"/>
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
        <result property="bookStoreId" column="bbs_id"/>
    </collection>
</resultMap>
复制代码

首先是<resultMap>标签,该标签会定义一种查询结果到实体对象的映射关系,通过namespace.id就可以引用。标签上的重要属性,说明如下。

属性

说明

id

表示resultMap在当前映射文件下的唯一标识,当前映射文件中通过id就可以引用该resultMap,其它映射文件中要引用该resultMap,需要通过当前映射文件的命名空间和resultMapid来引用,即namesapce.id

type

标识resultMap需要映射到哪个实体对象

然后是<id>和<result>标签,这两个标签作用一样:可以完成将一个列映射到实体对象的一个简单类型的属性上。唯一的区别就是<id>标签标记的属性可以作为对象的唯一标识符,用于对象的快速比较。标签上的重要属性,说明如下。

属性

说明

property

实体对象的属性名

column

表的列名

javaType

指定实体对象的属性的Java类型,用于TypeHandler的推断。通常可以不指定,MyBatis能够通过<resultMap>标签的type属性拿到实体对象对应的属性的Java类型

jdbcType

指定列的Jdbc类型,用于TypeHandler的推断

typeHandler

直接指定使用的TypeHandler

再然后是<association>标签,该标签用于映射结果时,映射的对象具有关联关系,例如下面的对象。

public class BsPeopleCombine {

    private int id;
    private String bookStoreName;

    private People boss;

    // 省略get和set

}
复制代码

书店关联一个老板,查询书店时需要将书店关联的老板也查询出来,通常有两种关联方式。

  • 先查询出书店,然后再根据书店的查询结果查询出老板,最后组装两次查询的结果;
  • 通过关联查询一次查询出书店以及和书店关联的老板,关联查询的结果直接完成组装。

所以在MyBatis中,就可以通过<association>标签,完成上述的功能。<association>标签有两种使用方式,分别对应上面的两种关联方式。

  • 嵌套SELECT。将另外一个SELECT查询的结果映射为实体对象的某一个对象属性。也就是示例中的BsPeopleCombineboss属性的值由一个查询People的查询语句的查询结果来完成映射。这种方式很简单,但会带来N+1问题,这点后面再分析;
  • 嵌套结果。将关联查询(内连接左右外连)出来的若干列映射为实体对象的某一个对象属性。这种方式能规避N+1问题,但会增加SQL的复杂度。

<association>标签的重要属性如下所示。

属性

说明

property

实体对象的属性名

javaType

实体对象的属性的Java类型

column

嵌套SELECT)通过列名或者列别名指定列,指定列的值会作为参数传递给嵌套的select语句

select

嵌套SELECT)通过namespace.id的形式引用select语句

fetchType

嵌套SELECT)指定为lazy会让select引用的查询语句延迟执行,指定为eager会让select引用的查询语句随着当前查询语句的执行而执行

如果对上述的标签作用和属性还是不清楚,可以先有个印象,下面的小节会以实际的例子进行说明。

最后是<collection>标签,作用与<association>标签相似,不过适用于和集合关联的情况。示例如下。

public class BsBookCombine {

    private int id;
    private String bookStoreName;

    private List<Book> books;

    // 省略get和set
	
}
复制代码

书店关联多本书,查询书店时需要将书店关联的所有书也查询出来,通常也有两种关联方式。

  • 先查询出书店,然后再根据书店的查询结果查询出所有书,最后组装两次查询的结果;
  • 通过关联查询一次查询出书店以及和书店关联的所有书,关联查询的结果直接完成组装。

那么到这里也就知道了<collection>标签和<association>标签极其相似,两个标签的属性也是近乎一样,不同点在于<collection>标签比<association>标签多了一个ofType属性,该属性用于配置集合的泛型的类型。

2. 简单查询的结果映射

book表列信息如下。

列名

JDBC类型

id

INT

b_name

VARCHAR

b_price

FLOAT

bs_id

INT

Book实体对象如下。

public class Book {

    private int id;
    private String bookName;
    private float bookPrice;
    private int bookStoreId;
	
    // 省略get和set
	
}
复制代码

那么映射文件中对应的查询语句的<select>标签如下所示。

<select id="queryBookByBookId" resultMap="bookResultMap">
    SELECT
        id,
        b_name,
        b_price,
        bs_id
    FROM book
    WHERE id=#{bookId}
</select>
复制代码

在<select>标签中通过resultMap属性设置<resultMap>标签的id来引用resultMap

那么对应的<resultMap>如下所示。

<resultMap id="bookResultMap" type="com.lee.learn.mybatis.entity.Book">
    <id property="id" column="id"/>
    <result property="bookName" column="b_name"/>
    <result property="bookPrice" column="b_price"/>
    <result property="bookStoreId" column="bs_id"/>
</resultMap>
复制代码

3. 关联的嵌套SELECT查询的结果映射

本小节演示使用<association>标签来完成嵌套SELECT查询结果的映射。

关联的两张表分别为bookstorepeoplebookstore表列信息如下。

列名

JDBC类型

id

INT

bs_name

VARCHAR

p_id

INT

people表列信息如下。

列名

JDBC类型

id

INT

p_name

VARCHAR

p_age

INT

关联查询结果的实体对象为BsPeopleCombine,如下所示。

public class BsPeopleCombine {

    private int id;
    private String bookStoreName;

    private People boss;

    // 省略get和set
	
}
复制代码

映射文件中对应的查询语句的<select>标签如下所示。

<select id="queryBsPeopleCombineByBsIdAssociatedNestSelect" resultMap="bsPeopleCombineQueryMap">
    SELECT
        id,
        bs_name,
        p_id
    FROM bookstore
    WHERE id=#{bsId}
</select>
复制代码

引用的<resultMap>如下所示。

<resultMap id="bsPeopleCombineQueryMap" type="com.lee.learn.mybatis.entity.BsPeopleCombine">
    <id property="id" column="id"/>
    <result property="bookStoreName" column="bs_name"/>
    <association property="boss" column="p_id"
                 javaType="com.lee.learn.mybatis.entity.People"
                 select="com.lee.learn.mybatis.dao.PeopleMapper.queryPeopleByPeopleId"/>
</resultMap>
复制代码

select属性引用的<select>标签在映射文件PeopleMapper.xml文件中,其namespacecom.lee.learn.mybatis.dao.PeopleMapperPeopleMapper.xml中的idqueryPeopleByPeopleId的<select>标签如下所示。

<select id="queryPeopleByPeopleId" resultMap="peopleResultMap">
    SELECT
        id,
        p_name,
        p_age
    FROM people
    WHERE id=#{id}
</select>
复制代码

上述查询语句的入参,就取<association>标签中通过column指定的列的值,也就是idqueryBsPeopleCombineByBsIdAssociatedNestSelect的<select>标签查询回来的p_id会作为入参传入到idqueryPeopleByPeopleId的<select>标签中。

测试程序如下。

@Test
public void 关联的嵌套SELECT查询() throws Exception {
    AssociatedMapper associatedMapper = sqlSession
            .getMapper(AssociatedMapper.class);
    BsPeopleCombine bsPeopleCombine = associatedMapper
            .queryBsPeopleCombineByBsIdAssociatedNestSelect(1);
    System.out.println(bsPeopleCombine.getBookStoreName());
    People boss = bsPeopleCombine.getBoss();
    System.out.println(objectMapper.writeValueAsString(boss));
}
复制代码

运行测试程序,打印如下。

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@38e79ae3]
==>  Preparing: SELECT id, bs_name, p_id FROM bookstore WHERE id=?
==> Parameters: 1(Integer)
<==    Columns: id, bs_name, p_id
<==        Row: 1, XinHua, 1
<==      Total: 1
XinHua
==>  Preparing: SELECT id, p_name, p_age FROM people WHERE id=?
==> Parameters: 1(Integer)
<==    Columns: id, p_name, p_age
<==        Row: 1, Lee, 20
<==      Total: 1
{"id":1,"peopleName":"Lee","peopleAge":20}
复制代码

由于在配置文件中添加了如下设置。

<setting name="lazyLoadingEnabled" value="true"/>
复制代码

表示所有的嵌套查询都延迟执行,即用到对应的属性时才执行对应的查询语句,也就是测试程序中执行到bsPeopleCombine.getBoss() 时才会触发idqueryPeopleByPeopleId的<select>标签的语句的执行。

4. 关联的嵌套结果查询的结果映射

本小节演示使用<association>标签来完成嵌套结果查询的结果映射。

关联的两张表分别为bookstorepeoplebookstore表列信息如下。

列名

JDBC类型

id

INT

bs_name

VARCHAR

p_id

INT

people表列信息如下。

列名

JDBC类型

id

INT

p_name

VARCHAR

p_age

INT

关联查询结果的实体对象为BsPeopleCombine,如下所示。

public class BsPeopleCombine {

    private int id;
    private String bookStoreName;

    private People boss;

    // 省略get和set
	
}
复制代码

映射文件中对应的查询语句的<select>标签如下所示。

<select id="queryBsPeopleCombineByBsIdAssociatedNestResult" resultMap="bsPeopleCombineResultMap">
    SELECT
        bs.id AS bs_id,
        bs.bs_name AS bs_name,
        p.id AS p_id,
        p.p_name AS p_name,
        p.p_age AS p_age
    FROM bookstore bs
        LEFT OUTER JOIN people p ON bs.p_id=p.id
    WHERE bs.id=#{bsId}
</select>
复制代码

上述是一个左外连接的关联查询,引用的<resultMap>如下所示。

<resultMap id="bsPeopleCombineResultMap" type="com.lee.learn.mybatis.entity.BsPeopleCombine">
    <id property="id" column="bs_id"/>
    <result property="bookStoreName" column="bs_name"/>
    <association property="boss" javaType="com.lee.learn.mybatis.entity.People">
        <id property="id" column="p_id"/>
        <result property="peopleName" column="p_name"/>
        <result property="peopleAge" column="p_age"/>
    </association>
</resultMap>
复制代码

测试程序如下所示。

@Test
public void 关联的嵌套结果查询() throws Exception {
    AssociatedMapper associatedMapper = sqlSession
            .getMapper(AssociatedMapper.class);
    BsPeopleCombine bsPeopleCombine = associatedMapper
            .queryBsPeopleCombineByBsIdAssociatedNestResult(1);
    System.out.println(objectMapper.writeValueAsString(bsPeopleCombine));
}
复制代码

执行结果如下。

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@38e79ae3]
==>  Preparing: SELECT bs.id AS bs_id, bs.bs_name AS bs_name, p.id AS p_id, p.p_name AS p_name, p.p_age AS p_age FROM bookstore bs LEFT OUTER JOIN people p ON bs.p_id=p.id WHERE bs.id=?
==> Parameters: 1(Integer)
<==    Columns: bs_id, bs_name, p_id, p_name, p_age
<==        Row: 1, XinHua, 1, Lee, 20
<==      Total: 1
{"id":1,"bookStoreName":"XinHua","boss":{"id":1,"peopleName":"Lee","peopleAge":20}}
复制代码

可见只会执行一次查询,并且能够完成结果映射。

5. 集合的嵌套SELECT查询的结果映射

本小节演示使用<collection>标签来完成嵌套SELECT查询结果的映射。

关联的两张表分别为bookstorebookbookstore表列信息如下。

列名

JDBC类型

id

INT

bs_name

VARCHAR

p_id

INT

book表列信息如下。

列名

JDBC类型

id

INT

b_name

VARCHAR

b_price

FLOAT

bs_id

INT

关联查询结果的实体对象为BsBookCombine,如下所示。

public class BsBookCombine {

    private int id;
    private String bookStoreName;

    private List<Book> books;

    // 省略get和set
	
}
复制代码

映射文件中对应的查询语句的<select>标签如下所示。

<select id="queryBsBookCombineByBsIdCollectionNestSelect" resultMap="bsBookCombineQueryMap">
    SELECT
        id,
        bs_name
    FROM bookstore
    WHERE id=#{bsId}
</select>
复制代码

引用的<resultMap>如下所示。

<resultMap id="bsBookCombineQueryMap" type="com.lee.learn.mybatis.entity.BsBookCombine">
    <id property="id" column="id"/>
    <result property="bookStoreName" column="bs_name"/>
    <collection property="books" column="id"
                javaType="ArrayList" ofType="com.lee.learn.mybatis.entity.Book"
                select="com.lee.learn.mybatis.dao.BookMapper.queryBooksByBsId"/>
</resultMap>
复制代码

select属性引用的<select>标签在映射文件BookMapper.xml文件中,其namespacecom.lee.learn.mybatis.dao.BookMapperBookMapper.xml中的idqueryBooksByBsId的<select>标签如下所示。

<select id="queryBooksByBsId" resultMap="bookResultMap">
    SELECT
        id,
        b_name,
        b_price,
        bs_id
    FROM book
    WHERE bs_id=#{bsId}
</select>
复制代码

上述查询语句的入参,就取<collection>标签中通过column指定的列的值,也就是idqueryBsBookCombineByBsIdCollectionNestSelect的<select>标签查询回来的id会作为入参传入到idqueryBooksByBsId的<select>标签中。

测试程序如下。

@Test
public void 集合的嵌套SELECT查询() {
    AssociatedMapper associatedMapper = sqlSession
            .getMapper(AssociatedMapper.class);
    BsBookCombine bsBookCombine = associatedMapper
            .queryBsBookCombineByBsIdCollectionNestSelect(1);
    System.out.println(bsBookCombine.getBookStoreName());
    List<Book> books = bsBookCombine.getBooks();
    books.forEach(book -> System.out.println(book.getBookName()));
}
复制代码

运行测试程序,打印如下。

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@38e79ae3]
==>  Preparing: SELECT id, bs_name FROM bookstore WHERE id=?
==> Parameters: 1(Integer)
<==    Columns: id, bs_name
<==        Row: 1, XinHua
<==      Total: 1
XinHua
==>  Preparing: SELECT id, b_name, b_price, bs_id FROM book b WHERE bs_id=?
==> Parameters: 1(Integer)
<==    Columns: id, b_name, b_price, bs_id
<==        Row: 1, Math, 20.5, 1
<==        Row: 2, English, 21.5, 1
<==      Total: 2
Math
English
复制代码

6. 集合的嵌套结果查询的结果映射

本小节演示使用<collection>标签来完成嵌套结果查询的结果映射。

关联的两张表分别为bookstorebookbookstore表列信息如下。

列名

JDBC类型

id

INT

bs_name

VARCHAR

p_id

INT

book表列信息如下。

列名

JDBC类型

id

INT

b_name

VARCHAR

b_price

FLOAT

bs_id

INT

关联查询结果的实体对象为BsBookCombine,如下所示。

public class BsBookCombine {

    private int id;
    private String bookStoreName;

    private List<Book> books;

    // 省略get和set
	
}
复制代码

映射文件中对应的查询语句的<select>标签如下所示。

<select id="queryBsBookCombineByBsIdCollectionNestResult" resultMap="bsBookCombineResultMap">
    SELECT
        bs.id AS bs_Id,
        bs.bs_name AS bs_name,
        b.id AS b_id,
        b.b_name AS b_name,
        b.b_price AS b_price,
        b.bs_id AS bbs_id
    FROM bookstore bs
        LEFT OUTER JOIN book b ON bs.id=b.bs_id
    WHERE bs.id=#{bsId}
</select>
复制代码

上述是一个左外连接的关联查询,引用的<resultMap>如下所示。

<resultMap id="bsBookCombineResultMap" type="com.lee.learn.mybatis.entity.BsBookCombine">
    <id property="id" column="bs_Id"/>
    <result property="bookStoreName" column="bs_name"/>
    <collection property="books" ofType="com.lee.learn.mybatis.entity.Book">
        <id property="id" column="b_id"/>
        <result property="bookName" column="b_name"/>
        <result property="bookPrice" column="b_price"/>
        <result property="bookStoreId" column="bbs_id"/>
    </collection>
</resultMap>
复制代码

测试程序如下所示。

@Test
public void 集合的嵌套结果查询() {
    AssociatedMapper associatedMapper = sqlSession
            .getMapper(AssociatedMapper.class);
    BsBookCombine bsBookCombine = associatedMapper
            .queryBsBookCombineByBsIdCollectionNestResult(1);
    System.out.println(bsBookCombine.getBookStoreName());
    List<Book> books = bsBookCombine.getBooks();
    books.forEach(book -> System.out.println(book.getBookName()));
}
复制代码

执行结果如下。

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@38e79ae3]
==>  Preparing: SELECT bs.id AS bs_Id, bs.bs_name AS bs_name, b.id AS b_id, b.b_name AS b_name, b.b_price AS b_price, b.bs_id AS bbs_id FROM bookstore bs LEFT OUTER JOIN book b ON bs.id=b.bs_id WHERE bs.id=?
==> Parameters: 1(Integer)
<==    Columns: bs_Id, bs_name, b_id, b_name, b_price, bbs_id
<==        Row: 1, XinHua, 1, Math, 20.5, 1
<==        Row: 1, XinHua, 2, English, 21.5, 1
<==      Total: 2
XinHua
Math
English
复制代码

7. N+1问题与解决

第3和第5小节中,分别在<association>和<collection>标签中使用了select属性来引用查询语句,表示在进行结果映射时,需要执行这个查询语句来得到更多的结果来完成映射。以如下的查询语句为例。

<resultMap id="bsPeopleCombineQueryMap" type="com.lee.learn.mybatis.entity.BsPeopleCombine">
    <id property="id" column="id"/>
    <result property="bookStoreName" column="bs_name"/>
    <association property="boss" column="p_id"
                 javaType="com.lee.learn.mybatis.entity.People"
                 select="com.lee.learn.mybatis.dao.PeopleMapper.queryPeopleByPeopleId"/>
</resultMap>

<select id="queryBsPeopleCombineByBsIdAssociatedNestSelect" resultMap="bsPeopleCombineQueryMap">
    SELECT
        id,
        bs_name,
        p_id
    FROM bookstore
    WHERE id=#{bsId}
</select>
复制代码

由于是通过id=#{bsId}作为查询条件,所以上述查询语句的一次查询只会返回一条数据,而这一条数据会触发一次<association>标签的select属性引用的查询语句,所以一次查询实际触发了两次查询操作。但是试想一下,如果上述的条件变更为id>#{bsId},那么上述查询语句的一次查询有可能会返回多条数据,从而触发多次查询操作,这在大数据量时会十分影响性能,称这种问题为N+1问题。

解决N+1问题,有两种方式,如下所示。

  • 延迟执行额外的select。也就是使用到对应的属性时,才触发这个属性对应的select查询语句,这能够一定程度缓解N+1问题带来的性能影响。要想延时执行,也有两种方式:第一种是在配置文件中通过<setting name="lazyLoadingEnabled" value="true"/>设置lazyLoadingEnabledtrue,第二种是在<association>或<collection>标签中设置fetchTypelazy,其中第二种方式优先级高于第一种方式;
  • 采用嵌套结果查询。也就是使用第4和第6小节中的嵌套结果查询的方式来代替嵌套SELECT查询的方式。

总结

MyBatis中的映射文件中,可以通过<select>标签来配置查询语句,而执行查询后的结果映射,可以通过两种方式来配置。

  1. <select>标签的resultType。使用resultType告诉MyBatis需要将查询结果映射为什么对象类型,这要求表列名与对象的属性名一样或者满足驼峰命名规则,如果是满足驼峰命名规则,还需要开启自动映射机制(<setting name="mapUnderscoreToCamelCase" value="true"/>),才能完成查询结果映射;
  2. <select>标签的resultMap

<select>标签的resultMap能够引用<resultMap>标签的内容,即当前<select>标签的结果映射规则使用引用的<resultMap>标签配置的规则。<resultMap>标签能够完成如下规则配置。

  1. <id>和<result>配置一个列与简单数据类型的映射关系。在这两个标签中,通过column配置列,通过property配置简单数据类型的属性名,MyBatis会在结果映射时,完成配置的列到属性的映射;
  2. <association>配置查询结果到一个复杂属性的映射关系。假如一个书店有一个老板,那么在查询书店时,可以通过<association>配置一个查询语句,在查询出书店后,再通过这个查询语句查询出老板,这会带来N+1问题;也可以通过<association>配置一个映射关系,在关联查询出书店和老板的记录后,通过关联关系将老板对应的列映射到老板对应的属性对象上。
  3. <collection>配置查询结果到多个复杂属性的映射关系。<collection>和<association>几乎一模一样,只不过<association>适用场景是一个书店有一个老板的场景,而<collection>适用场景是一个书店有多本书的场景。