1

データベース レベルで実施したい、特に難しいビジネス上の制約があります。データは本質的に財務的なものであるため、不整合から 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 で機能するソリューションがあれば、それについても知りたいです。

4

7 に答える 7

9

正規化でこれを行う方法は考えられません。ただし、データベースでこれを制限したい場合は、次の 2 つの方法のいずれかを実行します。

最初に、「ロックされた」列を請求書に追加します。

次に、2 つの方法:

  1. 参照される請求書がロックされている場合、挿入が行われる前にエラーをスローする「挿入前」トリガー。
  2. 料金を作成するストアド プロシージャでこのロジックを実行します。

編集:これらのいずれかを実行する方法に関する適切な MSDN 記事を見つけることができませんでしたが、IBM には SQL Server で非常にうまく機能する記事があります: http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index .jsp?topic=/sqlp/rbafybeforesql.htm

于 2009-10-01T16:58:27.457 に答える
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.
于 2009-10-07T20:08:12.120 に答える
1

請求書の「ロック/ロック解除」状態を請求書テーブルに明示的に保存してから、INSERT と DELETE (および UPDATE にトリガーを適用しますが、実際には請求書に手数料が必要だとは言いません)凍結) 請求書がロック状態の場合に変更を防止します。

請求書が作成されてから 2 時間後にロックされるタイミングを判断するための信頼できるアルゴリズム方法がない限り、ロックされたフラグが必要です。もちろん、請求書の行を更新してロックする必要があります。そのため、アルゴリズムによる方法の方が優れています (更新が少ない)。

于 2009-10-05T02:26:23.653 に答える
1

使用するデータ モデルを変更することで、FEES テーブルへの追加を制限できます。

請求書

  • INVOICE_ID
  • INVOICE_LOCKED_DATE、 ヌル

手数料

  • FEE_ID(ピーク)
  • INVOICE_ID(pk, fk INVOICES.INVOICE_ID)
  • INVOICE_LOCKED_DATE(pk, fk INVOICES.INVOICE_LOCKED_DATE)

一見冗長ですが、FEES テーブルの INSERT ステートメントに、ロックされた日付 (デフォルトは null) の INVOICES テーブルへのルックアップが含まれていない限り、新しいレコードには請求書がロックされた日付が含まれていることが保証されます。

もう 1 つのオプションは、料金の取り扱いに関する 2 つのテーブルを用意することPRELIMINARY_FEESですCONFIRMED_FEES

請求書料金は引き続き編集可能ですが、PRELIMINIARY_FEESテーブルに存在し、確認されると - に移動されますCONFIRMED_FEES。クエリの影響とともに2つの同一のテーブルを維持する必要があるため、これはあまり好きではありませんが、GRANTを使用して(ユーザーベースではなくロールベースで)、CONFIRMED_FEESINSERTを許可しながらSELECTアクセスのみを許可することができます。PRELIMINARY_FEESテーブルの更新、削除。助成金はデータに対応していないため、単一の FEES テーブル設定で助成金を制限することはできません。特定のステータスを確認することはできません。

于 2009-10-05T01:54:05.813 に答える
1

ブール値(または単一の文字、「y」、「n」など)の「ロックされた」列を作成し、更新クエリを微調整してサブクエリを使用しないのはなぜですか。

INSERT INTO Fees (InvoiceID, Amount) VALUES ((SELECT InvoiceID FROM Invoices WHERE InvoiceID = 3 AND NOT Locked), 13.37);

InvoiceID 列に null 以外の制約があると仮定すると、請求書がロックされている場合、挿入は失敗します。コードで例外を処理できるため、請求書がロックされている場合の手数料の追加を防ぐことができます。また、複雑なトリガーやストアド プロシージャを作成して維持する必要もなくなります。

PS。上記の挿入クエリは MySQL 構文を使用していますが、残念ながら私は SQL Server の TQL バリアントに精通していません。

于 2009-10-07T19:10:58.863 に答える
1

手数料を追加できるかどうかを示すために、請求書テーブルにロック ビットを追加する必要があるという一般的なコンセンサスに同意します。その後、TSQL コードを追加して、ロックされた請求書に関連するビジネス ルールを適用する必要があります。元の投稿には、請求書がロックされる条件についての詳細が含まれていないようですが、ロックビットを適切に設定できると仮定するのは合理的です (問題のこの側面は複雑になる可能性がありますが、別の方法で解決しましょう)スレッド)。

このコンセンサスを考えると、データ層でビジネス ルールを効果的に適用する実装の選択肢が 2 つあります。それは、トリガーと標準ストアド プロシージャです。標準のストアド プロシージャを使用するには、もちろん、Invoices テーブルと Fees テーブルの更新、削除、および挿入を拒否し、ストアド プロシージャを使用してすべてのデータ変更を行う必要があります。

トリガーを使用する利点は、テーブルに直接アクセスできるため、アプリケーション クライアント コードを簡素化できることです。たとえば、LINQ to SQL を使用している場合、これは重要な利点となります。

ストアド プロシージャを使用することには、いくつかの利点があります。1 つには、ストアド プロシージャ レイヤーを使用する方がより簡単で、メンテナンス プログラマーにとってより理解しやすいと思います。彼ら、または今から数年後のあなたは、あなたが作成した巧妙なトリガーを覚えていないかもしれませんが、ストアド プロシージャ レイヤーは紛れもないものです。関連する点として、誤ってトリガーを落としてしまう危険性があると私は主張します。これらのテーブルの権限を誰かが誤って変更して、直接書き込み可能にする可能性は低くなります。どちらのシナリオも可能ですが、これに多くの負荷がかかっている場合は、安全のためにストアド プロシージャ オプションを選択します。

この説明はデータベースにとらわれないことに注意してください。SQL Server の実装オプションについて説明しています。Oracle や SQL の手続きサポートを提供するその他のサーバーで同様のアプローチを使用できますが、このビジネス ルールは静的な制約を使用して強制することも、データベースに依存しない方法で強制することもできません。

于 2009-10-09T20:11:07.143 に答える
0

FK 制約などを使用するだけではできません。少なくとも、あまり意味のある方法ではありません。この制約を適用するには、SQL Server でINSTEAD OFトリガーを使用することをお勧めします。書くのはかなり簡単で、かなり簡単なはずです。

于 2009-10-05T00:15:37.180 に答える