多对多关系中的外键约束

时间:2013-03-23 00:40:39

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

上下文

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

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

多个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 
)

LabelPosts Post之间的关系[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时,如果从Labels表中删除对它的所有引用,则不会从数据库中删除LabelPosts。我认为由于Postgres给出的理由,尽管SQLite在没有警告的情况下接受了该表。

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

  

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

的键

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

2 个答案:

答案 0 :(得分:22)

  • 你的第一个大错误
  

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

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

  • 在PostgreSQL中使用serial column而不是SQLite AUTOINCREMENT 使用timestamp (or timestamptz)代替datetime

  • Don't use mixed case identifiers

  • 请勿使用id等非描述性列名。永远。这是由半智能中间件和ORM引入的反模式。当您加入几个表时,最终会有多个名为id的列。这是非常有害的。

  • 有许多命名样式,但大多数人认为最好将单数术语作为表名。它更短,至少是直观/合乎逻辑的。 label,而不是labels

  • 作为@Priidu mentioned in the comments,您的外键约束是向后的。这不是辩论,它们完全错误

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

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)
);

触发

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

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

  • 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

答案 1 :(得分:3)

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

您可以尝试编写类似的内容:

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,有效删除(现在)未使用的标签。
  6. 显然,命名不是最好的,并且必须存在大量语法错误,因此请参阅herehere以获取更多信息。可能有更好的方法来解决这个问题,但是目前我无法想到一个不会破坏漂亮的通用外观表结构的快速方法。

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