我们正在为介绍构建一个博客。到数据库课程项目。
在我们的博客中,我们希望能够在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
)
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抱怨labelId
在LabelPosts
中并不是唯一的,这是真的也是必需的,因为它是多对多的:
pq:S:“ERROR”R:“transformFkeyCheckAttrs”L:“6511”C:“42830”F:“tablecmds.c”
的键
M:“没有唯一的约束匹配给定的引用表\”labelposts \“”
所以我明白我的约束是错误的。但是我不知道如何正确地做到这一点。
答案 0 :(得分:22)
我们正在使用SQLite3(本地/测试)和PostgreSQL(部署)。
这是在乞求麻烦。您将继续遇到轻微的不兼容性。或者甚至在很久以后,当损坏完成时才注意到它们。 不要这样做。也可以在本地使用PostgreSQL。它可以免费用于大多数操作系统。对于参与“数据库课程项目”的人来说,这是一个令人惊讶的愚蠢。
在PostgreSQL中使用serial
column而不是SQLite AUTOINCREMENT
使用timestamp
(or timestamptz
)代替datetime
。
请勿使用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 TRIGGER
。 Just 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';
基本上,这里发生的是:
Posts
中删除一行。 LabelPosts
中的所有关联行(由于您的外键约束)。LabelPosts
中的每一行后,触发器被激活,后者又调用PostgreSQL函数。labelId
相关联。如果是这样,那么它完成后无需进一步修改。但是,如果关系表中没有任何其他行,则标签不会在别处使用,因此可以删除。Labels
表上执行删除DML,有效删除(现在)未使用的标签。显然,命名不是最好的,并且必须存在大量语法错误,因此请参阅here和here以获取更多信息。可能有更好的方法来解决这个问题,但是目前我无法想到一个不会破坏漂亮的通用外观表结构的快速方法。
虽然记住了 - 通常使用触发器使数据库负担过重并不是一个好习惯。它使得每个相关的查询/语句运行得慢一些。也使管理变得更加困难。 (有时您需要禁用触发器来执行某些DML操作,具体取决于触发器的性质)。