关系数据库系统中,对数据模型的多对多关系进行管理和操作,是非常常见的。经典的关系数据库系统使用中间关联表的方式进行处理,在处理少量和简单的数据时候,略显繁复。针对这个情况,本文提出了一种基于Postgres数组类型和相关操作,来处理多对多关系的数组的方式。在一些特定场景下,更加简单和方便。除此之外,笔者还希望通过相关技术和实现方式的探讨,帮助读者能够从不同的角度来分析问题,了解一些不同的知识和技能,并且拓展解决问题的思路和方式。
在实际内容开始之前,我们有必要先了解和熟悉一下关系数据库中几种比较常见的数据关联方式。
关系数据库中的数据关系
在关系型数据库中,任意两个有关联的数据记录集合,在数据记录之间的关系,都可以用三种类型来进行描述:
- 一对一(One-To-One)
严格而言,在数据库中实现和表达两个实体记录集合的记录之间的一对一的关系是比较多余的,它完全可以通过只使用一个表,但增加多个字段的方式来实现(即所谓的“宽表”)。
但在现实的应用场景中,可能基于业务区隔,方便管理和扩展,以及处理性能等多方面的考虑,也可能会设计多个表,并使用一对一的关系将它们关联起来。这一关系和本文主题关联不大,这里不深入讨论。
- 一对多(One-To-Many)
一对多关系,通常可以用于描述和记录树形结构(父子)的数据。一个记录(实体)可以关联多个其他记录,而每个被关联的实体只能关联一个实体。常见的例子是公司部门和员工之间的关系,每个部门可以有多个员工,而每个员工只能属于一个部门。这就是一个典型的树形结构的数据集合。
在关系型数据库的设计和实现中,这种结构通常使用外键和关联的方式来实现。在具有父子记录关系的两个表中,在子记录表上,增加一个字段作为外键来关联父表的记录ID,就可以将两类记录关联起来(详见示例数据)。在这种情况下,常见的数据处理需求和实现方式包括:
-- 建立/修改父子关联关系: 使用父记录ID更新子表中关联字段的内容
update employees set dep= 101 where id = 201 returning *;
-- 清除关联关系: 清除关联字段中的内容
-- 注意: 严格的外键定义不允许更新为不存在的值,会影响灵活性和性能
update employees set dep= nulll where id = 201 returning *;
-- 基于父记录查询子记录:从父记录表出发,直接连接查询(join)子记录表,可以有多条(或没有)
select D.*, E.id empid, E.name empname from departments D
join employees E on E.dep = D.id and D.id = 102;
-- 基于子记录查询父记录:从子记录表出发,关联查询父记录表, 应该只有1条
select E.*, D.id depid, D.name depname from employees E
join departments D on E.dep = D.id and E.id = 202;
- 多对多(Many-To-Many,下简称M2M)
多对多也是一种常见的数据和信息关联的类型。实体可以关联多个其他的实体,而被关联实体也可以关联多个实体,这样其实就可以在实体之间建立一种网络化的关联关系。如果以公司员工和他们所参与的项目为例,每个项目可以有多个员工参与,而每个员工也可以参与多个项目(见示例数据)。
一般SQL数据库并没有提供内置和显式的多对多关系映射的功能,因为这个需求可以简单的通过两个一对多的关系来实现。具体而言,就是通过建立除两个关联实体的数据表之外,第三个数据关联表。这张表通常包含两个表的外键,分别用于关联两张表的记录。如建立项目和员工的关联表,应当包括项目表的项目ID和员工表的员工ID,这个表的内容将会记录和体现项目和员工之间的关系。
这种模式下,常见的多对多关系处理操作和实现包括:
-- 为避免重复数据,创建约束
-- 建立关联关系: 在关联表中增加一条记录
insert into
update employees set dep= 101 where id = 201 returning *;
-- 清除关联关系: 删除关联表中的相关记录
delete from employees set dep= nulll where id = 201 returning *;
-- 基于父记录查询子记录:从父记录表出发,直接连接查询(join)子记录表,可以有多条(或没有)
select D.*, E.id empid, E.name empname from departments D
join employees E on E.dep = D.id and D.id = 102;
-- 基于子记录查询父记录:从子记录表出发,关联查询父记录表, 应该只有1条
select E.*, D.id depid, D.name depname from employees E
join departments D on E.dep = D.id and E.id = 202;
上面都是经典的关系型数据库处理数据关系的方式,下面我们进入本文的核心内容,基于数组来管理多对多关系的数据。这里的讨论基于Postgres数据库的数组相关功能和特性。其他的关系型数据库,如果也支持类似的数组功能,应当也是可以的。
PG数组操作
我们先来了解一下PG相关数组操作的相关特性和功能,这些内容对于后续的数据关联处理非常重要。PG可以将一个字段定义为数组的类型,并提供了相关的运算符和函数可以对其进行操作。要注意到PG中,数组的索引是从1开始的,下面这些语句可以帮助我们来理解PG是如何进行数组类型字段的操作的。
-- 查询数组和数组元素
with D(l) as (values (Array[1,2,3,4,6]), (Array[2,1,4,6]))
select *,l[2],l[2] from D ;
-- 数组转记录
with D(i,l) as (values (1,Array[1,2,3,4,6]), (2,Array[2,1,4,6]))
select i, unnest(l) from D ;
-- 记录转数组
with D as (
select 100 + generate_series(1,10) id
) select array_agg(id) from D;
-- 扩展数组
with D(a) as (values ('{1,3,5,6,8}'::int[]))
select 100 || D.a, D.a || 101, array_append(D.a,103) from D;
-- 从数组中删除值
with D(a) as (values ('{1,3,5,6,8,1}'::int[]))
select array_remove(D.a,103), array_remove(D.a,1) from D;
-- 查询数组是否包括值
with D(a) as (values ('{1,3,5,6,8,1}'::int[]))
select Array[1]