这是一个大问题。
在讨论期间,我将保留您的后续评论,这基本上是一个方便的问题:给定一个电话号码,能够轻松地知道它属于谁/属于什么会很好.
假设我们按照您的原始图表保持依赖关系的方向,其中电话号码“指向”其买方或供应商。那是什么样子的?
create table PhoneNumbers
(
phoneNumber varchar(16) primary key,
buyerId int foreign key references Buyers,
supplierId int foreign key references suppliers
);
这里的明显限制是,如果您添加更多种类的“有电话号码的事物”,您必须在电话号码表中添加越来越多的列。目前还不清楚一个电话号码是否应该同时适用于买家和供应商。只能有一个关联的 Id 不为空吗?如果添加更多相关内容,这将成为一个令人讨厌的检查约束。
但是,话虽如此,解决方案相当简单。如果你绝对肯定地知道你只有两种相关的类型,你可以“摆脱”这个。这很糟糕,但我们认识到这很糟糕,还是照做了。
但这方便吗?查询是什么样的?
select relatedThingType = case
when b.buyerId is not null then 'buyer'
when s.supplierId is not null then 'supplier'
else '????'
end
from PhoneNumbers p
left join Buyers b on b.buyerId = p.buyerId
left join Suppliers s on s.supplierId = p.supplierId
where p.phoneNumber = @phoneNumber
不太方便。我们在这里与模式作斗争,防止两者都为空的情况,根据我们的案例陈述的顺序任意决定如果两个外键都被填充,我们将选择买方而不是供应商,等等。
好的,如果我们按照建议和您更新的图表将其颠倒过来会怎样?
create table PhoneNumbers
(
phoneNumber varchar(16) primary key
);
create table Buyers
(
buyerId int primary key,
buyerName varchar(32),
phoneNumber varchar(16) foreign key references phoneNumbers
);
create table Suppliers
(
supplierId int primary key,
supplierName varchar(32),
phoneNumber varchar(16) foreign key references phoneNumbers
);
有一点很明显:给定一个电话号码,我们实际上不必加入 PhoneNumbers 表来找出谁拥有它 - 只要我们不使用代理 ID 而是使用电话号码本身作为键。
事实上,具有单列的表(因此必然是键)在查询中从来没有用过。它们的唯一功能是强制引用完整性(并使级联操作可用)。
好的,查询时间
select relatedThingType = 'buyer'
from Buyers
where phoneNumber = @phoneNumber
union all
select 'supplier'
from Suppliers
where phoneNumber = @phoneNumber;
基本上,join 变成了union。这样更方便吗?我认为,如果有一些“查询结构的整体大小”的度量标准,那将是大致相同的。但是我们不再那么频繁地与模式作斗争了。我们还“自然地”允许买家和供应商都拥有电话号码,如果发生这种情况,两者都会被退回。
因此,这些模型在可用基数方面存在功能差异,这也意味着查询语义存在差异。到目前为止,选择并不是真正基于“什么更方便”,而是基于“您的模型需要强制执行的基数是什么”?
但是...这个版本稍微更方便。添加新的 relatedThingType 不需要我们更改任何架构。我们只是将另一个 union all 附加到查询中。
好的,大揭秘时间。我一直在故意使用这个尴尬的术语relatedThingType。我们显然对这是什么没有一个清晰的概念。我们如何解决这个问题?
问题是,您实际上是在说“买家和供应商都有一个共同点:他们都是可以有电话号码的东西。但在其他方面它们是不同的”。
如果您进行任何非 sql 编程,那闻起来有点像继承关系。当您查看这两个表的模式时,会得到更多暗示。他们都有名字。所以他们有一个共同的属性。在 OOAD 中,这将是基类上的一个字段。
因此,解决此问题的一种方法是明确承认该基类。买家和供应商都是当事人,当事人可以有一个电话号码。
create table Parties
(
partyType varchar(16) check (partyType in ('buyer', 'supplier')),
partyId int primary key,
phoneNumber varchar(16),
partyName varchar(32),
constraint pk_p unique (partyType, partyId)
);
create table Buyers
(
partyId int primary key,
partyType varchar(16) default 'buyer' check (partyType = 'buyer'),
-- other buyer specific columns
constraint fk_b_p foreign key (partyType, partyId) references Parties(partyType, partyId)
);
create table Suppliers
(
partyId int primary key,
partyType varchar(16) default 'supplier' check (partyType = 'supplier'),
-- other supplier specific columns
foreign key (partyType, partyId) references Parties(partyType, partyId)
);
看起来不太方便。但是等等,这就是 schema。我们刚才讨论的查询呢?
select partyType, partyName, phoneNumber
from Parties
where phoneNumber = @phoneNumber
这很方便。
当然,您可以使用视图获得相同的结果,其定义与我之前编写的union all 查询基本相同。您是否真的要“实例化基类”只是一种选择。我们稍后会讨论如何做出选择...
但是模式中那些奇怪的默认和检查约束是怎么回事?他们正在执行的规则是,一方只能是任一供应商,或买方,但不能同时是两者。我们永远不会让买家和供应商意外共享partyId。这是一种“排他的子类型”关系。如果您需要强制执行此操作,则需要实例化基类。视图不会削减它。
我们可以走得更远,因为前一段暗示了另一种可能的情况:包含子类型关系的情况。一方可以是既是买家,又是供应商。为此,您将进入“基于角色的建模”模式,其中Party 具有一个或多个Roles,由PartyRoles 联结表确定。我通常将其称为“CounterParties”表,因为每一行都代表某个参与方在我们的观点中扮演某种角色 - 即,它们是由角色定义的某种特定类型的交易对手。
好的,它的架构是什么?嗯...我不会真的做这个。我会把它留作练习。
嗯,这一切都很好,很学术,但你还没有给我答案!
确实如此。但这里的想法不是给出“唯一的真实答案”。希望这一切都表明你的正确答案取决于你的模型需要建模的内容。这取决于您的域。买家也可以是供应商吗?当事人可以有多个电话号码吗?一个电话号码可以关联多个买家吗?这些问题的答案对你的模式必须做什么和不能做什么提出了严格的要求。您构建的任何模式都必须遵守这些约束。我提供的各种选项都遵循不同的域约束集,它们并不全都等价。
所以,你的问题的答案是......你会恨我......
“视情况而定”。
请原谅 DDL 中的任何语法错误。它只是为了说明概念