假设您的 table_item 如下所示:
create table table_item (
item_id int unsigned auto_increment not null,
record varchar(50),
primary key (item_id)
);
insert into table_item (record) values
('Record A'),
('Record B'),
('Record C'),
('Record D'),
('Record E'),
('Record F'),
('Record G'),
('Record H');
table_item_linked 可能是
create table table_item_linked (
linked_id int unsigned auto_increment not null,
item1_id int unsigned not null,
item2_id int unsigned not null,
linked_by int unsigned not null,
linked_timestamp timestamp not null default now(),
primary key (linked_id),
unique key (item1_id, item2_id),
index (item2_id, item1_id),
foreign key (item1_id) references table_item(item_id),
foreign key (item2_id) references table_item(item_id)
);
这基本上是同一类型的项目之间的多对多关系。
请注意,这里通常不需要 AUTO_INCREMENT 列。您可以删除它,并将(item1_id, item2_id) 定义为PRIMARY KEY。而linked_by 应该是引用users 表的 FOREGN KEY。
如果用户(ID 123)想要将“记录 A”(item_id = 1)与“记录 B”(item_id = 2)和“记录 B”(item_id = 2)与“记录 C”链接(item_id = 3),您的 INSERT 语句将是:
insert into table_item_linked (item1_id, item2_id, linked_by) values (1, 2, 123);
insert into table_item_linked (item1_id, item2_id, linked_by) values (2, 3, 123);
现在 - 当用户选择“记录 A”(item_id = 1) 时,您可以通过递归查询获取所有相关项(至少需要 MySQL 8.0 或 MariaDB 10.2):
set @input_item_id = 1;
with recursive input as (
select @input_item_id as item_id
), rcte as (
select item_id from input
union distinct
select t.item2_id as item_id
from rcte r
join table_item_linked t on t.item1_id = r.item_id
union distinct
select t.item1_id as item_id
from rcte r
join table_item_linked t on t.item2_id = r.item_id
)
select i.*
from rcte r
join table_item i on i.item_id = r.item_id
where r.item_id <> (select item_id from input)
结果将是:
item_id record
———————————————————
2 Record B
3 Record C
db-fiddle
在您的应用程序中,您将删除 set @input_item_id = 1; 并使用占位符将 select @input_item_id as item_id 更改为 select ? as item_id。然后准备语句并将item_id绑定为参数。
更新
如果服务器不支持递归 CTE,您应该考虑将冗余数据存储在一个单独的表中,这样便于查询。 闭包表 是一种选择,但在这里不是必需的,并且可能会占用太多存储空间。我会将连接在一起(直接和间接)的项目分组到集群中。
给定与上面相同的架构,我们定义一个新表table_item_cluster:
create table table_item_cluster (
item_id int unsigned not null,
cluster_id int unsigned not null,
primary key (item_id),
index (cluster_id, item_id),
foreign key (item_id) references table_item(item_id)
);
此表将项目 (item_id) 链接到集群 (cluster_id)。由于一个项目只能属于一个集群,我们可以将item_id定义为主键。它也是一个引用table_item的外键。
创建新项目时,它不会连接到任何其他项目并构建自己的集群。所以当我们插入一个新项目时,我们还需要在table_item_cluster 中插入一个新行。为简单起见,我们通过item_id (item_id = cluster_id) 来识别集群。这可以在应用程序代码中完成,也可以使用以下触发器:
delimiter //
create trigger table_item_after_insert
after insert on table_item
for each row begin
-- create a new cluster for the new item
insert into table_item_cluster (item_id, cluster_id)
values (new.item_id, new.item_id);
end//
delimiter ;
当我们链接两个项目时,我们只是合并它们的集群。来自两个合并集群的所有项目的cluster_id 现在需要相同。在这里,我只取两者中的至少一个。同样 - 我们可以在应用程序代码中或使用触发器来做到这一点:
delimiter //
create trigger table_item_linked_after_insert
after insert on table_item_linked
for each row begin
declare cluster1_id, cluster2_id int unsigned;
set cluster1_id = (
select c.cluster_id
from table_item_cluster c
where c.item_id = new.item1_id
);
set cluster2_id = (
select c.cluster_id
from table_item_cluster c
where c.item_id = new.item2_id
);
-- merge the linked clusters
update table_item_cluster c
set c.cluster_id = least(cluster1_id, cluster2_id)
where c.item_id in (cluster1_id, cluster2_id);
end//
delimiter ;
现在 - 当我们有一个项目并想要获取所有(直接和间接)链接的项目时,我们只需从同一个集群中选择所有项目(给定项目除外):
select i.*
from table_item i
join table_item_cluster c on c.item_id = i.item_id
join table_item_cluster c1
on c1.cluster_id = c.cluster_id
and c1.item_id <> c.item_id -- exclude the given item
where c1.item_id = ?
db-fiddle
c1.item_id = 1(“记录 A”)的结果是:
item_id record
———————————————————
2 Record B
3 Record C
但是:在处理冗余数据时几乎总是如此 - 使其与源数据保持同步可能会变得相当复杂。虽然添加和合并集群很简单 - 当您需要删除/删除项目或链接时,您可能需要拆分集群,这可能需要编写递归或迭代代码来确定哪些项目属于同一个集群。虽然一个简单(和“愚蠢”)的算法是删除并重新插入所有受影响的项目和链接,然后让插入触发器完成它的工作。
更新 2
最后但同样重要的是:您可以编写一个存储过程,它将遍历链接:
delimiter //
create procedure get_linked_items(in in_item_id int unsigned)
begin
set @ids := concat(in_item_id);
set @ids_next := @ids;
set @sql_tpl := "
select group_concat(distinct id order by id) into @ids_next
from (
select item2_id as id
from table_item_linked
where item1_id in ({params_in})
and item2_id not in ({params_not_in})
union all
select item1_id
from table_item_linked
where item2_id in ({params_in})
and item1_id not in ({params_not_in})
) x
";
while (@ids_next is not null) do
set @sql := @sql_tpl;
set @sql := replace(@sql, '{params_in}', @ids_next);
set @sql := replace(@sql, '{params_not_in}', @ids);
prepare stmt from @sql;
execute stmt;
set @ids := concat_ws(',', @ids, @ids_next);
end while;
set @sql := "
select *
from table_item
where item_id in ({params})
and item_id <> {in_item_id}
";
set @sql := replace(@sql, '{params}', @ids);
set @sql := replace(@sql, '{in_item_id}', in_item_id);
prepare stmt from @sql;
execute stmt;
end//
delimiter ;
要获取“记录 A”(item_id = 1) 的所有链接项目,您可以使用
call get_linked_items(1);
db-fiddle
用伪代码解释一下:
- 用输入参数初始化
@ids和@ids_next
- 查找与
@ids_next 中的任何ID 直接链接的所有项目ID,除了那些已经在@ids 中的项目ID
- 将结果存储到
@ids_next(覆盖它)
- 将 ID 从
@ids_next 附加到 @ids(将两个集合合并到 @ids)
- 如果
@ids_next 不为空:转到第 2 步。
- 返回 ID 为
@ids 的所有项目