将一个项目链接到同一个表中的另一个项目

Posted

技术标签:

【中文标题】将一个项目链接到同一个表中的另一个项目【英文标题】:Link one item to other item in same table 【发布时间】:2019-11-06 04:01:17 【问题描述】:

我搜索了很多,但一无所获。

我的场景是:

我有两个表table_itemtable_item_linked 的数据库。 table_item 有很多项目。用户会来添加项目。稍后其他用户通过带有两个dropdown 的表单来link 一个项目和其他项目。

到目前为止我所做的是:

table_item的结构:

+-------------------+
| table_item        |
+-------------------+
| item_id (Primary) |
| others            |
| ....              |
| ....              |
| ....              |
+-------------------+

table_item_linked的结构:

+---------------------+
| table_item_linked   |
+---------------------+
| linked_id           | (Primary)
| item_id             | (Foreign key referencing -> item_id of table_item) 
| linked_items        | (here I need to store ids of linked items)    
| linked_by           | (referencing to user_id of user_table)           
| linked_timestamp    | (timestamp) 
+---------------------+

如果我在table_item 中有项目,例如: ABCDEFGH

当我将DG 链接时

当我获取D 时,我可以成功获取G,反之亦然。但是当我遇到问题时

链接HG

所以我必须在获取G时获取DH

DHG 以各种方式链接,在获取一个时,必须附加并获取其余两个)

这就像一个多重关系(多对多关系)。

我知道必须有专业的方法来做到这一点。我希望得到任何指导。我什至可以改变我的数据库结构。

PS: 请不要建议添加#tag,因为其中一项与另一项完全相同。

更新

前端看起来像这样。如果我打算链接两条记录,我将有两个 dropdowns,如图所示:

如果我检查记录的详细信息A

如果我检查记录的详细信息B

如果我检查记录的详细信息C

【问题讨论】:

我什至不明白你到底想要什么 - 如果我链接 HG 比通常你没有从 GH 的链接,除非你想要它们 - 但是首先引起我注意的是您的专栏linked_items-永远不要以逗号分隔的方式存储关系-只需标准化您的数据-我建议您提供一些示例数据您的问题到底是什么,因为现在我无法弄清楚您的问题... 对不起,我不是很清楚。想想一个真实的场景,我们有一种语言的一个“词”,我想将它链接到另一种语言的另一个词(如翻译),所以我正在查看一个词,我必须有来自附加到它的其他语言也是如此。是的,我想将HGGH 链接到任何其他项目。如果改变我的“noob-ish”数据库设计,我愿意接受建议。 "Parent:child" 在单个表中很容易实现。但这是一个没有循环的 1:many 关系。 Many:many 需要一个额外的表。 请用英语和外来词代替G和H。仔细挑选例子,说英语的人不需要知道其他语言。示例:mouse:Maus 和 car:auto 表示英语:德语。 日语有十几种翻译“不”的方法——这取决于你需要多礼貌。这是否意味着您希望将“不”与 12 个日语单词“联系起来”? 【参考方案1】:

显而易见的解决方案是在table_item_linked 中为每个链接存储一行。

你的桌子就变成了

+---------------------+
| table_item_linked   |
+---------------------+
| linked_id           | (Primary
| from_item_id        | (The item linked _from_ -> item_id of table_item) 
| to_item_id          | the item linked _to_  
| linked_by           | (referencing to user_id of user_table)           
| linked_timestamp    | (timestamp) 
+---------------------+

在您的示例中,数据为:

linked_id     from_item_id    to_item_id   linked_by   linked_timestamp
------------------------------------------------------------------------
1                        D            H            sd      '1 jan 2020'
2                        H            G            sa      '2 Jan 2020'

然后你需要写一个hierarchical query来检索G的所有“孩子”。

【讨论】:

谢谢先生,我正在做志愿者项目,任何人都可以添加/编辑谚语,并将一种语言的谚语链接到另一种语言的其他谚语。你可以查看here,谚语可以链接here,我将删除此评论。 PS:我已经按照您的指示修改了我的表格,并努力为它编写查询。很快就会回来。 尊敬的先生@Neville Kuyt 非常感谢。您的回答确实为我打开了很多窗口,让我了解到我仍在学习很长时间,但我无法编写此查询。你能帮我写上面的分层查询吗?我什至无法入睡。先生,请编辑您的答案并包括检索G 的所有孩子的查询,谢谢。【参考方案2】:

假设您的 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 的所有项目

【讨论】:

嗨,谢谢。我尝试使用您提供的脚本创建table_item_linked,但它给了我错误:MySQL said: [Ref1] #1005 - Can't create table mydb.table_item_linked (errno: 150 "Foreign key constraint is incorrectly formed") ([Details…](http://localhost/phpmyadmin/server_engines.php?engine=InnoDB&amp;page=Status) ) 好像我正在使用XAMPP Control Panel v3.2.3Version: '10.1.39-MariaDB' 问题是我的Cpanel 上没有v10.2+:/ Ref1 @fWd82 item1_iditem2_id 的 DATA TYPE 必须与 table_item 中的 item_id 完全相同。由于您没有发布架构,所以我不知道它是哪个。 您使用的是哪个 GUI?使用 GUI 编写触发器时,您可能只需要从 BEGINEND 的代码。 对于 UPDATE 2,您应该创建一个存储过程而不是触发器。 请阅读您已链接的answer。删除begin 之前和end 之后的所有内容。或者使用这个answer。

以上是关于将一个项目链接到同一个表中的另一个项目的主要内容,如果未能解决你的问题,请参考以下文章

通过传输功能将数据从一个项目复制到 BQ 中的另一个项目是不是需要成本?

从 netbeans 中的另一种形式在表中添加行

将pk传递到Django中的另一个模板

将代码文件移动到项目中的另一个文件下

将变量从一个表传递到另一个 PHP 页面中的另一个 [重复]

如何将一个表中的行合并到另一个表中的另一行