多对多关系中的外国关键限制因素。

问题描述 投票:9回答:2

语境

我们正在为一个数据库入门课程项目建立一个博客。

在我们的博客中,我们希望能够设置为 Labels 关于 Posts. 该 Labels 不能单独存在,只有当它们与一个......相关时才会存在。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 
)

标签帖子 之间的关系 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抱怨说 labelId 不是唯一的 LabelPosts,这是真的,也是必须的,因为它是多对多的。

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

所以我明白我的约束是做错了。 然而我不知道如何正确地做。

sql sqlite postgresql database-design many-to-many
2个回答
22
投票

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

这是在自找麻烦。你会不断地遇到小的不兼容性。或者甚至直到很久以后才注意到它们,那时损害已经造成了。不要这样做。 在本地也使用PostgreSQL。它对大多数的操作系统都是免费的。对于一个参与 "数据库课程项目 "的人来说,这是一个令人惊讶的愚蠢行为。相关的。

其他建议。

  • @Priidu在评论中提到,你的外键约束是落后的。这不是争论的焦点,他们是 错的.

  • 在PostgreSQL中使用一个 serialIDENTITY 列(Postgres 10+)而不是SQLite。AUTOINCREMENT. 见。

  • 使用 timestamp (或 timestamptz) 而不是 datetime.

  • 不要使用混合大小写的标识符。

  • 不要使用非描述性的列名,如 id. 曾经。这是一个由半傻子中间件和ORMs引入的反模式。当你连接几个表时,你最终会得到多个名称为 id. 这是主动伤害。

  • 命名方式有很多,但大多数人都认为用单数词作为表名比较好。这样更简短,至少也是直观的逻辑。label,而不是 labels.

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

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

触发

要删除未使用的标签,请执行 触发. 我提供了另一个版本,因为我不满意 由@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();

没有... CREATE OR REPLACE TRIGGER 在PostgreSQL中,还没有。只是 CREATE TRIGGER.


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. 该函数在关系表中执行删除DML。Labels 表,有效地删除了(现在)未使用的标签。

很明显,这个命名并不是最好的,而且里面肯定有大量的语法错误,所以参见 此处此处 获取更多信息。也许有更好的方法来解决这个问题,但是目前我想不出一个快速的方法,不会破坏漂亮的通用表结构。

虽然要记住--用触发器给数据库带来过重的负担通常不是一个好的做法。它使每一个相关的查询语句运行得更慢&也使管理大大增加了难度。(有时你需要禁用触发器来执行某些DML操作,等等,这取决于你的触发器的性质)。

© www.soinside.com 2019 - 2024. All rights reserved.