如何在没有复合键的情况下强制执行二级关系?

时间:2016-06-10 08:31:05

标签: database-design

考虑这种多租户业务线Web应用程序的数据库设计:

Tenant是网络应用的租户,Tenant有许多ShopsCustomers之间的CustomerTenants条记录不在{ {1}},因此它对多个Customer记录有效,可以引用同一个真实的人类),每个Shop都有许多JobsJob也与Customer相关联。

存在一个问题,似乎没有一个简单的约束解决方案来防止Job的{​​{1}}更改为CustomerId而不是Customer的情况属于父Tenant,因此会产生无效数据。

以下是目前的架构:

CREATE TABLE Tenants (
    TenantId bigint IDENTITY(1,1) PRIMARY KEY
    ...
)

CREATE TABLE Shops (
    TenantId bigint FOREIGN KEY( Tenants.TenantId ),
    ShopId   bigint IDENTITY(1,1) PRIMAREY KEY,
    ...
)

CREATE TABLE Customers (
    TenantId   bigint FOREIGN KEY( Tenants.TenantId ),
    CustomerId bigint IDENTITY(1,1) PRIMARY KEY
    ...
)

CREATE TABLE Jobs (
    ShopId bigint FOREIGN KEY( Shops.ShopId )
    JobId bigint IDENTITY(1,1) PRIMARY KEY,
    CustomerId bigint FOREIGN KEY( Customers.CustomerId )
)

目前我能想到的唯一解决方案是将设计更改为使用始终包含父Tenant.TenantId的复合键,然后相应地共享它们:

CREATE TABLE Shops (
    TenantId bigint,
    ShopId   bigint IDENTITY(1,1),
    ...

    PRIMARY KEY( TenantId, ShopId )
    FOREIGN KEY( TenantId REFERENCES Tenants (TenantId) )
)

CREATE TABLE Customers (
    TenantId   bigint,
    CustomerId bigint IDENTITY(1,1)
    ...

    PRIMARY KEY( TenantId, CustomerId )
    FOREIGN KEY( TenantId REFERENCES Tenants (TenantId) )
)

CREATE TABLE Jobs (
    TenantId bigint
    ShopId bigint
    JobId bigint IDENTITY(1,1),
    CustomerId bigint

    PRIMARY KEY( TenantId, ShopId, JobId )

    FOREIGN KEY( TenantId REFERENCES Tenants ( TenantId ) )
    FOREIGN KEY( TenantId, ShopId REFERENCES Shops( TenantId, ShopID ) )
    FOREIGN KEY( TenantId, CustomerId REFERENCES Customers( TenantId, CustomerId ) )
)

...看起来有点像黑客,有很多冗余数据 - 尤其是当IDENTITY被使用时。有没有什么方法RDBMS可以在数据发生变异时测试JOIN的一致性?

3 个答案:

答案 0 :(得分:1)

复合外键约束完全有效且有用,但您不需要复合主键来使用它们!您只需要在引用的表中使用复合索引。由于FK约束,TenantId中的冗余Jobs不会产生更新异常的风险。

例如:

CREATE TABLE Shops (
    ShopId   bigint IDENTITY(1,1),
    TenantId bigint,
    PRIMARY KEY (ShopId),
    UNIQUE KEY (TenantId, ShopId),
    FOREIGN KEY (TenantId) REFERENCES Tenants (TenantId)
)

CREATE TABLE Customers (
    CustomerId bigint IDENTITY(1,1),
    TenantId   bigint,
    PRIMARY KEY (CustomerId),
    UNIQUE KEY (TenantId, CustomerId),
    FOREIGN KEY (TenantId) REFERENCES Tenants (TenantId)
)

CREATE TABLE Jobs (
    JobId      bigint IDENTITY(1,1),
    TenantId   bigint,
    ShopId     bigint,
    CustomerId bigint,
    PRIMARY KEY (JobId),
    FOREIGN KEY (TenantId, ShopId) REFERENCES Shops (TenantId, ShopID),
    FOREIGN KEY (TenantId, CustomerId) REFERENCES Customers (TenantId, CustomerId)
)

如果您担心存储空间,我建议您根据实际数据量计算该空间的实际成本,并对FK约束与触发器与涉及子查询的检查约束之间的性能差异进行基准测试。不要假设额外的属性效率低下。

答案 1 :(得分:0)

假设您的rdbms支持检查约束,那么您可以在作业表上使用检查约束,以检查客户ID是否指向与商店ID相同的tannent ID。
这样,每张桌子上都会留下一列主键 基于创建表语法,我猜测您正在使用,因此您的检查约束将是这样的:

ALTER TABLE Jobs
    ADD CONSTRAINT chk_jobs_customer_shop   
    CHECK dbo.fnCheckCustomerAndShopRelationship(customerId, shopId) = 1

当然,您需要先创建UDF:

CREATE FUNCTION dbo.fnCheckCustomerAndShopRelationship 
(
    @customerId int,
    @shopId int
)
RETURNS int   
AS
BEGIN

    IF EXISTS
    (
        SELECT 1
        FROM Customers c
        INNER JOIN Shops s ON c.TenantId = s.TenantId
    )
        RETURN 1
    ELSE
        RETURN 0
END;
GO

答案 2 :(得分:0)

您的第二个设计是典型SQL DBMS的简单声明式设计。

虽然标准SQL(和关系模型)允许任意声明性约束(CHECK和CREATE ASSERTION),但遗憾的是,典型的SQL DBMS只允许声明超级键(PRIMARY KEY& UNIQUE NOT NULL),外部超级键(FOREIGN KEY)和有限的检查。

任意约束强制执行的典型SQL解决方案是定义在INSRE,UPDATE和DELETE以及CHECK中的函数中根据需要计算表达式的触发器。遗憾的是,DBMS通常不会以正确的原子/序列化方式评估约束强制执行代码。

您将约束描述为“测试JOIN以保持一致性”反映了一个很好的理解,因为一般来说,为了完整性和清晰度,我们希望对任意表达式断言任意约束。

任意约束的任何合理实现都必须利用“每当数据发生变化时进行测试”,以避免重新评估整个表达式,这可能是一个成本低得多的测试,而不仅仅是已经发生了变化。这段代码就是你不幸用手写的触发器。它不是大多数供应商的优先事项。见Lex deHaan&amp ;;的Applied Mathematics for Database Professionals。 Toon Koppelaars)对这些问题和解决方案进行了很好的介绍。