Domanda

Ho particolarmente difficile vincolo che vorrei applicare a livello di database.I dati di natura finanziaria, e quindi deve essere protetto da incoerenze all'ennesima potenza – non fidandosi livello aziendale con questa roba.Io uso la parola "temporale" un po ' vagamente, il che significa che ho intenzione di controllare come un'entità può e non si può cambiare nel tempo.

Trascurando i dettagli, ecco il progetto:

  • Una fattura può dispongono di diverse commissioni.
  • Le tasse sono assegnati a una fattura, poco dopo la creazione della fattura.
  • La fattura raggiunge una tappa del processo dopo che si è "bloccata".
  • Da questo punto in poi, nessuna tassa può essere aggiunto o rimosso da questa fattura.

Ecco una spogliato di definizione dei dati:

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
)

Si noterà che la "serratura" natura della fattura non è qui rappresentato;come rappresentare – e se deve essere direttamente rappresentati, a tutti – è ancora una questione aperta.

Sono arrivato a credere che questa è una di quelle disposizioni che non possono essere tradotte in dominio-chiave a forma normale, anche se I può essere sbagliato.(Non c'è davvero modo di dire, dopo tutto.) Detto questo, tengo ancora la speranza per un grande normalizzato di soluzione.

Mi capita di essere l'implementazione di questo su SQL Server 2008 (la sintassi potrebbe essere stato un suggerimento), ma io sono un curioso ragazzo, quindi se ci sono soluzioni che funzionano su altri DBMS, mi piacerebbe sentire quelli.

È stato utile?

Soluzione

Non complicare, mi piacerebbe andare con i trigger.Non c'è vergogna nel loro utilizzo, questo è ciò che sono lì per.

Per evitare un sacco di logica di trigger, posso aggiungere un "Modificabile" colonna di bit nell'intestazione della tabella, quindi fondamentalmente utilizzare una divisione con Modificabili per lavoro o causare un errore di divisione per zero, che mi CATTURA e la conversione di un Invoice is not editable, no changes permitted messaggio.Non ci sono ESISTE utilizzati per eliminare il sovraccarico aggiuntivo.Prova questo:

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

ecco un semplice script di test per provare le diverse combinazioni:

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.

Altri suggerimenti

Io non posso pensare a un modo per fare questo con normalizzazione.Tuttavia, se volevo vincolare presente nel database, mi piacerebbe fare 1 dei due modi:

primo, vorrei aggiungere un 'blocco' di colonna per le Fatture, che è un po ' di sorta, solo un modo per chiave è come bloccato.

Quindi, i due modi:

  1. Un "prima di inserire il" trigger, che avrebbe dato errore prima di un inserto è fatto, se la fattura di cui è bloccato.
  2. Fare questa logica nella stored procedure che crea il pagamento.

EDIT:Non riuscivo a trovare un buon articolo di MSDN su come fare uno di questi, ma IBM ha uno che funziona abbastanza bene in SQL Server: http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=/sqlp/rbafybeforesql.htm

Si può vincolare aggiunte alla tabella TARIFFE alterando il vostro modello di dati da utilizzare:

FATTURE

  • INVOICE_ID
  • INVOICE_LOCKED_DATE, null

TASSE

  • FEE_ID (pk)
  • INVOICE_ID (pk, fk INVOICES.INVOICE_ID)
  • INVOICE_LOCKED_DATE (pk, fk INVOICES.INVOICE_LOCKED_DATE)
  • QUANTITÀ

In sintesi, è ridondante, ma un'istruzione INSERT per la tabella TARIFFE non includono una ricerca nella tabella FATTURE per il blocco di data (il valore predefinito null), assicura che il nuovo record la data della fattura è stato bloccato.

Un'altra opzione è quella di avere due tabelle in materia di tassa di gestione - PRELIMINARY_FEES e CONFIRMED_FEES.

Mentre le fatture tasse sono ancora modificabili, risiedono nel PRELIMINIARY_FEES tavolo e, una volta confermata - sono spostati CONFIRMED_FEES.Non mi piace molto per l'amor di dover gestire due tabelle identiche con query implicazioni, ma dovrebbe consentire l'utilizzo di GRANTs (in un ruolo, ma non per l'utente, in base solo a permettere l'accesso a CONFIRMED_FEES consentendo INSERT, UPDATE, DELETE sul PRELIMINARY_FEES tabella.Non è possibile limitare le sovvenzioni in una singola tabella TARIFFE di installazione a causa di una sovvenzione non è a conoscenza di dati che non si possono controllare per un dato stato.

Penso che sarà meglio in modo esplicito la memorizzazione di 'blocco/sblocco' stato per la fattura in fattura tabella, e quindi applicare trigger INSERT e DELETE (e AGGIORNARE, se non per dire che si desidera in fattura delle spese di frozen) per evitare modifiche, se la fattura è in stato di blocco.

Bloccato il flag è necessario se non c'è un affidabile metodo algoritmico per determinare quando la fattura è bloccato - forse 2 ore dopo si è prodotto.Naturalmente, è necessario aggiornare la riga della fattura per bloccare in modo un algoritmo è il metodo migliore (minor numero di aggiornamenti).

Perché non basta avere un 'Blocco' la colonna che è un valore booleano (o singolo char, 'y', 'n', per esempio) e modificare le query di aggiornamento per l'utilizzo di una sottoquery :

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

Se si dispone di un non-null constraint sulla InvoiceID colonna, l'inserimento avrà esito negativo quando la fattura viene bloccata.È possibile gestire l'eccezione nel codice e quindi prevenire la tassa aggiunte quando la fattura viene bloccata.Potrai anche evitare di dover scrivere e mantenere complicato trigger e stored procedure.

PS.L'inserimento di query sopra usa la sintassi MySQL, ho paura io non sono che la familiarità con SQL Server TQL variante.

Sono d'accordo con il generale consenso sul fatto che un blocco di bit deve essere aggiunto alla tabella Fatture per indicare se le tasse possono essere aggiunti.È quindi necessario aggiungere codice TSQL per far rispettare le regole di business relative al bloccata fatture.Il tuo post originale non sembra includere specifiche circa le condizioni in cui una fattura viene bloccato, ma è ragionevole supporre che il blocco di bit può essere impostato in modo appropriato (questo aspetto del problema potrebbe diventare complicato, ma bisogna accontentarsi che in un altro thread).

Dato questo consenso, ci sono 2 scelte implementative che effettivamente applicare la regola di business nel livello dati:trigger e stored procedure standard.Per utilizzare la stored procedure standard, si potrebbe, naturalmente, NEGARE AGGIORNA, ELIMINA E INSERTI per le Fatture e le tabelle delle Tasse e richiedono che tutti i dati modificazione essere fatto utilizzando stored procedure.

Il vantaggio di utilizzare i trigger, è che il codice del client di applicazioni può essere semplificata, perché le tabelle, è possibile accedere direttamente.Questo potrebbe essere un vantaggio importante se si sta utilizzando LINQ to SQL, per esempio.

Posso vedere un paio di vantaggi per l'utilizzo di stored procedure.Per una cosa, penso che utilizzando una stored procedure strato è più semplice e quindi più comprensibile per la manutenzione dei programmatori.Essi, o è da diversi anni ormai, non può ricordare che intelligente trigger è stato creato, ma una stored procedure strato è inconfondibile.Su un punto relativo, direi che vi è un pericolo di caduta accidentale di un trigger;è meno probabile che qualcuno avrebbe accidentalmente modificare le autorizzazioni di queste tabelle per rendere direttamente accessibile in scrittura.Mentre entrambi i casi è possibile, se c'è un sacco di guida su questo vorrei optare per l'opzione stored procedure per ragioni di sicurezza.

Va notato che questa discussione non è di database agnostico:stiamo discutendo di SQL Server opzioni di implementazione.Si potrebbe utilizzare un approccio simile con Oracle o qualsiasi altro server che fornisce il supporto per SQL, ma questo business regola non può essere applicata l'utilizzo di questo tipo di vincoli, né può essere applicata in un database neutralità.

Non si può semplicemente utilizzando FK vincoli e simili, almeno non in alcun modo che rende molto senso.Vorrei suggerire l'uso di un INVECE DI trigger in SQL Server per far rispettare questo vincolo.Dovrebbe essere abbastanza facile da scrivere e abbastanza semplice.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top