Pergunta

Tenho uma restrição comercial particularmente difícil que gostaria de aplicar no nível do banco de dados. Os dados são de natureza financeira e, portanto, devem ser protegidos das inconsistências para o enésimo grau - sem confiar na camada de negócios com essas coisas. Eu uso a palavra "temporal" um pouco vagamente, o que significa que pretendo controlar como uma entidade pode e não pode mudar com o tempo.

Organizando os detalhes, aqui está o design:

  • Uma fatura pode apresentar várias taxas.
  • As taxas são atribuídas a uma fatura logo após a criação da fatura.
  • A fatura chega a um estágio no processo, após o qual está "bloqueado".
  • A partir deste ponto, nenhuma taxa pode ser adicionada ou removida desta fatura.

Aqui está uma definição de dados despojada:

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
)

Você notará que a natureza "travável" da fatura não está representada aqui; Como representá -lo - e se ele precisa ser diretamente representado - ainda é uma questão em aberto.

Cheguei a acreditar que este é um daqueles acordos que não podem ser traduzidos para a forma normal da tecla de domínio, embora eu possa estar errado. (Afinal, não há como dizer, afinal.) Dito isso, ainda tenho esperança por uma solução altamente normalizada.

Por acaso, estou implementando isso no SQL Server 2008 (a sintaxe pode ter sido uma dica), mas sou um cara curioso; portanto, se houver soluções que funcionem em outros DBMs, eu adoraria ouvir sobre isso também.

Foi útil?

Solução

Não complique, eu iria com gatilhos. Não há vergonha em usá -los, é para isso que eles estão lá.

Para evitar muita lógica nos gatilhos, adiciono uma coluna de bits "editáveis" na mesa de cabeçalho e basicamente uso uma divisão com editável para trabalhar ou causar uma divisão por zero erro, que eu pego e convertido a um Invoice is not editable, no changes permitted mensagem. Não existe não existe usado para eliminar uma sobrecarga extra. Experimente isso:

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

Aqui está um script de teste simples para testar as diferentes combinações:

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.

Outras dicas

Não consigo pensar em uma maneira de fazer isso com a normalização. No entanto, se eu quisesse restringir isso no banco de dados, faria 1 de duas maneiras:

Primeiro, eu adicionaria uma coluna 'bloqueada' às faturas, que é um pouco de algum tipo, apenas uma maneira de digitá -la como bloqueada.

Então, as duas maneiras:

  1. Um gatilho "antes da inserção", que lançaria um erro antes que uma inserção seja feita, se a fatura referida for bloqueada.
  2. Faça essa lógica no procedimento armazenado que cria a taxa.

EDIT: Não consegui encontrar um bom artigo do MSDN sobre como fazer um desses, mas a IBM tem um que funciona muito bem no SQL Server: http://publib.boulder.ibm.com/infocenter/iseries/v5r3/index.jsp?topic=/sqlp/rbafybeforesql.htm

Você pode restringir as adições à tabela de taxas alterando seu modelo de dados para usar:

FATURAS

  • INVOICE_ID
  • INVOICE_LOCKED_DATE, nulo

HONORÁRIOS

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

À primeira vista, é redundante, mas, desde que uma declaração de inserção para a tabela de taxas não inclua uma pesquisa para a tabela de faturas para a data bloqueada (o padrão de ser nulo) - garante que novos registros tenham a data em que a fatura foi bloqueada.

Outra opção é ter duas tabelas em relação ao manuseio de taxas - PRELIMINARY_FEES e CONFIRMED_FEES.

Enquanto as taxas de faturas ainda são editáveis, elas residem no PRELIMINIARY_FEES tabela e uma vez confirmada - são movidos para o CONFIRMED_FEES. Eu não gosto muito disso por ter que manter duas tabelas idênticas, juntamente com implicações de consulta, mas isso permitiria usar CONCEDERs (em uma função, não usuário, base) para permitir apenas selecionar acesso a CONFIRMED_FEES Ao permitir a inserção, atualize, exclua sobre o PRELIMINARY_FEES tabela. Você não pode restringir as doações em uma única configuração da tabela de taxas porque uma concessão não está ciente dos dados - você não pode verificar se um determinado status.

Eu acho que você será melhor armazenando explicitamente o estado 'bloqueado/desbloqueado' para a fatura na tabela de fatura e depois aplicar gatilhos na inserção e excluir (e atualizar, embora você não diga que deseja as taxas na fatura congelado) para evitar modificações se a fatura estiver no estado bloqueado.

O sinalizador bloqueado é necessário, a menos que haja um método algorítmico confiável para determinar quando a fatura está bloqueada - talvez 2 horas após a produção. Obviamente, você deve atualizar a linha da fatura para bloqueá -la - para que um método algorítmico seja melhor (menos atualizações).

Por que não apenas ter uma coluna 'bloqueada' que é uma booleana (ou char solteiro, 'y', 'n', por exemplo) e ajuste sua consulta de atualização para usar uma subconsulta:

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

Supondo que você tenha uma restrição não nula na coluna InvoiceId, a inserção falhará quando a fatura estiver bloqueada. Você pode lidar com a exceção em seu código e, assim, evitar adições de taxas quando a fatura estiver bloqueada. Você também evitará escrever e manter gatilhos complicados e procedimentos armazenados.

Ps. A consulta de inserção acima usa sintaxe MySQL, receio que não esteja familiarizado com a variante TQL do SQL Server.

Concordo com o consenso geral de que um bit de bloqueio deve ser adicionado à tabela de faturas para indicar se as taxas podem ser adicionadas. É então necessário adicionar código TSQL para aplicar as regras de negócios relacionadas a faturas bloqueadas. Sua postagem original não parece incluir detalhes sobre as condições sob as quais uma fatura fica bloqueada, mas é razoável supor que o bit de bloqueio possa ser definido adequadamente (esse aspecto do problema pode se tornar complicado, mas vamos resolver isso em outro fio).

Dado esse consenso, existem 2 opções de implementação que efetivamente aplicarão a regra de negócios no nível de dados: gatilhos e procedimentos armazenados padrão. Para usar os procedimentos armazenados padrão, é claro que é necessário negar atualizações, exclusão e inserções para as tabelas de faturas e taxas e exigir que toda a modificação de dados seja feita usando procedimentos armazenados.

A vantagem de usar gatilhos é que o código do cliente do aplicativo pode ser simplificado porque as tabelas podem ser acessadas diretamente. Essa pode ser uma vantagem importante se você estiver usando o LINQ para SQL, por exemplo.

Eu posso ver algumas vantagens no uso de procedimentos armazenados. Por um lado, acho que o uso de uma camada de procedimento armazenado é mais direto e, portanto, mais compreensível para programadores de manutenção. Eles, ou você daqui a vários anos, podem não se lembrar do gatilho inteligente que você criou, mas uma camada de procedimento armazenada é inconfundível. Em um ponto relacionado, eu argumentaria que existe o perigo de cair acidentalmente um gatilho; É menos provável que alguém mude acidentalmente as permissões nessas tabelas para torná -las diretamente graváveis. Embora qualquer um dos cenários seja possível, se houver muita coisa sobre isso, eu optaria pela opção de procedimento armazenado por uma questão de segurança.

Deve -se notar que essa discussão não é agnóstica do banco de dados: estamos discutindo as opções de implementação do SQL Server. Poderíamos usar uma abordagem semelhante com o Oracle ou qualquer outro servidor que forneça suporte processual para o SQL, mas essa regra de negócios não pode ser aplicada usando restrições estáticas, nem pode ser aplicada de maneira neutra no banco de dados.

Você não pode apenas usar restrições de FK e coisas do gênero - pelo menos de forma alguma que façam muito sentido. Eu sugeriria usar um AO INVÉS DE gatilho no SQL Server para aplicar essa restrição. Deve ser bastante fácil escrever e bastante direto.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top