Reputation: 155250
Consider this database with 4 tables (primary-keys in asterisks):
Products( *ProductId*, SkuText, ... )
ProductRevisions( ProductId, *RevisionId*, ... )
Orders( *OrderId*, ... )
OrderItems( *OrderId*, *ProductRevisionId*, Quantity, ... )
The idea being that a Product SKU can have multiple revisions (e.g. a 2016 version of a product compared to its 2015 version). The business rules are such that an Order
for a Product
can only have a single ProductRevision
, e.g. an order cannot request both the 2014 and 2016 versions of the same product, they can only have the 2014 or 2016 version.
Ordinarily this wouldn't be a problem: the OrderItems
table would have a ProductId
column with a UNIQUE
constraint on OrderId
and ProductId
. However because OrderItems
's references ProductRevisionId
(so the reference to the ProductId
is indirect) it means a simple UNIQUE
constraint fails and the schema would accept the following data, even though it is invalid as-per the business rules:
Products
ProductId, SkuText
1, 'Kingston USB Stick'
ProductRevisions
ProductId, RevisionId, ...
1, 1, '2014 model'
1, 2, '2016 model'
Orders
OrderId
1
OrderItems
OrderId, ProductRevisionId, Quantity
1, 1, 100
1, 2, 50 -- Invalid data! Two revisions of the same Product should not be in the same order.
What I need is something like this:
ALTER TABLE OrderItems
ADD CONSTRAINT UNIQUE ( OrderId, SELECT ProductId FROM ProductRevisions WHERE RevisionId = OrderItems.ProductRevisionId )
I don't want to denormalize my OrderItems
table by adding an explicit ProductId
column because that adds a potential point of failure if the parent/child relationship between a given ProductId
and ProductRevisionId
were to change then the data becomes invalid.
What are my options?
Upvotes: 1
Views: 59
Reputation: 32697
You can create an indexed view to enforce the constraint. In your case, it'd be something like:
create view [OrderItemProductRevisions]
with schemabinding
as
select oi.OrderID, pr.ProductID
from dbo.OrderItems as oi
join dbo.ProductRevisions as pr
on oi.ProductRevisionID = pr.ProductRevisionID
go
create unique clustered index [CUIX_OrderItemProductRevisions]
on [OrderItemProductRevisions] (OrderID, ProductID)
go
Now, if you try to add two revisions of the same product to the same order, you should violate the unique index on the view and it will be disallowed.
Upvotes: 0
Reputation: 1269873
This is really more of a comment, but it is too long.
One option is to create a trigger. This allows you to validate the data using any rules that you want. However, triggers are cumbersome and unnecessary.
Another option is essentially what you say: include both Product
and ProductRevision
in OrderLines
. However, this doesn't quite solve the problem. You need to ensure that the product actually matches the product on the revision.
I am thinking that the best option might be to have a Revision
column in ProductRevisions
. So, this table would have:
ProductRevisionId
-- primary key for the tableProductId
RevisionId
(ProductId, RevisionId)
The foreign key constraint in OrderLines
can then have two columns in it -- (ProductId, RevisionId)
. Then a unique constraint on (OrderId, ProductId)
ensures only one revision.
The downside to this method is that a product can only appear on only one line in each order. However, you don't need triggers.
Upvotes: 1