级联关系
概述
级联关系,就是一对一关联、一对多关联、多对多关联。
在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时改变下类型,其他差不多,这里就不再写了。
接下来说一下执行的过程:
- 首先,通过
Employee employee = employeeMapper.getById(1);
调用对应方法查询到了id为1的员工信息。 - 然后通过
association
中的select
调用到EmployeeCardMapper
中的getByEmployeeId
方法,将员工信息中的员工id传给该方法,获取到id为1的员工的工牌信息。 - 最后把该员工的工牌信息赋给了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);
}
}