我有一个特别困难的业务约束,我想在数据库级别强制执行。这些数据本质上属于财务数据,因此必须保护其不存在与第n度的不一致 - 不要信任业务层。我稍微松散地使用“时态”这个词,这意味着我打算控制一个实体如何能够随时间变化而不能改变。
对细节进行着色,这是设计:
这是一个精简的数据定义:
CREATE TABLE Invoices
(
InvoiceID INT IDENTITY(1,1) PRIMARY KEY,
)
CREATE TABLE Fees
(
FeeID INT IDENTITY(1,1) PRIMARY KEY,
InvoiceID INT REFERENCES Invoices(InvoiceID),
Amount MONEY
)
您会注意到此处未显示发票的“可锁定”性质;如何表达它 - 以及它是否需要直接表示 - 仍然是一个悬而未决的问题。
我开始相信这是那些无法转换为域密钥正常形式的安排之一,尽管我可能错了。 (毕竟,实在没有办法说出来。)尽管如此,我仍然对高度标准化的解决方案抱有希望。
我碰巧在SQL Server 2008上实现了这个(语法可能是一个提示),但我是一个好奇的人,所以如果有其他DBMS的解决方案,我很想听听那些同样。
答案 0 :(得分:9)
我想不出用标准化做到这一点的方法。但是,如果我想在数据库上限制它,我会用两种方法中的一种方法:
首先,我会在发票上添加一个“锁定”列,这有点类似,只是将其锁定为锁定的方式。
然后,两种方式:
答案 1 :(得分:4)
不要复杂化,我会选择触发器。使用它们并不羞耻,这就是它们的用途。
为了避免触发器中的大量逻辑,我在头表中添加了一个“可编辑”位列,然后基本上使用带可编辑的除法来工作或导致除零错误,我捕获并转换为Invoice is not editable, no changes permitted
消息。没有用于消除额外开销的EXISTS。试试这个:
CREATE TABLE testInvoices
(
InvoiceID INT not null IDENTITY(1,1) PRIMARY KEY
,Editable bit not null default (1) --1=can edit, 0=can not edit
,yourData char(2) not null default ('xx')
)
go
CREATE TABLE TestFees
(
FeeID INT IDENTITY(1,1) PRIMARY KEY
,InvoiceID INT REFERENCES testInvoices(InvoiceID)
,Amount MONEY
)
go
CREATE TRIGGER trigger_testInvoices_instead_update
ON testInvoices
INSTEAD OF UPDATE
AS
BEGIN TRY
--cause failure on updates when the invoice is not editable
UPDATE t
SET Editable =i.Editable
,yourData =i.yourData
FROM testInvoices t
INNER JOIN INSERTED i ON t.InvoiceID=i.InvoiceID
WHERE 1=CONVERT(int,t.Editable)/t.Editable --div by zero when not editable
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_testInvoices_instead_delete
ON testInvoices
INSTEAD OF DELETE
AS
BEGIN TRY
--cause failure on deletes when the invoice is not editable
DELETE t
FROM testInvoices t
INNER JOIN DELETED d ON t.InvoiceID=d.InvoiceID
WHERE 1=CONVERT(int,t.Editable)/t.Editable --div by zero when not editable
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_TestFees_instead_insert
ON TestFees
INSTEAD OF INSERT
AS
BEGIN TRY
--cause failure on inserts when the invoice is not editable
INSERT INTO TestFees
(InvoiceID,Amount)
SELECT
f.InvoiceID,f.Amount/i.Editable --div by zero when invoice is not editable
FROM INSERTED f
INNER JOIN testInvoices i ON f.InvoiceID=i.invoiceID
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_TestFees_instead_update
ON TestFees
INSTEAD OF UPDATE
AS
BEGIN TRY
--cause failure on updates when the invoice is not editable
UPDATE f
SET InvoiceID =ff.InvoiceID
,Amount =ff.Amount/i.Editable --div by zero when invoice is not editable
FROM TestFees f
INNER JOIN INSERTED ff ON f.FeeID=ff.FeeID
INNER JOIN testInvoices i ON f.InvoiceID=i.invoiceID
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
CREATE TRIGGER trigger_TestFees_instead_delete
ON TestFees
INSTEAD OF DELETE
AS
BEGIN TRY
--cause failure on deletes when the invoice is not editable
DELETE f
FROM TestFees f
INNER JOIN DELETED ff ON f.FeeID=ff.FeeID
INNER JOIN testInvoices i ON f.InvoiceID=i.invoiceID AND 1=CONVERT(int,i.Editable)/i.Editable --div by zero when invoice is not editable
END TRY
BEGIN CATCH
IF ERROR_NUMBER()=8134 --catch div by zero error
RAISERROR('Invoice is not editable, no changes permitted',16,1)
ELSE
BEGIN
DECLARE @ErrorMessage nvarchar(400), @ErrorNumber int, @ErrorSeverity int, @ErrorState int, @ErrorLine int
SELECT @ErrorMessage = N'Error %d, Line %d, Message: '+ERROR_MESSAGE(),@ErrorNumber = ERROR_NUMBER(),@ErrorSeverity = ERROR_SEVERITY(),@ErrorState = ERROR_STATE(),@ErrorLine = ERROR_LINE()
RAISERROR (@ErrorMessage, @ErrorSeverity, @ErrorState, @ErrorNumber,@ErrorLine)
END
END CATCH
GO
这是一个简单的测试脚本,用于测试不同的组合:
INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO testInvoices VALUES(default,default) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (1,111) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (1,1111) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,22) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,222) --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (2,2222) --works
update testInvoices set Editable=0 where invoiceid=3 --works
INSERT INTO TestFees (InvoiceID,Amount) VALUES (3,333) --error<<<<<<<
UPDATE TestFees SET Amount=1 where feeID=1 --works
UPDATE testInvoices set Editable=0 where invoiceid=1 --works
UPDATE TestFees SET Amount=11111 where feeID=1 --error<<<<<<<
UPDATE testInvoices set Editable=1 where invoiceid=1 --error<<<<<<<
UPDATE testInvoices set Editable=0 where invoiceid=2 --works
DELETE TestFees WHERE invoiceid=2 --error<<<<<
DELETE FROM testInvoices where invoiceid=2 --error<<<<<
UPDATE testInvoices SET Editable='A' where invoiceid=1 --error<<<<<<< Msg 245, Level 16, State 1, Line 1 Conversion failed when converting the varchar value 'A' to data type bit.
答案 2 :(得分:1)
您可以通过更改要使用的数据模型来限制FEES表的添加:
INVOICE_ID
INVOICE_LOCKED_DATE
,null FEE_ID
(pk)INVOICE_ID
(pk,fk INVOICES.INVOICE_ID
)INVOICE_LOCKED_DATE
(pk,fk INVOICES.INVOICE_LOCKED_DATE
)一目了然,它是多余的,但只要FEES表的INSERT语句不包含查找INVOICES表的锁定日期(默认为null) - 它确保新记录具有发票的日期被锁了。
另一种选择是有两个关于费用处理的表格 - PRELIMINARY_FEES
和CONFIRMED_FEES
。
虽然发票费用仍然可以编辑,但它们位于PRELIMINIARY_FEES
表格中,一旦确认,就会移至CONFIRMED_FEES
。我不喜欢这个,因为必须维护两个相同的表以及查询含义,但它允许使用GRANT s(在角色上,而不是用户的基础上)仅允许SELECT访问在CONFIRMED_FEES
表上允许INSERT,UPDATE,DELETE时PRELIMINARY_FEES
。您无法在单个FEES表设置中限制授权,因为授权不能识别数据 - 您无法检查给定状态。
答案 3 :(得分:1)
我认为您最好在发票表中明确存储发票的“锁定/解锁”状态,然后在INSERT和DELETE上应用触发器(和更新,尽管您实际上并不是说您想要费用如果发票处于锁定状态,则在冻结发票上)以防止修改。
锁定标志是必要的,除非有一个可靠的算法方法来确定发票何时被锁定 - 可能是在生产后2小时。当然,您必须更新发票行才能将其锁定 - 因此算法方法更好(更新更少)。
答案 4 :(得分:1)
为什么不只有一个'Locked'列是一个布尔值(或单个字符,'y','n'),并调整更新查询以使用子查询:
INSERT INTO Fees (InvoiceID, Amount) VALUES ((SELECT InvoiceID FROM Invoices WHERE InvoiceID = 3 AND NOT Locked), 13.37);
假设您在InvoiceID列上有一个非空约束,则在锁定发票时插入将失败。您可以在代码中处理异常,从而防止在发票被锁定时增加费用。您还可以避免编写和维护复杂的触发器和存储过程。
PS。上面的插入查询使用MySQL语法,我担心我不熟悉SQL Server的TQL变体。
答案 5 :(得分:1)
我同意普遍的共识,即应在发票表中添加锁定位,以指示是否可以添加费用。然后,必须添加TSQL代码以强制执行与锁定发票相关的业务规则。您的原始帖子似乎没有包含有关发票被锁定的条件的详细信息,但可以合理地假设可以正确设置锁定位(问题的这一方面可能会变得复杂,但让我们在另一个方面解决这个问题)线程)。
鉴于此共识,有两种实现选择可以有效地强制执行数据层中的业务规则:触发器和标准存储过程。要使用标准存储过程,当然可以对发票和费用表进行更新,删除和插入,并要求使用存储过程完成所有数据修改。
使用触发器的优点是可以简化应用程序客户端代码,因为可以直接访问表。例如,如果您使用LINQ to SQL,这可能是一个重要的优势。
我可以看到使用存储过程的一些优点。首先,我认为使用存储过程层更直接,因此维护程序员更容易理解。他们,或者你几年后,可能不记得你创建的聪明的触发器,但存储过程层是明确无误的。在一个相关的观点上,我认为存在意外掉落触发器的危险;不太可能有人不小心改变这些表的权限,使它们直接可写。虽然任何一种情况都是可能的,但如果有很多这样的话,我会选择存储过程选项以保证安全。
应该注意的是,此讨论与数据库无关:我们正在讨论SQL Server实现选项。我们可以使用与Oracle或任何其他为SQL提供过程支持的服务器类似的方法,但是这个业务规则不能使用静态约束来强制执行,也不能以数据库中立的方式强制执行。
答案 6 :(得分:0)
你不能仅仅通过使用FK约束等 - 至少不是以任何有意义的方式。我建议在SQL Server中使用INSTEAD OF触发器来强制执行此约束。它应该相当容易编写并且非常简单。