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

但是,我可以看到以下问题:

你描述的关系没有问题。如果你要求一个团队有一个队长(即列是 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

它将实体 TeamPlayer 分开,并使用关联 table 定义它们之间的 所有 关系。

我喜欢这个设计的一点是,一旦你拥有了你的球队和球员,所有交易都只针对 table 完成,所有约束都只设置为 table。
当然,也有一些需要注意的地方,比如当你更换队长时:
首先你必须将当前队长的is_captain值设置为0,然后再改变新队长的is_captain值改为1.

你想走哪条路,是你满足所有要求后的决定,我想这可能比队长的定义更进一步。