困難な時間的クロステーブルデータベースの制約
-
19-09-2019 - |
質問
データベースレベルで実施したいという特に困難なビジネス上の制約があります。データは本質的に財政的であるため、矛盾から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
)
請求書の「ロック可能な」性質はここには表されていないことに気付くでしょう。それを表現する方法 - そしてそれがまったく直接表現される必要があるかどうかは、まだ未解決の問題です。
私はこれがドメインキーの通常の形に翻訳できないアレンジメントの1つであると信じるようになりましたが、私は間違っているかもしれません。 (結局のところ、実際に伝える方法はありません。)それは、私はまだ非常に通常のソリューションへの希望を抱いています。
私はたまたまSQL Server 2008でこれを実装しています(構文はヒントだったかもしれません)が、私は好奇心が強い人なので、他のDBMSで機能するソリューションがある場合、それらについても聞きたいです。
解決
それを複雑にしないでください、私はトリガーと一緒に行きます。それらを使用することに恥はありません。これは彼らがそこにいるものです。
トリガーの多くのロジックを回避するために、ヘッダーテーブルに「編集可能な」ビット列を追加し、基本的に編集可能な除算を使用して作業を行うか、ゼロエラーで除算を引き起こします。 Invoice is not editable, no changes permitted
メッセージ。余分なオーバーヘッドを排除するために使用される存在はありません。これを試して:
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つを実行します。
まず、「ロックされた」列を請求書に追加します。これは、ある種の少しの種類であり、ロックされているようにキーキングする方法です。
次に、2つの方法:
- 「挿入前」トリガー。挿入が行われる前にエラーが発生します。請求書がロックされている場合。
- 料金を作成するストアドプロシージャでこのロジックを実行します。
編集:これらのいずれかを実行する方法に関する優れたMSDN記事を見つけることができませんでしたが、IBMにはSQL Serverで非常にうまく機能するものがあります。 http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=/sqlp/rbafybeforesql.htm
データモデルを変更して使用することにより、手数料テーブルへの追加を制約できます。
請求書
INVOICE_ID
INVOICE_LOCKED_DATE
, 、 ヌル
料金
FEE_ID
(PK)INVOICE_ID
(PK、FKINVOICES.INVOICE_ID
)INVOICE_LOCKED_DATE
(PK、FKINVOICES.INVOICE_LOCKED_DATE
)- 額
一見すると、それは冗長ですが、料金テーブルの挿入ステートメントにロックされた日付の請求書テーブルの検索が含まれていない限り(デフォルトはnull) - 新しいレコードに請求書がロックされた日付が保証されます。
別のオプションは、料金の取り扱いに関する2つのテーブルを持つことです - PRELIMINARY_FEES
と CONFIRMED_FEES
.
請求書の料金はまだ編集可能ですが、 PRELIMINIARY_FEES
テーブルと確認されたら - に移動されます CONFIRMED_FEES
. 。クエリへの影響とともに2つの同一のテーブルを維持しなければならないために、私はこれがあまり好きではありませんが、それは使用することができます 許すs(ユーザーではなく、役割について)を選択することのみを許可します CONFIRMED_FEES
挿入、更新、削除を許可します PRELIMINARY_FEES
テーブル。助成金がデータを認識していないため、単一の手数料テーブルのセットアップで助成金を制限することはできません。特定のステータスを確認することはできません。
請求書の請求書の「ロック/ロック解除」状態を明示的に保存するのが最善だと思います。挿入と削除にトリガーを適用します(および更新しますが、実際には請求書の料金が必要なとは言いませんが凍結)請求書がロックされた状態にある場合の変更を防ぐため。
ロックされたフラグは、請求書がいつロックされているかを決定するための信頼できるアルゴリズム方法がない限り、おそらく生成されてから2時間後に必要です。もちろん、請求書の行を更新してロックする必要があります。そのため、アルゴリズム方法の方が優れています(更新が少ない)。
ブール(またはシングルチャー、「Y」、「n」)である「ロック」列を持っていて、更新クエリを調整してサブクエリを使用してみませんか。
INSERT INTO Fees (InvoiceID, Amount) VALUES ((SELECT InvoiceID FROM Invoices WHERE InvoiceID = 3 AND NOT Locked), 13.37);
InvoiceID列に非ヌルの制約があると仮定すると、請求書がロックされているときに挿入が失敗します。コードで例外を処理するため、請求書がロックされているときに料金の追加を防ぐことができます。また、複雑なトリガーやストアドプロシージャも書き込み、維持する必要があります。
詩上記の挿入クエリはMySQL構文を使用していますが、SQL ServerのTQLバリアントに精通していないのではないかと思います。
私は、料金を追加できるかどうかを示すために、請求書テーブルにロックビットを追加する必要があるという一般的なコンセンサスに同意します。その後、ロックされた請求書に関連するビジネスルールを実施するために、TSQLコードを追加する必要があります。元の投稿には、請求書がロックされる条件に関する詳細は含まれていないようですが、ロックビットを適切に設定できると仮定するのは合理的です(問題のこの側面は複雑になる可能性がありますが、別のものでそれを解決しましょうスレッド)。
このコンセンサスを考慮して、データ層のビジネスルールを効果的に実施する2つの実装の選択肢があります:トリガーと標準のストアドプロシージャ。標準のストアドプロシージャを使用するには、もちろん、請求書と料金のテーブルの更新、削除、および挿入物を拒否し、すべてのデータ変更をストアドプロシージャを使用して実行する必要があります。
トリガーを使用する利点は、テーブルに直接アクセスできるため、アプリケーションクライアントコードを簡素化できることです。これは、たとえばLINQをSQLに使用している場合に重要な利点になる可能性があります。
ストアドプロシージャの使用にはいくつかの利点があります。一つには、ストアドプロシージャレイヤーを使用することはより簡単であるため、メンテナンスプログラマーにとってより理解しやすいと思います。彼らまたはあなたは今から数年後、あなたが作成した巧妙なトリガーを覚えていないかもしれませんが、ストアドプロシージャレイヤーは紛れもないものです。関連する点では、私は誤って引き金を落とす危険があると主張します。誰かがこれらのテーブルの許可を誤って変更して直接書き込みできるようにする可能性は低くなります。どちらのシナリオも可能ですが、これに多くの乗車があれば、安全のためにストアドプロシージャオプションを選択します。
このディスカッションはデータベース不可欠なものではないことに注意する必要があります。SQLServerの実装オプションについて説明しています。 SQLの手続き上のサポートを提供するOracleまたは他のサーバーで同様のアプローチを使用することもできますが、このビジネスルールは静的制約を使用して実施することも、データベースの中立的な方法で実施することもできません。
FKの制約などを使用するだけではできません。 Anを使用することをお勧めします それ以外の SQL Serverでトリガーして、この制約を実施します。書くのはかなり簡単で、非常に簡単なはずです。