我们正在为一个数据库入门课程项目建立一个博客。
在我们的博客中,我们希望能够设置为 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\"
所以我明白我的约束是做错了。 然而我不知道如何正确地做。
我们同时使用SQLite3(本地测试)和PostgreSQL(部署)。
这是在自找麻烦。你会不断地遇到小的不兼容性。或者甚至直到很久以后才注意到它们,那时损害已经造成了。不要这样做。 在本地也使用PostgreSQL。它对大多数的操作系统都是免费的。对于一个参与 "数据库课程项目 "的人来说,这是一个令人惊讶的愚蠢行为。相关的。
其他建议。
如 @Priidu在评论中提到,你的外键约束是落后的。这不是争论的焦点,他们是 错的.
在PostgreSQL中使用一个 serial
或 IDENTITY
列(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
.
实现你所寻求的行为(从数据库中删除未使用的标签)的一种方法是使用触发器。
你可以尝试写这样的东西。
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操作,等等,这取决于你的触发器的性质)。