Frage

Ich habe eine besonders schwierige Geschäftsbeschränkung, die ich auf Datenbankebene durchsetzen möchte. Die Daten sind finanzieller Natur und müssen daher vor Inkonsistenzen bis zum n -ten Grad geschützt werden - kein Vertrauen der Geschäftsschicht mit diesem Zeug. Ich benutze das Wort "zeitlich" etwas locker, was bedeutet, dass ich steuern möchte, wie eine Entität im Laufe der Zeit ändern kann und kann.

Über die Details beschönigen, hier ist das Design:

  • Eine Rechnung kann mehrere Gebühren enthalten.
  • Die Gebühren werden kurz nach der Erstellung der Rechnung einer Rechnung zugeordnet.
  • Die Rechnung erreicht eine Bühne, wonach sie "gesperrt" ist.
  • Ab diesem Zeitpunkt darf keine Gebühr hinzugefügt oder aus dieser Rechnung entfernt werden.

Hier ist eine abgespeckte Datennfinition:

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
)

Sie werden feststellen, dass die "abschließbare" Natur der Rechnung hier nicht dargestellt wird. Wie man es darstellt - und ob es überhaupt direkt dargestellt werden muss - ist immer noch eine offene Frage.

Ich bin zu der Überzeugung, dass dies eine dieser Arrangements ist, die nicht in eine normale Form von Domain-Key übersetzt werden können, obwohl ich mich möglicherweise irren kann. (Immerhin gibt es wirklich keine Möglichkeit zu sagen.) Das heißt, ich habe immer noch Hoffnung auf eine hoch-almalisierte Lösung.

Ich implementiere dies auf SQL Server 2008 (die Syntax war vielleicht ein Hinweis), aber ich bin ein neugieriger Typ. Wenn es also Lösungen gibt, die an anderen DBMs arbeiten, würde ich auch gerne über diese hören.

War es hilfreich?

Lösung

Komplizieren Sie es nicht, ich würde mit Auslöser gehen. Es ist keine Schande, sie zu benutzen, dafür sind sie da.

Um viel Logik in den Triggern zu vermeiden, füge ich in der Header -Tabelle eine "bearbeitbare" Bitspalte hinzu und verwende dann im Grunde eine Kluft mit bearbeitbarem Arbeiten, um entweder zu arbeiten oder eine Kluft durch Null zu verursachen, die ich fange und zu einem konvertieren kann Invoice is not editable, no changes permitted Botschaft. Es gibt keine existierenden, um zusätzlichen Aufwand zu beseitigen. Versuche dies:

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

Hier ist ein einfaches Testskript zum Testen der verschiedenen Kombinationen:

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.

Andere Tipps

Ich kann mir keinen Weg vorstellen, dies mit Normalisierung zu tun. Wenn ich dies jedoch in der Datenbank einschränken wollte, würde ich es 1 von zwei Möglichkeiten tun:

Zunächst würde ich Rechnungen eine "gesperrte" Spalte hinzufügen, die ein bisschen irgendeiner Art ist, nur eine Möglichkeit, sie als gesperrt zu tasten.

Dann die beiden Wege:

  1. Ein "vor Einfügen" -auslöser, der einen Fehler auswirft, bevor ein Einsatz hergestellt wird, wenn die gerechnungsgezogene Rechnung gesperrt ist.
  2. Tun Sie diese Logik in der gespeicherten Prozedur, die die Gebühr erstellt.

Bearbeiten: Ich konnte keinen guten MSDN -Artikel darüber finden, wie man einen davon macht, aber IBM hat einen, der in SQL Server ziemlich gut funktioniert: http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=/sqlp/rbafyBeforesql.htm

Sie können die Ergänzungen zur Gebührentabelle einschränken, indem Sie Ihr Datenmodell ändern:

Rechnungen

  • INVOICE_ID
  • INVOICE_LOCKED_DATE, Null

HONORARE

  • FEE_ID (PK)
  • INVOICE_ID (PK, FK INVOICES.INVOICE_ID)
  • INVOICE_LOCKED_DATE (PK, FK INVOICES.INVOICE_LOCKED_DATE)
  • MENGE

Auf einen Blick ist es überflüssig, aber solange eine Einfügungsanweisung für die Gebührentabelle nicht in die Rechnungstabelle für das gesperrte Datum (Standard ist null) nachgeschlagen wird - stellt sicher, dass neue Datensätze das Datum haben, an dem die Rechnung gesperrt wurde.

Eine weitere Option besteht darin, zwei Tabellen bezüglich der Gebührenbearbeitung zu haben - PRELIMINARY_FEES und CONFIRMED_FEES.

Während ein Rechnungsgebühren noch bearbeitet werden kann, wohnen sie in der PRELIMINIARY_FEES Tabelle und einmal bestätigt - werden in die verschoben CONFIRMED_FEES. Ich mag dieses nicht sehr, um zwei identische Tabellen zusammen mit den Auswirkungen von Abfragen aufrechtzuerhalten, aber es würde die Verwendung ermöglichen GEWÄHRENs (in einer Rolle, nicht Benutzerbasis), um nur Zugriff auf den Zugriff zu ermöglichen CONFIRMED_FEES Wenn Sie einfügen, aktualisieren Sie beim Einfügen auf die PRELIMINARY_FEES Tisch. Sie können die Zuschüsse in einer einzigen Gebühren -Tabellen -Setup nicht einschränken, da ein Zuschuss keine Daten beachtet wird. Sie können keinen bestimmten Status überprüfen.

Ich denke gefroren), um Änderungen zu verhindern, wenn sich die Rechnung im verschlossenen Zustand befindet.

Die verschlossene Flagge ist erforderlich, es sei denn, es gibt eine zuverlässige algorithmische Methode, um zu bestimmen, wann die Rechnung gesperrt ist - möglicherweise 2 Stunden nach seiner Herstellung. Natürlich müssen Sie die Rechnungszeile aktualisieren, um sie zu sperren - also ist eine algorithmische Methode besser (weniger Updates).

Warum nicht einfach eine "gesperrte" Spalte haben, die ein Boolescher (oder ein einzelnes Zeichen "Y", "n" ist) und Ihre Update -Abfrage für eine Unterabfrage optimieren:

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

Angenommen, Sie haben eine nicht-null-Einschränkung in der Invoiceid-Spalte, fällt die Einfügung fehl, wenn die Rechnung gesperrt ist. Sie können die Ausnahme in Ihrem Code bewältigen und somit Gebührenabzüge verhindern, wenn die Rechnung gesperrt ist. Sie vermeiden es auch, komplizierte Auslöser und gespeicherte Verfahren zu schreiben und aufrechtzuerhalten.

Ps. Die obige Insertion -Abfrage verwendet die MySQL -Syntax. Ich fürchte, ich bin nicht so mit der TQL -Variante von SQL Server vertraut.

Ich stimme dem allgemeinen Konsens darüber zu, dass die Rechnungstabelle hinzugefügt werden sollte, um anzuzeigen, ob Gebühren hinzugefügt werden können. Es ist dann erforderlich, den TSQL -Code hinzuzufügen, um die Geschäftsregeln im Zusammenhang mit gesperrten Rechnungen durchzusetzen. Ihr ursprünglicher Beitrag scheint keine Einzelheiten zu den Bedingungen zu enthalten, unter denen eine Rechnung gesperrt wird, aber es ist vernünftig anzunehmen, dass das Schloss angemessen eingestellt werden kann (dieser Aspekt des Problems könnte kompliziert werden, aber lassen Sie uns das in einem anderen regeln Faden).

Angesichts dieses Konsens gibt es zwei Implementierungsentscheidungen, die die Geschäftsregel in der Datenebene effektiv durchsetzen: Auslöser und Standardverfahren. Um Standardverfahren zu verwenden, würde man natürlich Aktualisierungen, Löschungen und Einfügungen für die Rechnungen und Gebührentabellen verweigern und alle Datenänderungen unter Verwendung gespeicherter Prozeduren erfordern.

Der Vorteil der Verwendung von Triggern besteht darin, dass der Anwendungsclientcode vereinfacht werden kann, da die Tabellen direkt zugegriffen werden können. Dies könnte ein wichtiger Vorteil sein, wenn Sie beispielsweise LINQ zu SQL verwenden.

Ich kann ein paar Vorteile für die Verwendung gespeicherter Verfahren sehen. Zum einen denke ich, dass die Verwendung einer gespeicherten Prozedurschicht für Wartungsprogrammierer einfacher und daher verständlicher ist. Sie oder Sie in einigen Jahren erinnern sich möglicherweise nicht an diesen cleveren Auslöser, den Sie erstellt haben, aber eine gespeicherte Verfahrensschicht ist unverkennbar. An einem verwandten Punkt würde ich argumentieren, dass es eine Gefahr besteht, einen Auslöser versehentlich fallen zu lassen. Es ist weniger wahrscheinlich, dass jemand versehentlich die Berechtigungen auf diesen Tabellen ändern würde, um sie direkt zu beschreiben. Während eines der beiden Szenarien möglich ist, würde ich mich für die Option für gespeicherte Verfahren aus Sicherheitsgründen für die Option für gespeicherte Prozeduren willen entscheiden.

Es ist zu beachten, dass diese Diskussion keine Datenbank Agnostic ist: Wir diskutieren SQL Server -Implementierungsoptionen. Wir könnten einen ähnlichen Ansatz mit Oracle oder einem anderen Server verwenden, der SQL verfahrensmäßig unterstützt. Diese Geschäftsregel kann jedoch nicht mit statischen Einschränkungen durchgesetzt werden, und es kann auch nicht auf Datenbank neutral erzwungen werden.

Sie können nicht nur FK -Einschränkungen und dergleichen verwenden - zumindest in keiner Weise, was viel Sinn macht. Ich würde vorschlagen, eine zu verwenden ANSTATT VON Trigger in SQL Server, um diese Einschränkung durchzusetzen. Es sollte ziemlich einfach zu schreiben und ziemlich einfach zu schreiben.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top