【问题标题】:Foreign key contraints in many-to-many relationships多对多关系中的外键约束
【发布时间】:2013-03-12 23:58:45
【问题描述】:

上下文

我们正在建立一个介绍博客。到数据库课程项目。

在我们的博客中,我们希望能够在Posts 上设置LabelsLabels 不能单独存在,只有与 Posts 相关时才会存在。这样,未被任何Posts 使用的Labels 不应留在数据库中。

多个Label可以属于一个Post,多个Post可以使用一个Label

我们同时使用 SQLite3(本地/测试)和 PostgreSQL(部署)。

实施

这是我们用来创建这两个表的 SQL(SQLite3 风格)以及关系表:

帖子

CREATE TABLE IF NOT EXISTS Posts(
   id INTEGER PRIMARY KEY AUTOINCREMENT,
   authorId INTEGER,
   title VARCHAR(255),
   content TEXT,
   imageURL VARCHAR(255),
   date DATETIME,
   FOREIGN KEY (authorId) REFERENCES Authors(id) ON DELETE SET NULL
)

标签

CREATE TABLE IF NOT EXISTS Labels(
   id INTEGER PRIMARY KEY AUTOINCREMENT,
   name VARCHAR(255) UNIQUE,
   -- This is not working:
   FOREIGN KEY (id) REFERENCES LabelPosts(labelId) ON DELETE CASCADE 
)

LabelPostsPost [1..*] -- * Label 之间的关系)

CREATE TABLE IF NOT EXISTS LabelPosts(
    postId INTEGER,
    labelId INTEGER,
    PRIMARY KEY (postId, labelId),
    FOREIGN KEY (postId) REFERENCES Posts(id) ON DELETE CASCADE
)

问题

  • 使用 SQLite3,当我从 LabelPosts 表中删除对它的所有引用时,Labels 不会从数据库中删除。我认为由于 Postgres 给出的原因,尽管 SQLite 在没有警告的情况下接受了该表。

  • PostgreSQL 抱怨 labelIdLabelPosts 中不是唯一的,这是正确的,也是必需的,因为它是多对多的:

pq: S:"ERROR" R:"transformFkeyCheckAttrs" L:"6511" C:"42830" F:"tablecmds.c"
M:"没有唯一约束匹配被引用表的给定键\"labelposts\""

所以我明白我的约束是错误的。但是我不知道如何正确地做到这一点。

【问题讨论】:

  • 你可以使用触发器吗?
  • 是的,但我对它们不太熟悉。
  • 这看起来不像是一种非常标准的做事方法。我可以看到您要实现的目标,但是表 Labels 上的外键约束不适合。你应该在LabelPosts 上做CONSTRAINT fk_LabelPosts_labelId FOREIGN KEY(labelId) REFERENCES Labels(labelId) 代替(在我看来)。其他一些小观察: 1. 通常建议以单数形式命名您的实体,即。在这种情况下,LabelPostLabelPost。 2. 我建议为所有约束命名——这样以后会更容易 DROP 不必要的约束。
  • 如果我切换关系上的约束,这意味着如果Label 已经存在,Post 只能与Label 形成关系,这不是我想要的。此外,它不会解决我希望Labels 在未被任何Post 引用时自动消失的问题。

标签: sql sqlite postgresql database-design many-to-many


【解决方案1】:

实现您所寻求的行为(从数据库中删除未使用的标签)的一种方法是使用触发器。

您可以尝试编写如下内容:

CREATE OR REPLACE TRIGGER tr_LabelPosts_chk_no_more_associated_posts 
AFTER DELETE ON LabelPosts 
FOR EACH ROW 
EXECUTE PROCEDURE f_LabelPosts_chk_no_more_associated_posts();


CREATE OR REPLACE FUNCTION f_LabelPosts_chk_no_more_associated_posts() 
RETURNS TRIGGER AS $$
DECLARE
    var_associated_post_count INTEGER;
BEGIN
    SELECT Count(*) AS associated_post_count INTO var_associated_post_count FROM LabelPosts WHERE labelId = OLD.labelId;
    IF(var_associated_post_count = 0) THEN
        DELETE FROM Labels WHERE labelId = OLD.labelId;
    END IF;
END
$$ LANGUAGE 'plpgsql';

基本上,这里发生的是:

  1. 从表Posts 中删除一行。
  2. 删除将级联到LabelPosts 中的所有关联行(感谢您的外键约束)。
  3. 删除LabelPosts 中的每一行后,触发器被激活,进而调用 PostgreSQL 函数。
  4. 该函数检查是否有任何其他帖子与相关labelId 相关联。如果是这样,那么它会在没有任何进一步修改的情况下完成。但是,如果关系表中没有任何其他行,则标签不会在其他地方使用,因此可以删除。
  5. 该函数对Labels 表执行删除DML,有效地删除(现在)未使用的标签。

显然命名不是最好的,其中肯定存在大量语法错误,请参阅herehere 了解更多信息。可能有更好的方法来解决这个问题,但是目前我想不出一种不会破坏漂亮的通用表结构的快速方法。

尽管没有想到 - 使用触发器使数据库负担过重通常不是一个好习惯。它使每个关联的查询/语句运行速度变慢,也使管理变得更加困难。 (有时您需要禁用触发器来执行某些 DML 操作等,具体取决于触发器的性质)。

【讨论】:

  • +1 解释和建议(不是触发器实现)。
【解决方案2】:

我们同时使用 SQLite3(本地/测试)和 PostgreSQL(部署)。

这是自找麻烦。您将继续遇到轻微的不兼容问题。或者直到很久以后,当损坏完成时才注意到它们。 不要这样做。 也可以在本地使用 PostgreSQL。大多数操作系统都可以免费使用它。对于参与“数据库课程项目”的人来说,这是一个令人惊讶的愚蠢行为。相关:

其他建议:

所有东西放在一起,它可能看起来像这样:

CREATE TABLE IF NOT EXISTS post (
   post_id   serial PRIMARY KEY
 , author_id integer
 , title     text
 , content   text
 , image_url text
 , date      timestamp
);

CREATE TABLE IF NOT EXISTS label (
   label_id  serial PRIMARY KEY
 , name      text UNIQUE
);

CREATE TABLE IF NOT EXISTS label_post(
    post_id  integer REFERENCES post(post_id) ON UPDATE CASCADE ON DELETE CASCADE
  , label_id integer REFERENCES label(label_id) ON UPDATE CASCADE ON DELETE CASCADE
  , PRIMARY KEY (post_id, label_id)
);

触发器

要删除未使用的标签,请实施触发器。我提供另一个版本,因为我对the one provided by @Priidu 不满意:

CREATE OR REPLACE FUNCTION f_trg_kill_orphaned_label() 
  RETURNS trigger
  LANGUAGE plpgsql AS
$func$
BEGIN
   DELETE FROM label l
   WHERE  l.label_id = OLD.label_id
   AND    NOT EXISTS (
      SELECT 1 FROM label_post lp
      WHERE  lp.label_id = OLD.label_id
      );
END
$func$;
  • 触发器函数必须在之前触发器创建。

  • 一个简单的DELETE 命令就可以完成这项工作。不需要第二次查询 - 特别是不需要count(*)EXISTS 更便宜。

  • 语言名称可以使用单引号,但它确实是一个标识符,所以请省略废话:LANGUAGE plpgsql

CREATE TRIGGER label_post_delaft_kill_orphaned_label
AFTER DELETE ON label_post
FOR EACH ROW EXECUTE PROCEDURE f_trg_kill_orphaned_label();

PostgreSQL 中还没有CREATE OR REPLACE TRIGGERJust CREATE TRIGGER.

【讨论】:

  • 你应该更喜欢 Erwin 的触发器实现,因为他在一个原子语句中完成所有事情,这总是好的(不仅是为了更快)。
  • @Priidu .. 也最大限度地减少了竞争条件的机会。 :)
  • 哇,有很多好信息!我真的很感谢你的洞察力,我没有从课堂上得到这样的实用建议。
  • @ErwinBrandstetter 很好的答案!我有点不同意你所说的Use PostgreSQL locally, too. It's freely available for most every OS 通常开发人员并且不应该在他们的服务器上安装完整的DBMS,太多的维护和资源的损失。这样做的正确方法是使用像 ORM 这样的抽象来对用户隐藏所有这些 SQL 内容。在我以前的项目中,我几乎总是在本地使用 H2 Memory Store 和生产 oracle/mssql/postgres。允许快速开发和简单的开发设置。
  • @MohamedMansour:嗯,我不同意。 ORM 是原始的拐杖,通常无法充分发挥 RDBMS 的潜力。对于 Oracle,降低成本可能是有意义的,但这不适用于 PostgreSQL。即使您使用 ORM,使用不同的 RDBMS 也无济于事。你只能输。您需要快速查询并决定使用某个查询,因为它可以提供最佳性能?你猜怎么了?在生产数据库上并非如此......如果可以的话,使用相同的 RDBMS 进行开发!这很简单。
猜你喜欢
  • 2019-06-20
  • 1970-01-01
  • 1970-01-01
  • 2021-07-10
  • 2015-08-19
  • 2020-02-10
  • 1970-01-01
  • 2019-08-16
  • 1970-01-01
相关资源
最近更新 更多