• MyBatis 关联映射


    在关系型数据库中,多表之间存在着三种关联关系,分别为一对一、一对多和多对多:

    • 一对一:在任意一方引入对方主键作为外键。
    • 一对多:在 "多" 的一方,添加 "一" 的一方的主键作为外键。
    • 多对多:产生中间关系表,引入两张表的主键作为外键,两个主键成为联合主键或使用新的宇段作为主键。

    环境配置

    本文在 SpringBoot 框架的基础上介绍 MyBatis 的关联映射。

    • pom.xml 文件
    <dependencies>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
    
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
    
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    
        <!-- junit5 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    
    • application.properties配置文件
    # 数据库连接配置
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/mybatis_learning?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong
    spring.datasource.username=root
    spring.datasource.password=123456
    
    # 可选性配置
    # 开启驼峰命名匹配规则,默认为 false
    mybatis.configuration.map-underscore-to-camel-case=true
    # Mapper 接口所对应的 XML 映射文件位置,多个配置可以使用英文逗号隔开
    mybatis.mapper-locations=classpath:mapper/*.xml
    # 别名包扫描路径,通过该属性可以给包中的类注册别名,多个配置可以使用英文逗号隔开
    mybatis.type-aliases-package=com.example.entity
    # 开启控制台打印 sql 日志(以下两种方式都行)
    # mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
    logging.level.com.example.mapper=trace
    
    # 开启延迟加载
    # 设置为 true 表示开启全局延迟加载,默认为 false
    # mybatis.configuration.lazy-loading-enabled=true
    # 设置为 false 表示按需加载,在 MyBatis3.4.1 版本之前默认值为 true,之后为 false
    # mybatis.configuration.aggressive-lazy-loading=false
    

    一对一关联

    生活中,一对一关联关系是十分常见的。例如,一个人只能有一个身份证,同时一个身份证也只会对应一个人。Mybatis 一般使用<resultMap>的子标签<association>处理一对一关联关系。

    前期准备

    • 数据库脚本
    DROP TABLE IF EXISTS `tb_idcard`;
    CREATE TABLE `tb_idcard` (
      `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键自增',
      `code` VARCHAR(18) COMMENT '身份证号'
    ) COMMENT '身份证';
    
    INSERT INTO `tb_idcard`(`code`) VALUES('111111111111111111');
    INSERT INTO `tb_idcard`(`code`) VALUES('222222222222222222');
    
    DROP TABLE IF EXISTS `tb_person`;
    CREATE TABLE `tb_person` (
      `id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键自增',
      `name` VARCHAR(32) COMMENT '姓名',
      `age` INT COMMENT '年龄',
      `sex` VARCHAR(8) COMMENT '性别',
      `card_id` INT UNIQUE COMMENT '身份证 id',
       FOREIGN KEY(`card_id`) REFERENCES `tb_idcard`(`id`)
    )COMMENT '个人信息表';
    
    INSERT INTO `tb_person`(`name`,`age`,`sex`,`card_id`) VALUES('Rose', 29, '女', 1);
    INSERT INTO `tb_person`(`name`,`age`,`sex`,`card_id`) VALUES('Tom', 30, '男', 2);
    
    • 实体类
    package com.example.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.io.Serializable;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class IdCard implements Serializable {
        private Integer id;   // 主键 id
        private String code;  // 身份证件号
    }
    
    package com.example.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.io.Serializable;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Person implements Serializable {
        private Integer id;   // 主键 id
        private String name;  // 姓名
        private Integer age;  // 年龄
        private String sex;   // 性别
        private IdCard idCard;  // 个人关联的证件
    }
    
    • PersonMapper 接口:
    package com.example.mapper;
    
    import com.example.entity.Person;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface PersonMapper {
    
    }
    
    • PersonMapper.xml映射文件:
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.example.mapper.PersonMapper">
        
    </mapper>    
    

    查询方式

    查询方式可分为嵌套结果嵌套查询两种。嵌套结果是多表联合查询,将所有需要的值一次性查询出来;嵌套查询是通过多次查询,一般为多次单表查询,最终将结果进行组合。

    嵌套结果

    PersonMapper接口中定义方法:

    // 嵌套结果
    Person selectById(Integer id);
    

    PersonMapper.xml映射文件添加对应方法的<select>语句:

    <!-- 嵌套结果 -->
    <resultMap id="IdCardWithPersonResult" type="com.example.entity.Person">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <!-- 对于 pojo 类属性,使用 <association> 标签进行映射 -->
        <association property="idCard" javaType="com.example.entity.IdCard">
            <id property="id" column="card_id"/>
            <result property="code" column="code"/>
        </association>
    </resultMap>
    
    <!-- 多表联合查询,一次性将所需要的值查询出来 -->
    <select id="selectById" resultMap="IdCardWithPersonResult">
        select p.id, p.name, p.age, p.sex, p.card_id, card.code
        from tb_person p, tb_idcard card
        where p.card_id = card.id and p.id = #{id}
    </select>
    

    <association>标签的嵌套结果常用属性如下:

    • property:对应实体类中的属性名,必填项。
    • javaType: 属性对应的 Java 类型。
    • resultMap:可以直接使用现有的 resultMap,而不需要在这里配置。
    • columnPrefix:查询列的前缀,配置前缀后,在子标签配置 result 的 column 时可以省略前缀。

    除了这些属性外,还有其他属性,此处不做介绍。 下面对selectById()方法进行测试:

    package com.example.mapper;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class PersonMapperTest {
    
        @Autowired
        private PersonMapper personMapper;
        
        @Test
        void selectById() {
            System.out.println(personMapper.selectById(1));
        }
    }
    
    ==>  Preparing: select p.id, p.name, p.age, p.sex, p.card_id, card.code from tb_person p, tb_idcard card where p.card_id = card.id and p.id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, name, age, sex, card_id, code
    <==        Row: 1, Rose, 29, 女, 1, 111111111111111111
    <==      Total: 1
    Person(id=1, name=Rose, age=29, sex=女, idCard=IdCard(id=1, code=111111111111111111))    
    

    像这种通过一次查询将结果映射到不同对象的方式, 称之为关联的嵌套结果映射。关联的嵌套结果映射需要关联多个表将所有需要的值一次性查询出来。这种方式的好处是减少数据库查询次数, 减轻数据库的压力,缺点是要写很复杂的 SQL,并且当嵌套结果更复杂时,不容易一次写正确,由于要在应用服务器上将结果映射到不同的类上,因此也会增加应用服务器的压力。当一定会使用到嵌套结果,并且整个复杂的 SQL 执行速度很快时,建议使用关联的嵌套结果映射。

    嵌套结果映射也可以通过设置查询结果列别名的方式实现,不使用 resultMap:

    <!-- 多表联合查询,一次性将所有需要的值查询出来 -->
    <select id="selectById" resultType="com.example.entity.Person">
        select p.id, p.name, p.age, p.sex, p.card_id,
        card.id "idCode.id",
        card.code "idCard.code"
        from tb_person p, tb_idcard card
        where p.card_id = card.id and p.id = #{id}
    </select>
    

    需要注意,上面的idCode.ididCard.code必须加""号。

    嵌套查询

    在 PersonMapper 接口中定义方法:

    // 嵌套查询
    Person selectById2(Integer id);
    

    PersonMapper.xml映射文件添加对应方法的<select>语句:

    <!-- 方式二:嵌套查询 -->
    <resultMap id="IdCardWithPersonResult2" type="com.example.entity.Person">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <!-- 通过引用另一条查询 SQL 设置该 pojo 类属性 -->
        <association property="idCard" column="card_id" select="selectCardById"/>
    </resultMap>
    
    <!-- 被引用的查询 SQL -->
    <select id="selectCardById" resultType="com.example.entity.IdCard">
        select id, code from tb_idcard where id = #{id}
    </select>
    
    <!-- 多次查询,最终将结果组合 -->
    <select id="selectById2" resultMap="IdCardWithPersonResult2">
        select id, name, age, sex, card_id  from tb_person where id = #{id}
    </select>
    

    <association>标签的嵌套查询常用的属性如下:

    • property:对应实体类中的属性名,必填项。

    • select:被引用的查询 SQL 的 id,MyBatis 会额外执行这个查询获取嵌套对象的结果。

    • column:设置嵌套查询(被引用的查询)的传入参数,该参数是主查询中列的结果。对于单个传入参数,可以直接设置;对于多个传入参数,通过column="{prop1=col1,prop2=col2}"方式设置,在嵌套查询中使用#{prop1}#{prop2}获取传入参数值,效果等同@param注解。

    • fetchType:数据加载方式,可选值为 lazy 和 eager,分别为延迟加载和立即加载,这个配置会覆盖全局的 lazyLoadingEnabled配置。

    下面对selectById2()方法进行测试:

    @Test
    void selectById2() {
        System.out.println(personMapper.selectById2(1));
    }
    
    ==>  Preparing: select id, name, age, sex, card_id from tb_person where id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, name, age, sex, card_id
    <==        Row: 1, Rose, 29, 女, 1
    ====>  Preparing: select id, code from tb_idcard where id = ?
    ====> Parameters: 1(Integer)
    <====    Columns: id, code
    <====        Row: 1, 111111111111111111
    <====      Total: 1
    <==      Total: 1
    Person(id=1, name=Rose, age=29, sex=女, idCard=IdCard(id=1, code=111111111111111111))    
    

    嵌套查询是使用简单的 SQL 通过多次查询转换为我们需要的结果,这种方式与根据业务逻辑手动执行多次 SQL 的方式相像,最后会将结果组合成一个对象。


    延迟加载

    嵌套查询的延迟加载

    对于嵌套查询而言,有时候不必每次都使用嵌套查询里的数据,例如上面的 IdCard 可能不必每次都使用。如果查询出来并没有使用,会白白浪费一次查询,此时可以使用延迟加载。在上面介绍<association>标签的属性时,介绍了 fetchType,通过该属性可以设置延迟加载,这个配置会覆盖全局的lazyLoadingEnabled配置,默认的全局配置是立即加载。

    <resultMap id="IdCardWithPersonResult2" type="com.example.entity.Person">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <association property="idCard" column="card_id"
                     select="selectCardById" fetchType="lazy" />
    </resultMap>
    

    fetchType 可选值为 lazy 和 eager,分别为延迟加载和立即加载。这两个数据加载方式定义如下:

    • 立即加载:默认的数据加载方式,执行主查询时,被关联的嵌套查询也会执行。
    • 延迟加载:也叫懒加载,只有当在真正需要数据的时候,才真正执行数据加载操作。也就是说,配置了延迟加载,被关联的嵌套查询不会立即执行,只有需要的时候,才执行该 SQL 语句。

    下面selectById2()方法重新测试:

    @Test
    void selectById2() {
        Person person = personMapper.selectById2(1);
        System.out.println("=========执行 person.getIdCard()============");
        person.getIdCard();
    }
    
    ==>  Preparing: select id, name, age, sex, card_id from tb_person where id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, name, age, sex, card_id
    <==        Row: 1, Rose, 29, 女, 1
    <==      Total: 1
    =========执行 person.getIdCard()============
    ==>  Preparing: select id, code from tb_idcard where id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, code
    <==        Row: 1, 111111111111111111
    <==      Total: 1    
    

    由上可以看出,执行selectById2()时只运行了主查询的 SQL,只有需要到关联的属性时,嵌套查询的 SQL 才会被执行。

    全局配置

    与延迟加载有关的全局配置有两个:

    • lazyLoadingEnabled:开启全局的延迟加载开关;true 表示开启,false 表示关闭,默认为 false。
    • aggressiveLazyLoading:用于控制具有延迟加载特性的对象的属性的加载情况;true 表示具有延迟加载特性的对象的任意调用都会导致这个对象的完整加载,fasle 表示每种属性都是按需加载,在 3.4.1 版本之前默认值为 true,之后为 false。

    第二个值可能不好理解,这里举个例子说明。将全局配置中的aggressiveLazyLoading设置为 true,再次对selectById2()方法进行测试:

    mybatis.configuration.aggressive-lazy-loading=true
    
    @Test
    void selectById2() {
        Person person = personMapper.selectById2(1);
        System.out.println("=========执行 person.setSex()============");
        person.setSex("女");
    }
    
    ==>  Preparing: select id, name, age, sex, card_id from tb_person where id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, name, age, sex, card_id
    <==        Row: 1, Rose, 29, 女, 1
    <==      Total: 1
    =========执行 person.setSex()============
    ==>  Preparing: select id, code from tb_idcard where id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, code
    <==        Row: 1, 111111111111111111
    <==      Total: 1
    

    由上面可以看出,当aggressiveLazyLoading设置为 true,只要具有延迟加载特性的对象任意调用,无论该调用是否与关联属性有关,嵌套查询的 SQL 都会被执行;而当aggressiveLazyLoading设置为 false,只有需要到关联属性时,即执行关联属性对应的getXXX()方法,才会执行嵌套查询里的 SQL。

    一般全局延迟加载配置如下:

    # 开启延迟加载
    # 设置为 true 表示开启全局延迟加载,默认为 false
    mybatis.configuration.lazy-loading-enabled=true
    # 设置为 false 表示按需加载,在 MyBatis3.4.1 版本之前默认值为 true,之后为 false
    mybatis.configuration.aggressive-lazy-loading=false
    

    特别提醒:MyBatis 延迟加载是通过动态代理实现的,当调用配置为延迟加载的属性方法时,动态代理的操作会被触发,这些额外的操作就是通过 MyBatis 的 SqlSession 去执行嵌套 SQL 的。 由于在和某些框架集成时,SqlSession 的生命周期交给了框架来管理,因此当对象超出 SqlSession 生命周期调用时,会由于链接关闭等问题而抛出异常。在和 Spring 集成时,要确保只能在 Service 层调用延迟加载的属性。 当结果从 Service 层返回至 Controller 层时,如果获取延迟加载的属性值,会因为 SqlSession 已经关闭而抛出异常。


    一对多关联

    与一对一的关联关系相比,开发人员接触更多的关联关系是一对多(或多对一)。 例如一个用户可以有多个订单,同时多个订单归一个用户所有。Mybatis 一般使用<resultMap>的子标签<collection>处理一对多关联关系。

    前期准备

    • 数据库脚本
    DROP TABLE IF EXISTS `tb_user`;
    CREATE TABLE `tb_user` (
    	`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键自增',
    	`username` VARCHAR(32) COMMENT '用户名',
    	`address` VARCHAR(256) COMMENT '地址'
    ) COMMENT '用户表';
    
    INSERT INTO `tb_user` VALUES(1, '小米', '北京');
    INSERT INTO `tb_user` VALUES(2, '小明', '上海');
    INSERT INTO `tb_user` VALUES(3, '小红', '天津');
    
    DROP TABLE IF EXISTS `tb_orders`;
    CREATE TABLE `tb_orders` (
    	`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键自增',
    	`number` VARCHAR(32) NOT NULL COMMENT '订单号',
    	`user_id` INT NOT NULL COMMENT '外键',
    	FOREIGN KEY(`user_id`) REFERENCES `tb_user`(`id`)
    ) COMMENT '订单表'; 
    
    INSERT INTO `tb_orders` VALUES(1, '1001', 1);
    INSERT INTO `tb_orders` VALUES(2, '1002', 1);
    INSERT INTO `tb_orders` VALUES(3, '1003', 2);
    
    • 实体类
    package com.example.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.io.Serializable;
    import java.util.List;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User implements Serializable {
        private Integer id;       // 用户 id
        private String username;  // 用户名
        private String address;   // 地址
        private List<Orders> ordersList;  // 订单列表
    }
    
    package com.example.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.io.Serializable;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Orders implements Serializable {
        private Integer id;     // 订单 id
        private String number;  // 订单编号
    }
    
    • UserMapper 接口
    package com.example.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface UserMapper {
    }
    
    • UserMapper.xml映射文件
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.example.mapper.UserMapper">
    
    </mapper>
    

    查询方式

    和一对一关联方式一样,查询方式可分为嵌套结果嵌套查询两种。

    嵌套结果

    在 UserMapper 接口中定义方法:

    // 嵌套结果
    User selectById(Integer id);
    

    UserMapper.xml映射文件添加对应方法的<select>语句:

    <!-- 嵌套结果 -->
    <resultMap id="UserWithOrdersResult" type="com.example.entity.User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="address" column="address"/>
        <!-- 对于集合类属性,可以使用<collection>进行映射 -->
        <collection property="ordersList" ofType="com.example.entity.Orders">
            <id property="id" column="orders_id"/>
            <result property="number" column="number"/>
        </collection>
    </resultMap>
    
    <!-- 多表联合查询,一次性将所需要的值查询出来 -->
    <select id="selectById" resultMap="UserWithOrdersResult">
        select u.id, u.username, u.address, o.id orders_id, o.number
        from tb_user u, tb_orders o
        where u.id = o.user_id and u.id = #{id}
    </select>
    

    <collection><association>大部分属性相同,但还包含一个特殊属性 ofType,该属性对 javaType 属性对应,用来表示实体类对象中集合类属性所包含的元素类型。需要注意,一对多的嵌套结果方式的查询一定要设置<id>标签,它配置的是主键,MyBatis 在处理多表联合查询出来的数据,会逐条比较全部数据中<id>标签配置的字段值是否相同,相同的数据进行合并,最后映射到 resultMap 中。

    selectById()进行测试:

    package com.example.mapper;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class UserMapperTest {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void selectById() {
            System.out.println(userMapper.selectById(1));
        }
    }
    
    ==>  Preparing: select u.id, u.username, u.address, o.id orders_id, o.number from tb_user u, tb_orders o where u.id = o.user_id and u.id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, username, address, orders_id, number
    <==        Row: 1, 小米, 北京, 1, 1001
    <==        Row: 1, 小米, 北京, 2, 1002
    <==      Total: 2
    User(id=1, username=小米, address=北京, ordersList=[Orders(id=1, number=1001), Orders(id=2, number=1002)])        
    

    嵌套查询

    在 UserMapper 接口中定义方法:

    // 嵌套结果
    User selectById2(Integer id);
    

    UserMapper.xml映射文件添加对应方法的<select>语句:

    <!-- 嵌套查询 -->
    <resultMap id="UserWithOrdersResult2" type="com.example.entity.User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="address" column="address"/>
        <!-- 通过引用另一条查询 SQL 设置该集合类属性 -->
        <collection property="ordersList" column="id" select="selectOrdersById"/>
    </resultMap>
    
    <!-- 被引用的查询 SQL -->
    <select id="selectOrdersById" resultType="com.example.entity.Orders">
        select id, number from tb_orders where user_id = #{id}
    </select>
    
    <!-- 多次查询,最终将结果组合 -->
    <select id="selectById2" resultMap="UserWithOrdersResult2">
        select id, username, address from tb_user where id = #{id}
    </select>
    

    selectById2()方法进行测试:

    ==>  Preparing: select id, username, address from tb_user where id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, username, address
    <==        Row: 1, 小米, 北京
    ====>  Preparing: select id, number from tb_orders where user_id = ?
    ====> Parameters: 1(Integer)
    <====    Columns: id, number
    <====        Row: 1, 1001
    <====        Row: 2, 1002
    <====      Total: 2
    <==      Total: 1
    User(id=1, username=小米, address=北京, ordersList=[Orders(id=1, number=1001), Orders(id=2, number=1002)])    
    

    延迟加载的方式和一对一关联的一样。


    多对多关联

    在实际项目开发中,多对多的关联关系也是非常常见的。以订单和商品为例,一个订单可以包含多种商品,而一种商品又可以属于多个订单,订单和商品就属于多对多的关联关系。

    前期准备

    • 数据库脚本

    在一对多的数据库脚本基础上创建新表:

    DROP TABLE IF EXISTS `tb_product`;
    CREATE TABLE `tb_product` (
    	`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键自增', 
    	`name` VARCHAR(32) COMMENT '商品名',              
    	`price` DOUBLE COMMENT '价钱'                            
    ) COMMENT '商品表';
    
    INSERT INTO `tb_product` VALUES(1, '商品1', 10.0);
    INSERT INTO `tb_product` VALUES(2, '商品2', 20.0);
    INSERT INTO `tb_product` VALUES(3, '商品3', 30.0);
    
    DROP TABLE IF EXISTS `tb_ordersitem`;
    CREATE TABLE `tb_ordersitem` (
    	`id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键自增',
    	`orders_id` INT(32) COMMENT '订单 id',
    	`product_id` INT(32) COMMENT '商品 id',
    	FOREIGN KEY(`orders_id`) REFERENCES `tb_orders`(`id`),
    	FOREIGN KEY(`product_id`) REFERENCES `tb_product`(`id`)
    ) COMMENT '订单和商品中间表';
    
    INSERT INTO `tb_ordersitem` VALUES(1, 1, 1);
    INSERT INTO `tb_ordersitem` VALUES(2, 1, 3);
    INSERT INTO `tb_ordersitem` VALUES(3, 3, 3);
    
    • 实体类

    在一对多的实体类基础上创建 Product 类和更改 Orders 类:

    package com.example.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.io.Serializable;
    import java.util.List;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Product implements Serializable {
        private Integer id;     // 商品 id
        private String name;    // 商品名称
        private Double price;   // 商品价格
        private List<Orders> orders;  // 商品所属的订单
    }
    
    package com.example.entity;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.io.Serializable;
    import java.util.List;
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Orders implements Serializable {
        private Integer id;     // 订单 id
        private String number;  // 订单编号
        private List<Product> productList;  // 订单的商品列表
    }
    
    • OrdersMapper 接口
    package com.example.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface OrdersMapper {
    }
    
    • OrdersMapper.xml映射文件
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.example.mapper.OrdersMapper">
        
    </mapper>
    

    查询方式

    查询方式可分为嵌套结果嵌套查询两种。

    嵌套结果

    在 OrdersMapper 接口中定义方法:

    // 嵌套结果
    Orders selectById(Integer id);
    

    OrdersMapper.xml映射文件添加对应方法的<select>语句:

    <!-- 嵌套结果 -->
    <resultMap id="OrdersWithProductResult" type="com.example.entity.Orders">
        <id property="id" column="id"/>
        <result property="number" column="number"/>
        <!-- 对于集合类属性,可以使用<collection>进行映射 -->
        <collection property="productList" ofType="com.example.entity.Product">
            <id property="id" column="pid"/>
            <result property="name" column="name"/>
            <result property="price" column="price"/>
        </collection>
    </resultMap>
    
    <!-- 多表联合查询,一次性将所需要的值查询出来 -->
    <select id="selectById" resultMap="OrdersWithProductResult">
        select o.id, o.number, o.user_id, p.id pid, p.name, p.price
        from tb_orders o, tb_product p, tb_ordersitem oi
        where oi.orders_id = o.id and oi.product_id = p.id and o.id = #{id}
    </select>
    

    selectById()方法进行测试:

    @Test
    void selectById() {
        System.out.println(ordersMapper.selectById(1));
    }
    
    ==>  Preparing: select o.id, o.number, o.user_id, p.id pid, p.name, p.price from tb_orders o, tb_product p, tb_ordersitem oi where oi.orders_id = o.id and oi.product_id = p.id and o.id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, number, user_id, pid, name, price
    <==        Row: 1, 1001, 1, 1, 商品1, 10.0
    <==        Row: 1, 1001, 1, 3, 商品3, 30.0
    <==      Total: 2
    Orders(id=1, number=1001, productList=[Product(id=1, name=商品1, price=10.0, orders=null), Product(id=3, name=商品3, price=30.0, orders=null)])
    

    嵌套查询

    在 OrdersMapper 接口中定义方法:

    // 嵌套查询
    Orders selectById2(Integer id);
    

    UserMapper.xml映射文件添加对应方法的<select>语句:

    <!-- 嵌套查询 -->
    <resultMap id="OrdersWithProductResult2" type="com.example.entity.Orders">
        <id property="id" column="id"/>
        <result property="number" column="number"/>
        <!-- 通过引用另一条查询 SQL 设置该集合类属性 -->
        <collection property="productList" column="id" select="selectProductById"/>
    </resultMap>
    
    <!-- 被引用的查询 SQL -->
    <select id="selectProductById" resultType="com.example.entity.Product">
        select id, name, price from tb_product
        where id in(select product_id from tb_ordersitem where orders_id = #{id})
    </select>
    
    <!-- 多次查询,最终将结果组合 -->
    <select id="selectById2" resultMap="OrdersWithProductResult2">
        select id, number, user_id from tb_orders where id = #{id}
    </select>
    

    selectById2()方法进行测试:

    @Test
    void selectById2() {
        System.out.println(ordersMapper.selectById(1));
    }
    
    ==>  Preparing: select o.id, o.number, o.user_id, p.id pid, p.name, p.price from tb_orders o, tb_product p, tb_ordersitem oi where oi.orders_id = o.id and oi.product_id = p.id and o.id = ?
    ==> Parameters: 1(Integer)
    <==    Columns: id, number, user_id, pid, name, price
    <==        Row: 1, 1001, 1, 1, 商品1, 10.0
    <==        Row: 1, 1001, 1, 3, 商品3, 30.0
    <==      Total: 2
    Orders(id=1, number=1001, productList=[Product(id=1, name=商品1, price=10.0, orders=null), Product(id=3, name=商品3, price=30.0, orders=null)])    
    

    延迟加载的方式和一对一关联的一样。


    参考

    1. 《MyBatis从入门到精通》
    2. 《Java EE企业级应用开发教程(Spring+Spring MVC+MyBatis)》
  • 相关阅读:
    【转】JSP三种页面跳转方式
    我要从头做起
    转载:用 Tomcat 和 Eclipse 开发 Web 应用程序
    html的style属性
    Java连接oracle数据库
    tomcat遇到的问题(总结)
    ceshi
    今天要小结一下
    argument.callee.caller.arguments[0]与window.event
    JavaScript事件冒泡简介及应用
  • 原文地址:https://www.cnblogs.com/zongmin/p/13369404.html
Copyright © 2020-2023  润新知