SQL 中的循环关系本质上是错误的吗?
Are cyclic relationships in SQL inherently wrong?
我知道这个话题已经被多次提及。但是,我还没有找到 question/answer 讨论循环关系 一般 而不是在特定情况下。
假设我想模拟一个相当简单的情况:参加给定运动的团队。每支队伍可以有一名队长,也可以没有队长;每个玩家可以属于一个团队或不属于任何团队。
假设我使用以下代码将两个 table 添加到一个空数据库中。 (我最熟悉 SQLite,所以这就是我使用的语法。)
CREATE TABLE Team (
id INTEGER PRIMARY KEY,
team_name TEXT NOT NULL,
captain INTEGER,
FOREIGN KEY(captain) REFERENCES Player(id)
);
CREATE TABLE Player (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
team INTEGER,
FOREIGN KEY(team) REFERENCES Team(id)
);
现在让我们开始使用以下代码添加数据:
INSERT INTO Player (id, full_name) VALUES (1, 'Bob');
INSERT INTO Team (id, team_name, captain) VALUES (1, 'Team Bob', 1);
UPDATE Player SET team = 1 WHERE id = 1;
上面的代码运行没有错误,它做了我想要它做的事情。数据库设计是有意义的——至少在直观层面上是这样。 但是设计有问题吗,可能是因为两个table之间的循环关系?
我了解到,在面向对象编程 (OOP) 中,几乎在所有情况下都应避免循环依赖。 OOP 和数据库设计不是不相关的话题,但是循环关系是不是也应该普遍避免?
我想我可以重新设计上面的数据库如下:
CREATE TABLE Team (
id INTEGER PRIMARY KEY,
team_name TEXT NOT NULL,
);
CREATE TABLE Player (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
);
CREATE TABLE TeamCaptain (
team INTEGER PRIMARY KEY,
captain INTEGER UNIQUE,
FOREIGN KEY(team) REFERENCES Team(id),
FOREIGN KEY(captain) REFERENCES Player(id)
);
但是,我可以看到以下问题:
- 它涉及添加一个额外的 table。
- 它使设计不那么直观。
- 循环关系可以说是被混淆了,而不是被删除了。
你描述的关系没有问题。如果你要求一个团队有一个队长(即列是 NOT NULL
),这会变得很棘手,因为那时:
- 您不能添加没有队长的队伍。
- 你不能有队长,因为没有球员(直到球队存在)。
还有其他方法可以处理此问题,例如 players.is_captain
具有过滤唯一索引的列。不过你的方法没问题
我们不能说循环引用总是“错误的”。代码设计中很少有如此明确的事情。
即使是可怕的 GOTO 也有一些合法用途。
另一个例子可能是除以零。是的,除以零是未定义的,所以我们应该避免除以零。但这并不意味着我们应该避免 all 使用除法运算符。这只是意味着我们应该小心确保除数不为零。
Gordon Linoff 介绍了在创建具有循环引用的实体时必须特别注意的一些问题。
两种设计都有需要注意的问题
第一个更容易理解和操作,但是一个团队的队长也必须属于该团队的约束呢?
事实上,对此没有任何限制。
您可以将球队的队长设置为任何球员的 id,即使该球员不属于该球队。
当然,您可以在应用程序级别设置此约束,但如果它是通过设计设置的,那么它会更安全,因为它需要一个触发器(这不是什么坏事,相反,这是常见的做法)。
你的第二个设计使用了关联table,这也是常见的做法。
我会拒绝它,不是因为如你所说,它 不够直观 ,而是因为它会添加一个额外的 table 只是为了定义船长的简单要求团队.
也考虑这个设计:
CREATE TABLE Team (
id INTEGER PRIMARY KEY,
team_name TEXT NOT NULL
);
CREATE TABLE Player (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL
);
CREATE TABLE TeamPlayer (
team_id INTEGER NOT NULL,
player_id INTEGER NOT NULL UNIQUE,
is_captain INTEGER NOT NULL DEFAULT 0 CHECK(is_captain IN (0, 1)),
FOREIGN KEY(team_id) REFERENCES Team(id) ON DELETE CASCADE,
FOREIGN KEY(player_id) REFERENCES Player(id) ON DELETE CASCADE,
PRIMARY KEY(team_id, player_id)
);
CREATE UNIQUE INDEX idx_teamplayer_captain
ON TeamPlayer(team_id, is_captain) WHERE is_captain = 1; -- each team can have only 1 captain
它将实体 Team
和 Player
分开,并使用关联 table 定义它们之间的 所有 关系。
我喜欢这个设计的一点是,一旦你拥有了你的球队和球员,所有交易都只针对 table 完成,所有约束都只设置为 table。
当然,也有一些需要注意的地方,比如当你更换队长时:
首先你必须将当前队长的is_captain
值设置为0
,然后再改变新队长的is_captain
值改为1
.
你想走哪条路,是你满足所有要求后的决定,我想这可能比队长的定义更进一步。
我知道这个话题已经被多次提及。但是,我还没有找到 question/answer 讨论循环关系 一般 而不是在特定情况下。
假设我想模拟一个相当简单的情况:参加给定运动的团队。每支队伍可以有一名队长,也可以没有队长;每个玩家可以属于一个团队或不属于任何团队。
假设我使用以下代码将两个 table 添加到一个空数据库中。 (我最熟悉 SQLite,所以这就是我使用的语法。)
CREATE TABLE Team (
id INTEGER PRIMARY KEY,
team_name TEXT NOT NULL,
captain INTEGER,
FOREIGN KEY(captain) REFERENCES Player(id)
);
CREATE TABLE Player (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
team INTEGER,
FOREIGN KEY(team) REFERENCES Team(id)
);
现在让我们开始使用以下代码添加数据:
INSERT INTO Player (id, full_name) VALUES (1, 'Bob');
INSERT INTO Team (id, team_name, captain) VALUES (1, 'Team Bob', 1);
UPDATE Player SET team = 1 WHERE id = 1;
上面的代码运行没有错误,它做了我想要它做的事情。数据库设计是有意义的——至少在直观层面上是这样。 但是设计有问题吗,可能是因为两个table之间的循环关系?
我了解到,在面向对象编程 (OOP) 中,几乎在所有情况下都应避免循环依赖。 OOP 和数据库设计不是不相关的话题,但是循环关系是不是也应该普遍避免?
我想我可以重新设计上面的数据库如下:
CREATE TABLE Team (
id INTEGER PRIMARY KEY,
team_name TEXT NOT NULL,
);
CREATE TABLE Player (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL,
);
CREATE TABLE TeamCaptain (
team INTEGER PRIMARY KEY,
captain INTEGER UNIQUE,
FOREIGN KEY(team) REFERENCES Team(id),
FOREIGN KEY(captain) REFERENCES Player(id)
);
但是,我可以看到以下问题:
- 它涉及添加一个额外的 table。
- 它使设计不那么直观。
- 循环关系可以说是被混淆了,而不是被删除了。
你描述的关系没有问题。如果你要求一个团队有一个队长(即列是 NOT NULL
),这会变得很棘手,因为那时:
- 您不能添加没有队长的队伍。
- 你不能有队长,因为没有球员(直到球队存在)。
还有其他方法可以处理此问题,例如 players.is_captain
具有过滤唯一索引的列。不过你的方法没问题
我们不能说循环引用总是“错误的”。代码设计中很少有如此明确的事情。
即使是可怕的 GOTO 也有一些合法用途。
另一个例子可能是除以零。是的,除以零是未定义的,所以我们应该避免除以零。但这并不意味着我们应该避免 all 使用除法运算符。这只是意味着我们应该小心确保除数不为零。
Gordon Linoff 介绍了在创建具有循环引用的实体时必须特别注意的一些问题。
两种设计都有需要注意的问题
第一个更容易理解和操作,但是一个团队的队长也必须属于该团队的约束呢?
事实上,对此没有任何限制。
您可以将球队的队长设置为任何球员的 id,即使该球员不属于该球队。
当然,您可以在应用程序级别设置此约束,但如果它是通过设计设置的,那么它会更安全,因为它需要一个触发器(这不是什么坏事,相反,这是常见的做法)。
你的第二个设计使用了关联table,这也是常见的做法。
我会拒绝它,不是因为如你所说,它 不够直观 ,而是因为它会添加一个额外的 table 只是为了定义船长的简单要求团队.
也考虑这个设计:
CREATE TABLE Team (
id INTEGER PRIMARY KEY,
team_name TEXT NOT NULL
);
CREATE TABLE Player (
id INTEGER PRIMARY KEY,
full_name TEXT NOT NULL
);
CREATE TABLE TeamPlayer (
team_id INTEGER NOT NULL,
player_id INTEGER NOT NULL UNIQUE,
is_captain INTEGER NOT NULL DEFAULT 0 CHECK(is_captain IN (0, 1)),
FOREIGN KEY(team_id) REFERENCES Team(id) ON DELETE CASCADE,
FOREIGN KEY(player_id) REFERENCES Player(id) ON DELETE CASCADE,
PRIMARY KEY(team_id, player_id)
);
CREATE UNIQUE INDEX idx_teamplayer_captain
ON TeamPlayer(team_id, is_captain) WHERE is_captain = 1; -- each team can have only 1 captain
它将实体 Team
和 Player
分开,并使用关联 table 定义它们之间的 所有 关系。
我喜欢这个设计的一点是,一旦你拥有了你的球队和球员,所有交易都只针对 table 完成,所有约束都只设置为 table。
当然,也有一些需要注意的地方,比如当你更换队长时:
首先你必须将当前队长的is_captain
值设置为0
,然后再改变新队长的is_captain
值改为1
.
你想走哪条路,是你满足所有要求后的决定,我想这可能比队长的定义更进一步。