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

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

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

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

1,概述

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

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

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

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

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

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

这里可见:

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

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

2,数据库表的设计

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

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

网络异常,图片无法展示
|

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

网络异常,图片无法展示
|

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

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

网络异常,图片无法展示
|

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

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

网络异常,图片无法展示
|

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

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

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

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

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

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

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

-- 表acreatetable `a` (    `id` intunsigned auto_increment,    ...,    primary key (`id`))...;-- 表bcreatetable `b` (    `id` intunsigned auto_increment,    `a_id` intunsignednotnull,-- 注意这里    ...,    foreign key (`a_id`) references `a` (`id`)ondelete cascade onupdate cascade,    ...
)...;

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

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

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

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

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

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

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

结果:

网络异常,图片无法展示
|

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

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

结果:

网络异常,图片无法展示
|

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

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

结果:

网络异常,图片无法展示
|

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

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

结果:

网络异常,图片无法展示
|

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

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

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

3,SSM中的级联

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

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

我们做类图如下:

网络异常,图片无法展示
|

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

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

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

网络异常,图片无法展示
|

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

网络异常,图片无法展示
|

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

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

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

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

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

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

<resultMapid="characterResultMap"type="com.example.relationmapping.dataobject.Character"><idcolumn="id"property="id"/><resultcolumn="name"property="name"/><resultcolumn="type"property="type"/><associationproperty="guild"select="com.example.relationmapping.dao.GuildDAO.getById"column="guild_id"fetchType="lazy"/><collectionproperty="weapons"ofType="com.example.relationmapping.dataobject.Weapon"fetchType="lazy"><idcolumn="weapon_id"property="id"/><resultcolumn="weapon_name"property="name"/></collection></resultMap><selectid="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>节点中的语句看看会得到哪些结果:

网络异常,图片无法展示
|

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

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

网络异常,图片无法展示
|

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

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

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

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

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

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

结果:

网络异常,图片无法展示
|

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

4,总结

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

示例仓库地址

相关文章
|
26天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
43 4
SpringBoot入门(4) - 添加内存数据库H2
|
20天前
|
SQL Java 数据库连接
深入 MyBatis-Plus 插件:解锁高级数据库功能
Mybatis-Plus 提供了丰富的插件机制,这些插件可以帮助开发者更方便地扩展 Mybatis 的功能,提升开发效率、优化性能和实现一些常用的功能。
114 26
深入 MyBatis-Plus 插件:解锁高级数据库功能
|
25天前
|
SQL 安全 Java
MyBatis-Plus条件构造器:构建安全、高效的数据库查询
MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的 SQL 语句,从而提高开发效率并减少 SQL 注入的风险。
14 1
MyBatis-Plus条件构造器:构建安全、高效的数据库查询
|
29天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
31 2
SpringBoot入门(4) - 添加内存数据库H2
|
12天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
24 2
|
21天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
60 13
|
16天前
|
Java 数据库连接 测试技术
SpringBoot入门(4) - 添加内存数据库H2
SpringBoot入门(4) - 添加内存数据库H2
34 4
|
25天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
18天前
|
存储 安全 Java
springboot当中ConfigurationProperties注解作用跟数据库存入有啥区别
`@ConfigurationProperties`注解和数据库存储配置信息各有优劣,适用于不同的应用场景。`@ConfigurationProperties`提供了类型安全和模块化的配置管理方式,适合静态和简单配置。而数据库存储配置信息提供了动态更新和集中管理的能力,适合需要频繁变化和集中管理的配置需求。在实际项目中,可以根据具体需求选择合适的配置管理方式,或者结合使用这两种方式,实现灵活高效的配置管理。
13 0
|
1月前
|
Java 测试技术 开发者
springboot学习四:Spring Boot profile多环境配置、devtools热部署
这篇文章主要介绍了如何在Spring Boot中进行多环境配置以及如何整合DevTools实现热部署,以提高开发效率。
67 2
下一篇
无影云桌面