级联关系

概述

级联关系,就是一对一关联、一对多关联、多对多关联。

在Mybatis中的级联关系分为三种:

  • 鉴别器:它是一个根据某些条件决定采用具体实现级联的方案。
  • 一对一:比如说每个人都有一个身份证,这个身份证是唯一的,我们每个人和这个身份证就是一种一对一的级联。
  • 一对多:比如一个班级有多个学生,这就是一种一对多的级联关系。

Mybatis中的多对多关系其实就是两个一对多组成的,所以说在Mybatis中其实是没有多对多级联的,一般是采用两个一对多级联进行替换。

建表准备

-- ----------------------------
-- Table structure for employee
-- ----------------------------
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee`  (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键Id',
  `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '雇员姓名',
  `gender` tinyint(1) NOT NULL COMMENT '性别,1表示男,2表示女',
  `birthday` date NOT NULL COMMENT '对应的出生日期',
  `mobile` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户手机号',
  `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '对应的邮箱',
  `position` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户职位',
  `remarks` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '备注',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '员工表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for employee_card
-- ----------------------------
DROP TABLE IF EXISTS `employee_card`;
CREATE TABLE `employee_card`  (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id',
  `employee_id` int(11) NOT NULL COMMENT '对应的员工表',
  `card_no` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '对应的员工卡号',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '员工工牌表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for fmale_health_form
-- ----------------------------
DROP TABLE IF EXISTS `fmale_health_form`;
CREATE TABLE `fmale_health_form`  (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id',
  `employee_id` int(11) NOT NULL COMMENT '对应的员工编号id',
  `heart` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '心脏检测信息',
  `liver` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '肝脏检测信息',
  `lung` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '肺部检测信息',
  `uterus` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '子宫信息',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '女性体检表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for male_health_form
-- ----------------------------
DROP TABLE IF EXISTS `male_health_form`;
CREATE TABLE `male_health_form`  (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id',
  `employee_id` int(11) NOT NULL COMMENT '对应的员工编号id',
  `heart` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '心脏检测信息',
  `liver` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '肝脏检测信息',
  `lung` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '肺部检测信息',
  `prostate` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '前列腺信息',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '男性体检表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for task
-- ----------------------------
DROP TABLE IF EXISTS `task`;
CREATE TABLE `task`  (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id',
  `employee_id` int(11) NOT NULL COMMENT '对应的员工id',
  `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '对应的标题',
  `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '对应的任务内容',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '任务表' ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

创建对应的POJO,Mapper,以及初始化数据代码:

package com.xl.main;

import com.xl.mapper.*;
import com.xl.pojo.*;
import com.xl.tool.MybatisTool;
import org.apache.ibatis.session.SqlSession;

import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

public class Test {
    public static void main(String[] args) throws IOException {
        try(SqlSession sqlSession = MybatisTool.getSqlSession()) {
            EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
            EmployeeCardMapper employeeCardMapper = sqlSession.getMapper(EmployeeCardMapper.class);
            FmaleHealthFormMapper fmaleHealthFormMapper = sqlSession.getMapper(FmaleHealthFormMapper.class);
            MaleHealthFormMapper maleHealthFormMapper = sqlSession.getMapper(MaleHealthFormMapper.class);
            TaskMapper taskMapper = sqlSession.getMapper(TaskMapper.class);
            List<Employee> employees = Arrays.asList(
                    new Employee(null, "金钟国", (short) 1, new Date(1983, 11, 1), "13611111111", "mail.jkj@163.com",
                            "总经理", "流行歌手"),
                    new Employee(null, "刘在石", (short) 1, new Date(1985, 11, 1), "13622222222", "mail.shen@163.com",
                            "首席财务管理", "流行歌手"),
                    new Employee(null, "宋智孝", (short) 2, new Date(1991, 11, 1), "13333333333", "mail.song@163.com",
                            "首席技术官", "演员")
            );
            int index = 1;
            for(Employee employee : employees) {
                employeeMapper.add(employee);
                if (employee.getGender() == (short) 1) {
                    MaleHealthForm maleHealthForm = new MaleHealthForm(null, employee.getId(), "很好", "很好", "还可以", "要注意");
                    maleHealthFormMapper.add(maleHealthForm);
                } else {
                    FmaleHealthForm fmaleHealthForm = new FmaleHealthForm(null, employee.getId(), "很好", "很好", "还可以", "赶快结婚生孩子了");
                    fmaleHealthFormMapper.add(fmaleHealthForm);
                }
                employeeCardMapper.add(new EmployeeCard(null, employee.getId(), "EMVGXIT000" + (index++)));
                for (int i = 0; i < 10; i++) {
                    Task task = new Task(null, employee.getId(), "任务标题" + index + ":" + i, "任务内容" + index + ":" + i);
                    taskMapper.add(task);
                }
            }
            sqlSession.commit();
        }
    }
}

一对一关联

一对一关联,利用员工和工牌举例。一个员工有一个工牌,一个工牌属于一个员工,他们之间就是一对一的关系。

通过员工获取工牌号,通过工牌号获取员工

1,通过员工获取工牌号

首先在员工的POJO中加上一个字段:

/**
* 员工的工牌
*/
private EmployeeCard employeeCard;

然后在EmployeeCardMapper中加一个通过员工id获取卡号的方法:

/**
* 通过员工id获取对应的工牌
* @param employeeId
* @return
*/
EmployeeCard getByEmployeeId(int employeeId);

对应的xml中:

<select id="getByEmployeeId" resultType="EmployeeCard" parameterType="int">
	select * from employee_card where employee_id = #{employeeId}
</select>

接下来在EmployeeMapper中定义方法:

/**
* 通过id获取员工信息
* @param id
* @return
*/
Employee getById(int id);

在EmployeeMapper对应的xml中配置一个resultMap和对应的sql:

<!--
resultMap:当返回类型不止一个的时候使用,比如配置一对一关系或一对多关系时,普通查询单表的情况用resultType就好。
    id:给当前resultMap起一个名字,这个名字必须是唯一的
    type:表示当前resultMap返回的类型(返回值类型)
-->
<resultMap id="employeeMap" type="Employee">
    <!--
        id:配置查询结果中的主键id
        column和property:表示将查询结果中的column字段的值赋给POJO中的property属性
    -->
    <id property="id" column="id"/>
    <!--
        association:表示配置一对一的关系
            select:表示调用EmployeeCardMapper中的getByEmployeeId方法
            column:将这个参数传给select调用的方法
            property:最后的结果赋给Employee中的employeeCard属性
    -->
	<association property="employeeCard" column="id" select="com.xl.mapper.EmployeeCardMapper.getByEmployeeId"/>
</resultMap>
<select id="getById" resultMap="employeeMap" parameterType="int">
	select * from employee where id = #{id}
</select>

对应的调用:

public static void testOneToOne() throws IOException {
    try(SqlSession sqlSession = MybatisTool.getSqlSession()) {
        EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
        Employee employee = employeeMapper.getById(3);
        System.out.println(employee.getEmployeeCard());
    }
}

查询结果:

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@158d2680]
> Preparing: select * from employee where id = ?
> Parameters: 3(Integer)
< Columns: id, name, gender, birthday, mobile, email, position, remarks
< Row: 3, 宋智孝, 2, 1991-12-01, 13333333333, mail.song@163.com, 首席技术官, <>
<== Total: 1
宋智孝

2,通过工牌号获取对应员工

跟上面的步骤反着来,配置resultMap时改变下类型,其他差不多,这里就不再写了。

接下来说一下执行的过程:

  1. 首先,通过Employee employee = employeeMapper.getById(1);调用对应方法查询到了id为1的员工信息。
  2. 然后通过association中的select调用到EmployeeCardMapper中的getByEmployeeId方法,将员工信息中的员工id传给该方法,获取到id为1的员工的工牌信息。
  3. 最后把该员工的工牌信息赋给了Employee对应POJO中的employeeCard属性。从而获取到了该员工的工牌信息。

一对多关联

一对多关联,用员工和任务表举例。一个员工可以有多个任务,一个任务只有一个员工做。他们之间是一对多的关系。

例一:通过员工获取对应的任务(一对多)

1,首先在员工POJO里创建一个属性

/**
* 员工的任务,因为一个员工可能有多个任务,所以这里是list类型
*/
private List<Task> tasks;

2,定义对应的查询员工的方法和查询任务的方法

//EmployeeMapper中
/**
* 通过id获取员工信息
* @param id
* @return
*/
Employee getById(int id);

//TaskMapper中
/**
* 通过员工id获取对应的任务
* @param employeeId
* @return
*/
List<Task> findByEmployeeId(int employeeId);

对应的xml:

<!--TaskMapper中-->
<select id="findByEmployeeId" resultType="Task" parameterType="int">
	select * from task where employee_id = #{employeeId}
</select>

3,定义获取对应员工的resultMap

<resultMap id="employeeMap" type="Employee">
    <!--
        id:配置查询结果中的主键id
        column和property:表示将查询结果中的column字段的值赋给POJO中的property属性
    -->
    <id property="id" column="id"/>
    <!--
        association:表示配置一对一的关系
            select:表示调用EmployeeCardMapper中的getByEmployeeId方法
            column:将这个参数传给select调用的方法
            property:最后的结果赋给Employee中的employeeCard属性
	-->
	<association property="employeeCard" column="id" select="com.xl.mapper.EmployeeCardMapper.getByEmployeeId"/>
	<!--
		collection:表示配置一对多的关系,其他属性意义和association一样
	-->
	<collection property="tasks" column="id" select="com.xl.mapper.TaskMapper.findByEmployeeId"/>
</resultMap>
<select id="getById" resultMap="employeeMap" parameterType="int">
    select * from employee where id = #{id}
</select>

例二:通过任务获取对应的员工

1,在Task的PO里添加一个属性:

/**
* 任务对应的员工
*/
private Employee employee;

2,在TaskMapper.xml中定义对应的resultMap:

<resultMap id="allTask" type="Task">
	<id column="id" property="id"/>
	<association property="employee" column="employee_id" select="com.xl.mapper.EmployeeMapper.getById"/>
</resultMap>

3,获取所有Task的sql:

mapper中:

/**
* 获取所有任务
* @return
*/
List<Task> all();

xml中:

<select id="all" resultMap="allTask">
	select * from task
</select>

延迟加载

在上面的级联关系中,不管是一对一还是一对多关联都是有着一些问题的。比如说一对一,在写完以上所有代码后在执行,会发送3条sql语句,如果没有写完以上代码,只是一对一,那么也会发送2条sql。

如果此时只需要得到员工的名字,那么其实只要发送一条sql就够了,但是它还是会发送至少两条sql,这就是问题。

此时,可以通过使用mybatis的延迟加载来解决这个问题。

  • lazyLoadingEnabled:延迟加载的全局开关。开启时,所有关联的对象都会使用延迟加载,只有当需要使用时才会加载。mybatis通过判断有没有调用级联属性的get方法来决定是否加载。
  • aggressiveLazyLoading: 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。否则,每个延迟加载属性会按需要加载。这个默认是false,不管它就好。

在mybatis-config.xml中配置:

<!--开启延迟加载的开关 可用值:true/false -->
<setting name="lazyLoadingEnabled" value="true"/>

开启之后再运行前面的方法:

Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@77847718]
> Preparing: select * from task where id = ?
> Parameters: 1(Integer)
< Columns: id, employee_id, title, content
< Row: 1, 1, 任务标题2:0, <>
<== Total: 1
任务标题2:0

可以看到只发送了一条sql。

多对多关联

在Mybatis中没有多对多关联,但是生活中又存在多对多关联的情况,比如发文章,一篇文章可以有多个标签,一个标签也可以有多篇文章。Mybatis中的多对多其实就是两个一对多组成的,下面简单举个例子。

用文章和标签举例,定义对应的表,然后创建POJO

通过文章获取对应的标签:

article对应的POJO:

package com.xl.pojo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 文章对应的PO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Article {
    private Integer id;
    private String title;
    private String content;
    private List<Tag> tags;
}

tag对应的POJO:

package com.xl.pojo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;

/**
 * 标签对应的PO
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Tag {
    private Integer id;
    private String name;
    private List<Article> articles;
}

然后创建对应的ArticleMapper:

package com.xl.mapper;

import com.xl.pojo.Article;

public interface ArticleMapper {

    /**
     * 通过id获取一篇文章
     * @param id
     * @return
     */
    Article getById(int id);
}

对应的xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//rnybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xl.mapper.ArticleMapper">
    <resultMap id="articleMap" type="Article">
        <id column="id" property="id"/>
        <collection property="tags" column="id" select="com.xl.mapper.TagMapper.findByArticleId"/>
    </resultMap>
    <select id="getById" resultMap="articleMap" parameterType="int">
        select * from article where id = #{id}
    </select>
</mapper>

然后创建TagMapper:

package com.xl.mapper;

import com.xl.pojo.Tag;

import java.util.List;

public interface TagMapper {
    List<Tag> findByArticleId(int articleId);
}

对应的xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//rnybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xl.mapper.TagMapper">

    <select id="findByArticleId" resultType="Tag">
        select t.* from tag t left join tag_article ta on t.id=ta.tag_id where ta.article_id=#{articleId}
    </select>
</mapper>

测试:

public static void testMany2Many() throws IOException {
    try(SqlSession sqlSession = MybatisTool.getSqlSession()) {
        ArticleMapper articleMapper = sqlSession.getMapper(ArticleMapper.class);
        Article article = articleMapper.getById(1);
        for (Tag tag : article.getTags()) {
            System.out.println(tag.getName());
        }
    }
}

通过标签来获取文章也是一样的,这里就不再写了。这两个组合在一起就是所谓的多对多,写法其实跟一对多差不多。在TagMapper的xml中也可以不用左连接的写法,就跟一对多完全一样了。

基于连表查询的级联

假如现在有一个需求:查询所有文章和这些文章对应的所有标签

按之前的写法就是:

//ArticleMapper中
/**
* 通过id获取一篇文章
* @return
*/
List<Article> findAll();

//对应xml中:
<resultMap id="articleMap" type="Article">
	<id column="id" property="id"/>
	<collection property="tags" column="id" select="com.xl.mapper.TagMapper.findByArticleId"/>
</resultMap>
<select id="findAll" resultMap="articleMap">
	select * from article
</select>
    
//对应使用:
public static void testMany2Many() throws IOException {
    try(SqlSession sqlSession = MybatisTool.getSqlSession()) {
        ArticleMapper articleMapper = sqlSession.getMapper(ArticleMapper.class);
        List<Article> articles = articleMapper.findAll();
        for(Article article : articles) {
            System.out.println("Title-->"+article.getTitle());
            for (Tag tag : article.getTags()) {
                System.out.println("tag---->"+tag);
            }
        }
    }
}

以上的查询结果会循环发送多条sql去查询,数据少的时候还好,如果数据特别多的话,就有可能要发几百条sql去查询,这样来回访问数据库,效率肯定是很低的。所以一定要杜绝在循环里发送SQL。

这个时候可以用连接查询的方式来解决级联的问题

1,定义查询文章的方法:

List<Article> all();

2,然后定义对应的resultMap和sql:

<resultMap id="articleResultMap" type="Article">
    <id column="a_id" property="id"/>
    <result column="a_title" property="title"/>
    <result column="a_content" property="content"/>
    <!--
        如果是一对一关联,用association标签,指定类型用javaType
        如果是一对多关联,用collection标签,指定类型用ofType
            result:表示将普通字段赋给POJO中的属性
    -->
    <collection property="tags" ofType="Tag">
        <id column="t_id" property="id"/>
        <result column="t_name" property="name"/>
    </collection>
</resultMap>
<select id="all" resultMap="articleResultMap">
    select
        t.id as t_id,
        t.name as t_name,
        a.id as a_id,
        a.title as a_title,
        a.content as a_content
    from
        tag t
    left join
        tag_article ta on ta.tag_id = t.id
    left join
        article a on a.id=ta.article_id
</select>

还是差不多的测试方法:

public static void testMany2Many() throws IOException {
   try(SqlSession sqlSession = MybatisTool.getSqlSession()) {
        ArticleMapper articleMapper = sqlSession.getMapper(ArticleMapper.class);
        List<Article> articles = articleMapper.all();
        for(Article article : articles) {
            System.out.println("Title-->"+article.getTitle());
            for (Tag tag : article.getTags()) {
                System.out.println("tag---->"+tag);
            }
        }
    }
}

这个样子的结果,只发送了一条sql就查询出了所有数据。

基于Annotation的级联

Annotation实现级联,简单举一个例子。通过标签获取对应的文章

1,通过注解定义Mapper:

package com.xl.mapper;

import com.xl.pojo.Article;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface ArticleAnnMapper {

    @Select("select * from article")
    //Results:注解方式定义ResultMap  定义之后可多次使用
    @Results(id = "articleAnnResultMap", value = {
            @Result(id = true, property = "id", column = "id"),
            @Result(property = "tags", column = "id", many = @Many(
                    select = "com.xl.mapper.TagMapper.findByArticleId"
            ))
    })
    List<Article> all();

    @Select("select * from article where id=#{id}")
    @ResultMap("articleAnnResultMap")
    Article getById(int id);
}

2,调用:

public static void testAnn() throws IOException {
    try(SqlSession sqlSession = MybatisTool.getSqlSession()) {
        ArticleAnnMapper articleAnnMapper = sqlSession.getMapper(ArticleAnnMapper.class);
        Article article = articleAnnMapper.getById(1);
        System.out.println(article);
    }
}

public static void testAnn1() throws IOException {
    try(SqlSession sqlSession = MybatisTool.getSqlSession()) {
        ArticleAnnMapper articleAnnMapper = sqlSession.getMapper(ArticleAnnMapper.class);
        List<Article> articles = articleAnnMapper.all();
        System.out.println(articles);
    }
}