Eu tenho um requisito semelhante às discussões anteriores em:
- Escrevendo um esquema bancário simples: como devo manter meus saldos sincronizados com o histórico de transações?
- Gatilho em combinação com a transação
Eu tenho duas tabelas [Account].[Balance]
e [Transaction].[Amount]
:
CREATE TABLE Account (
AccountID INT
, Balance MONEY
);
CREATE TABLE Transaction (
TransactionID INT
, AccountID INT
, Amount MONEY
);
Quando houver uma inserção, atualização ou exclusão na [Transaction]
tabela, o [Account].[Balance]
deve ser atualizado com base no arquivo [Amount]
.
Atualmente eu tenho um gatilho para fazer este trabalho:
ALTER TRIGGER [dbo].[TransactionChanged]
ON [dbo].[Transaction]
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
IF EXISTS (select 1 from [Deleted]) OR EXISTS (select 1 from [Inserted])
UPDATE [dbo].[Account]
SET
[Account].[Balance] = [Account].[Balance] +
(
Select ISNULL(Sum([Inserted].[Amount]),0)
From [Inserted]
Where [Account].[AccountID] = [Inserted].[AccountID]
)
-
(
Select ISNULL(Sum([Deleted].[Amount]),0)
From [Deleted]
Where [Account].[AccountID] = [Deleted].[AccountID]
)
END
Embora isso pareça estar funcionando, tenho dúvidas:
- O gatilho segue o princípio ACID do banco de dados relacional? Existe alguma chance de uma inserção ser confirmada, mas o gatilho falhar?
- Minhas declarações
IF
eUPDATE
parecem estranhas. Existe alguma maneira melhor de atualizar a[Account]
linha correta?
Esta pergunta é parcialmente respondida em uma pergunta relacionada à qual você vinculou. O código do gatilho é executado no mesmo contexto transacional da instrução DML que o fez disparar, preservando a parte atômica dos princípios ACID que você mencionou. A instrução de acionamento e o código do acionador são bem-sucedidos ou falham como uma unidade.
As propriedades ACID também garantem que toda a transação (incluindo o código do acionador) deixará o banco de dados em um estado que não viola nenhuma restrição explícita ( Consistente ) e quaisquer efeitos consolidados recuperáveis sobreviverão a uma falha do banco de dados ( Durável ).
A menos que a transação circundante (talvez implícita ou de confirmação automática) esteja em execução no
SERIALIZABLE
nível de isolamento , a propriedade Isolated não é garantida automaticamente. Outra atividade de banco de dados simultânea pode interferir na operação correta de seu código de gatilho. Por exemplo, o saldo da conta pode ser alterado por outra sessão depois de lê-lo e antes de atualizá-lo - uma clássica condição de corrida.Existem razões muito boas para a outra pergunta à qual você vinculou não oferecer nenhuma solução baseada em gatilho. O código de gatilho projetado para manter uma estrutura desnormalizada sincronizada pode ser extremamente complicado de acertar e testar corretamente. Mesmo pessoas muito avançadas do SQL Server com muitos anos de experiência lutam com isso.
Manter um bom desempenho ao mesmo tempo em que preserva a exatidão em todos os cenários e evita problemas como impasses adiciona dimensões extras de dificuldade. Seu código de gatilho está longe de ser robusto e atualiza o saldo de todas as contas , mesmo que apenas uma única transação seja modificada. Existem todos os tipos de riscos e desafios com uma solução baseada em gatilho, o que torna a tarefa profundamente inadequada para alguém relativamente novo nessa área de tecnologia.
Para ilustrar alguns dos problemas, mostro alguns exemplos de código abaixo. Esta não é uma solução rigorosamente testada (os gatilhos são difíceis!) e não estou sugerindo que você a use como outra coisa senão um exercício de aprendizado. Para um sistema real, as soluções não-gatilho têm benefícios importantes, portanto, você deve revisar cuidadosamente as respostas para a outra pergunta e evitar completamente a ideia do gatilho.
Tabelas de exemplo
prevenir
TRUNCATE TABLE
Gatilhos não são acionados por
TRUNCATE TABLE
. A tabela vazia a seguir existe apenas para evitar que aTransactions
tabela seja truncada (ser referenciada por uma chave estrangeira evita o truncamento da tabela):Definição de gatilho
O código de gatilho a seguir garante que apenas as entradas de conta necessárias sejam mantidas e usa
SERIALIZABLE
semântica lá. Como um efeito colateral desejável, isso também evita os resultados incorretos que podem resultar se um nível de isolamento de controle de versão de linha estiver em uso. O código também evita a execução do código do gatilho se nenhuma linha for afetada pela instrução de origem. A tabela temporária eRECOMPILE
a dica são usadas para evitar problemas no plano de execução do acionador causados por estimativas de cardinalidade imprecisas:teste
O código a seguir usa uma tabela de números para criar 100.000 contas com saldo zero:
O código de teste abaixo insere 10.000 transações aleatórias:
Usando a ferramenta SQLQueryStress , executei este teste 100 vezes em 32 threads com bom desempenho, sem deadlocks e resultados corretos. Eu ainda não recomendo isso como nada além de um exercício de aprendizado.