开发者社区> 守望时空33> 正文

玩“公主焊接”,顺便学习学习数据库关系映射与Spring Boot中MyBatis(SSM框架)的级联操作(bushi)

简介: 玩“公主焊接”,顺便学习学习数据库关系映射与Spring Boot中MyBatis(SSM框架)的级联操作(bushi)
+关注继续查看

在做一个系统之前,设计其数据库表、建立类图(数据模型),也是非常重要的一步。

一个复杂的系统,其各个数据之间也有着各种关系,这也给我们设计数据模型造成了困扰。今天我就来分享一下,数据库中的表的关系以及实际的操作方案。

1,概述

其实数据之间的关系,无外乎就这么几种:

  • 一对一
  • 一对多(多对一也是一回事)
  • 多对多

其实一对一是最好理解和操作的,在实现上有一定的难度的就是多对多了。

那么今天我主要是分享,实际情况下我们怎么实现这些关系,以及在SSM框架中,我们怎么操作。文末会给出示例代码仓库。

今天我将以游戏《公主连结 Re:Dive》中的角色公会武器为例,来讲解这个关系的实现。

在公主连结游戏中,有很多的角色,其中每个角色属于一个公会(组织),其中一个角色还需要装备一些武器,且每个角色有她的专属武器

这里可见:

  • 角色和专属武器是一对一的关系,一个角色只能有一个专属武器,一个专属武器也只属于一个角色
    image角色“宫子”只有一个专属武器“灵甘幽灵布丁”
  • 公会和角色是一对多的关系,一个角色只能属于一个公会,一个公会可以有多个角色
    imageimage角色“美美”和“镜华”都属于公会“小小甜心”,且她们每个人只属于一个公会
  • 武器和角色是多对多的关系,一个角色会装备多个武器,而一个武器也可能被多个角色使用
    image角色“未奏希”装备了“岚神风暴护手”等四件武器装备
    imageimage角色“真步”和“茜里”都装备了武器“细冰姬的蝴蝶结”

一对一关系比较简单,所以今天就只讲解一对多多对多这两个关系。

2,数据库表的设计

根据以上信息,我们一步一步地来设计数据库表。

首先是建立角色(character)公会(guild)武器(weapon) 三个表:

image

我们先来实现公会和角色的一对多关系,其实很简单,只需要在角色表中新增一个字段表示公会主键id即可

image

也就是说,一对多关系中,只需要在“多”的那个表中,加入一个字段表示“一”的那个表的主键字段,作为“多”的表的外键,就实现了一对多的关系的构建和联系,可以说这个还是很简单的。

然后我们来实现角色和武器多对多的关系。多对多需要在两个表之间额外再建立一个表专门用于表示两个表的关系,这个表一般没有主键,只有外键,其外键就是两个“多”的表的主键:

image

可见weapon_character表即为“武器-角色”关系表,它是用于建立武器和角色表的多对多的桥梁

至于多对多为什么这么设计呢?我们举个例子,把几个表格放出来看看:

image

看见“角色-武器”表,我们就恍然大悟了,在这个表中,表示了:id为0的角色(镜华)使用id为0的武器(牺牲月法杖),id为0的角色(镜华)还使用id为1的武器(细冰姬的蝴蝶结)

这样,角色和武器表只需要存储自己的信息即可,使用“角色-武器”表,以表示两者之间多对多的关系。

正是借助“角色-武器”表,通过MySQL关联查询,也可以很方便地双向查询彼此关系。

至此,我们的数据库表设计就完成了!

这里放上sql语句,这里除了建表之外,也会初始化一部分示例数据:

-- 公会
drop table if exists `guild`;
create table `guild`
(
   `id`   int         not null,
   `name` varchar(16) not null unique,
   primary key (`id`)
) engine = InnoDB
  default charset = utf8mb4;
-- 武器
drop table if exists `weapon`;
create table `weapon`
(
   `id`   int         not null,
   `name` varchar(16) not null unique,
   primary key (`id`)
) engine = InnoDB
  default charset = utf8mb4;
-- 角色
drop table if exists `character`;
create table `character`
(
   `id`       int         not null,
   `name`     varchar(16) not null unique,
   `type`     varchar(8)  not null,
   -- 这张表中,guild_id是外键
   `guild_id` int         not null,
   primary key (`id`),
   -- 指定外键建立关系
   foreign key (`guild_id`) references `guild` (`id`) on delete cascade on update cascade
) engine = InnoDB
  default charset = utf8mb4;
-- 武器-角色关系表
drop table if exists `weapon_character`;
create table `weapon_character`
(
   -- 这张表中,character_id、weapon_id都是外键
   `character_id` int not null,
   `weapon_id`    int not null,
   primary key (`character_id`, `weapon_id`),
   -- 指定外键建立联系
   foreign key (`character_id`) references `character` (`id`) on delete cascade on update cascade,
   foreign key (`weapon_id`) references `weapon` (`id`) on delete cascade on update cascade
) engine = InnoDB
  default charset = utf8mb4;
-- 初始化数据
insert into `guild`
values (0, '小小甜心'),
      (1, '恶魔伪王国军'),
      (2, '自卫团');
insert into `character`
values (0, '镜华', '后卫法师输出', 0),
      (1, '美美', '中卫物理输出', 0),
      (2, '未奏希', '前卫物理辅助', 0),
      (3, '宫子', '前卫物理坦克', 1),
      (4, '茜里', '中卫法师辅助', 1),
      (5, '伊莉亚', '中卫法师输出', 1),
      (6, '真步', '后卫法师辅助', 2),
      (7, '真琴', '前卫物理输出', 2),
      (8, '香澄', '后卫法师辅助', 2);
insert into `weapon`
values (0, '牺牲月渊杖'),
      (1, '细冰姬的蝴蝶结'),
      (2, '星域天球杖'),
      (3, '水之支配者神凑剑'),
      (4, '焰火牡丹花簪'),
      (5, '毁灭之伤冥神枪'),
      (6, '海龙神的发饰'),
      (7, '岚神风暴护手');
insert into `weapon_character`
values (0, 0),
      (0, 1),
      (1, 3),
      (1, 4),
      (2, 6),
      (2, 7),
      (3, 5),
      (3, 6),
      (4, 1),
      (4, 2),
      (5, 0),
      (5, 1),
      (6, 1),
      (6, 2),
      (7, 3),
      (7, 4),
      (8, 1),
      (8, 2);

我们看到,上述在建立角色表中指定了其中guild_id为外键,并指定了级联操作。在一个表创建之时指定外键,我们使用foreign key语句,上述角色表中,我们指定公会id字段为外键,并关联公会表的主键;建立角色-武器关联表时,我们同时指定其中角色id武器id字段为外键并关联角色武器表的主键,这样就建立起来了几个表的外键约束关系。

通常,外键和被关联的键的数据类型、长度必须完全一致!如果说一个表的主键是无符号自增主键,那么对应关联它的外键也应该是无符号的整数,例如:

-- 表a
create table `a` (
    `id` int unsigned auto_increment,
    ...,
    primary key (`id`)
)...;
-- 表b
create table `b` (
    `id` int unsigned auto_increment,
    `a_id` int unsigned not null, -- 注意这里
    ...,
    foreign key (`a_id`) references `a` (`id`) on delete cascade on update cascade,
    ...
)...;

上述表b中的外键是a_id关联表a的主键,由于表a的主键是无符号自增主键,因此字段a_id也应该设为整型无符号类型。如果只设定a_id为整型就会在关联外键时报错。

语句在最后面的on delete cascade on update cascade表示级联删除和更新,这样,譬如说我们删除一个角色的时候,在角色-武器关联表中关于这个角色的对应的记录也会被删除。

如果说创建表的时候没有添加外键约束,也可以在后续表创建好了后用alter table语句进行外键约束:

-- 把角色表中的公会id字段设为外键并与公会表主键关联
alter table `character` add foreign key (`guild_id`) references `guild` (`id`) on delete cascade on update cascade;

ok,这里来几个简单的查询例子。

查询角色“宫子”属于哪个公会:

select `guild`.*
from `guild`
       left join `character` on `guild`.id = `character`.guild_id
where `character`.name = '宫子';

结果:

image

查询公会“小小甜心”中的所有成员

select `character`.*
from `character`
       left join `guild` on `character`.guild_id = `guild`.id
where `guild`.name = '小小甜心';

结果:

image

查询角色“镜华”使用了哪些武器

select `weapon`.*
from `weapon`
       left join `weapon_character` on `weapon`.id = `weapon_character`.weapon_id
       left join `character` on `weapon_character`.character_id = `character`.id
where `character`.name = '镜华';

结果:

image

查询武器“细冰姬的蝴蝶结”被哪些角色使用

select `character`.*
from `character`
       left join `weapon_character` on `character`.id = `weapon_character`.character_id
       left join `weapon` on `weapon_character`.weapon_id = `weapon`.id
where `weapon`.name = '细冰姬的蝴蝶结';

结果:

image

可见,通过上述的方式建立了表之间的一对一、一对多的关系,可以实现灵活地双向查询,主要也是通过左连接,实现各种关系查询。

一对一的关系就更简单了,只需要把两者任意一个的主键作为另一者的外键即可。

平常在SSM开发中都是通过id查询,并且会在select时指定查询字段且起别名,这里方便起见,就通过名字查询。这里方便起见也没有使用自增主键,但是平时开发过程中是要使用的。

3,SSM中的级联

那么这样的关系,在MyBatis中又应当如何实现呢?一般是通过级联来实现。

还是先设计类图,因为这里存在着互相关联的关系,因此这里设计的类,会和平常简单情况下有点小区别。也就是说,数据库表字段和类属性不再完全一一对应。

我们做类图如下:

image

大家发现,图中紫色的属性,和上述数据库表结构有一些差异,并且没有设计“武器-角色”类,因为在类中,属性可以是复杂的数据结构,但是数据库表字段不行

也就是说,在这样的互相关联的数据中,我们将数据库表设计为Java的类时,需要做一定的“转换”:

一对多情况下,需要将“多”的表中的表示“一”的主键字段,设计为“多”的类中类型为“一”属性:

image

那么在多对多情况下,我们无需再设计其关系表的类,只需在两个对应的类中,加入一个List或者Set类型属性,表示其中关联互相即可:

image

好了,设计好了类,就需要编写MyBatis Mapper XML了,这里才是真正的难点。

为了看着方便,下面的示例XML文件我只贴出resultMap节点和select节点。

我们的DAO层只写查询的方法,这里就不贴DAO类的代码了。

先编写最简单的,公会的Mapper XML文件:

<resultMap id="guildResultMap" type="com.example.relationmapping.dataobject.Guild">
   <id column="id" property="id"/>
   <result column="name" property="name"/>
</resultMap>
<select id="getById" resultMap="guildResultMap">
   select *
   from `guild`
   where id = #{id}
</select>

这里不再多说,然后编写角色的:

<resultMap id="characterResultMap" type="com.example.relationmapping.dataobject.Character">
   <id column="id" property="id"/>
   <result column="name" property="name"/>
   <result column="type" property="type"/>
   <association property="guild" select="com.example.relationmapping.dao.GuildDAO.getById" column="guild_id" fetchType="lazy"/>
   <collection property="weapons" ofType="com.example.relationmapping.dataobject.Weapon" fetchType="lazy">
      <id column="weapon_id" property="id"/>
      <result column="weapon_name" property="name"/>
   </collection>
</resultMap>
<select id="getByName" resultMap="characterResultMap">
   select `character`.*, `weapon`.id as weapon_id, `weapon`.name as weapon_name
   from `character`
          left join `weapon_character` on `character`.id = `weapon_character`.character_id
          left join `weapon` on `weapon_character`.weapon_id = `weapon`.id
   where `character`.name = #{name}
</select>

这里重点是resultMap中的associationcollection节点,我们一一来看:

association表示一个复杂类型的关联,一般用于一对多中的“多”的resultMap中,表示“多”的类(角色)中间那个“一”的属性(公会),其上面各个属性意义:

  • property 表示这个复杂类型字段对应的类中的属性名
  • select 表示查询这个字段的数据库表方法,填写DAO中的方法全限定名
  • column 查询参数,用<select>节点语句查得的结果中某一个字段值作为参数,填入select指定的方法进行查询,多个参数使用逗号隔开
  • fetchType 一般设定为lazy表示懒查询,也就是说当查询了这个表但是没使用这个复杂字段时,就不会去查询这个字段,以提升性能,解决N+1问题

那么association是怎么工作的呢?我们来看看。

我们知道,resultMap的作用就是把数据库表和我们的Java类对应起来,在取出记录之后,把记录中的值赋给对应的类的相应属性。

上述例子中,角色表有idnametype、和guild_id,那么<select>节点也是会先将这些原始数据取出。

遇到了association,其中设定了参数(column)为guild_id字段,设定了查询方法(select)为GuildDAO中的getById,那么MyBatis会把取出记录中的guild_id字段值作为参数使用getById方法进行查询,查得一个Guild实例,将其赋给这个Character实例的guild属性上。可见,这里其实进行了两次查询操作

通过这个也知道了,select节点中一定要选择到guild_id这个字段,否则无法成功关联。

collection表示这个字段类型是个集合,多对多的类的resultMap中就使用collection表示自己的集合属性。其上面各个属性意义:

  • property 表示这个集合对应的类中的属性名,和上面类似
  • ofType 表示这个集合中元素的类型
  • fetchType 同上

collection中,我们定义了集合元素类的映射关系。

那么collection又是怎么工作的呢?

首先,我们来执行一下这个<select>节点中的语句看看会得到哪些结果:

image

我们应该只查询一个角色,但是出现了多个结果,且人物的信息是重复的,那么怎么把武器的信息合并为一个集合呢?很显然,collection起了作用。

在上述resultMap中,除了角色表的idtypename被赋给了角色实例的相应属性,guild_id通过association转换为了相应复杂属性,剩余的字段weapon_idweapon_name很显然被放进了Weapon类的实例,因为在collection中定义了对武器类的映射。就这样MyBatis生成了多个武器实例并将每一条记录值放进去,这些武器实例构成一个集合,赋给了角色类中的使用武器字段。

image

仔细琢磨<select>节点,也是通过多个表的连接,实现关联查询。也就是说,这里的collection是基于关联查询的级联

除此之外,在select语句中,我们使用as对武器的查询结果字段起了别名(把weapon表的id起别名为weapon_id,把name起别名为weapon_name),这是因为武器表中的idname字段和角色表的同名,若不起别名将其区分会导致resultMap映射时发生错误(因为resultMap中同时包含了角色表和武器表的映射)

这里有人会问:武器类中也有角色列表这个属性,为什么不写进collection里面?因为这样没必要,并且会发生无限循环。

好了,这样就完成了角色的XML编写,反过来武器的也是差不多,这里就不贴代码了。

最后我们测试测试,在Test里面写两个查询方法并调用:

/**
 * 获取角色信息
 *
 * @param name 角色名
 */
private void getCharacterInfo(String name) {
   Character xcw = characterDAO.getByName(name);
   System.out.println("角色名:" + xcw.getName());
   System.out.println("角色类型:" + xcw.getType());
   System.out.println("角色公会:" + xcw.getGuild().getName());
   System.out.println("角色武器:");
   for (Weapon weapon : xcw.getWeapons()) {
      System.out.println(" - " + weapon.getName());
   }
}
/**
 * 获取武器信息
 *
 * @param name 武器名
 */
private void getWeaponInfo(String name) {
   Weapon weapon = weaponDAO.getByName(name);
   System.out.println("武器名:" + weapon.getName());
   System.out.println("使用该武器角色:");
   for (Character character : weapon.getCharacters()) {
      System.out.println(" - " + character.getName() + " " + character.getType() + " " + character.getGuild().getName());
   }
}
@Test
void contextLoads() {
   getCharacterInfo("真步");
   getWeaponInfo("水之支配者神凑剑");
}

结果:

image

其实collection中还可以嵌套collection或者association,大家可以自行尝试。当然实际开发中不能将数据结构设计得过于复杂,影响数据库执行效率。

4,总结

数据库是抽象的,后端的数据建模也是抽象的,明确数据模型之间的关系,并实现它们的互相关联非常重要。这里尤其是MyBatis的级联还是有一定难度,需要大致理解其原理、各个参数对应的意义,以及关联查询等等。

示例仓库地址

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
spring整合mybatis、springMVC(总结)
spring整合mybatis、springMVC(总结)
18 0
Spring整合Mybatis&Junit单元测试
Spring整合Mybatis&Junit单元测试
39 0
Spring Boot2.x-10 基于Spring Boot 2.1.2 + Mybatis 2.0.0实现多数据源,支持事务
Spring Boot2.x-10 基于Spring Boot 2.1.2 + Mybatis 2.0.0实现多数据源,支持事务
7377 0
Spring Boot2.x-09 基于Spring Boot 2.1.2 + Mybatis使用自定义注解实现数据库切换
Spring Boot2.x-09 基于Spring Boot 2.1.2 + Mybatis使用自定义注解实现数据库切换
23 0
Spring Boot2.x-07Spring Boot2.1.2整合Mybatis
Spring Boot2.x-07Spring Boot2.1.2整合Mybatis
15 0
【Spring】使用 MyBatis 操作数据库
1. MyBatis 是什么 2. 为什么要使用 MyBatis 3. MyBatis 框架交互流程 4. 配合 Spring 来使用 MyBatis 4.1 添加 MyBatis 框架支持 4.2 配置数据库 4.3 添加实体类 4.4 添加 mapper 接口 4.5 通过注解的方式操作数据库 4.5.1 增加用户 4.5.2 删除用户 4.5.3 修改用户数据 4.5.4 查询用户数据 4.6 通过 XML 文件的形式操作数据库 4.6.1 配置 XML 文件 4.6.2 在 yml 中配置 xml 路径 4.6.3 查询用户数据 4.6.4 批量查询用户数据 4.6.5 批量插入用户数
38 0
Mybatis是如何向Spring注册Mapper的?
有时候我们需要自行定义一些注解来标记某些特定功能的类并将它们注入Spring IoC容器。比较有代表性的就是Mybatis的Mapper接口。假如有一个新的需求让你也实现类似的功能你该如何下手呢?今天我们就从Mybatis的相关功能入手来学习其思路并为我所用。
43 0
Java学习路线-60:spring 整合 mybatis
Java学习路线-60:spring 整合 mybatis
50 0
Spring+SpringMVC+MyBatis整合Druid之入门
Spring+SpringMVC+MyBatis整合Druid之入门
124 0
Spring、SpringMVC、MyBatis整合
Spring、SpringMVC、MyBatis整合
102 0
SSM框架整合(Spring + SpringMVC + Mybatis)(二)
SSM框架整合(Spring + SpringMVC + Mybatis)
35 0
SSM框架整合(Spring + SpringMVC + Mybatis)(一)
SSM框架整合(Spring + SpringMVC + Mybatis)
65 0
Spring+Mybatis整合核心知识
Spring+Mybatis整合核心知识点
50 0
MyBatis 学习笔记(三)MyBatis与Spring 和SpringBoot整合
接上一篇MyBatis 学习笔记(二)MyBatis常用特性运用 在真实的项目我们几乎不会将MyBatis 单独运用到项目中,而是将其整合到Spring框架或者SpringBoot中,本文将通过两个demo演示MyBatis 与Spring和SpringBoot的整合。
57 0
+关注
守望时空33
业余Java开发者。
文章
问答
视频
相关电子书
更多
Spring框架入门
立即下载
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
相关实验场景
更多