1. O gatilho segue o princípio ACID do banco de dados relacional? Existe alguma chance de uma inserção ser confirmada, mas o gatilho falha?
Esta pergunta é parcialmente respondida em uma pergunta relacionada à qual você vinculou. O código de acionamento é executado no mesmo contexto transacional que a instrução DML que causou o disparo, preservando a parte Atômica dos princípios ACID mencionados. A instrução de acionamento e o código de 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 viole restrições explícitas ( Consistente ) e quaisquer efeitos confirmados recuperáveis sobreviverão a uma falha no banco de dados ( Durável ).
A menos que a transação circundante (talvez implícita ou confirmada automaticamente) esteja sendo executada no SERIALIZABLE
nível de isolamento , a propriedade Isolated não será garantida automaticamente. Outra atividade simultânea do banco de dados pode interferir na operação correta do seu código de gatilho. Por exemplo, o saldo da conta pode ser alterado por outra sessão após a leitura e antes da atualização - uma condição de corrida clássica.
2. Minhas declarações IF e UPDATE parecem estranhas. Existe alguma maneira melhor de atualizar a linha [Conta] correta?
Há boas razões pelas quais a outra pergunta à qual você se vinculou não oferece nenhuma solução baseada em gatilho. O código de acionamento projetado para manter uma estrutura desnormalizada sincronizada pode ser extremamente complicado para acertar e testar corretamente. Até 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 correção em todos os cenários e evita problemas como conflitos aumenta dimensões adicionais de dificuldade. Seu código de gatilho não está nem perto 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 acionadoras têm benefícios importantes, portanto, você deve revisar cuidadosamente as respostas para a outra pergunta e evitar completamente a ideia do acionador.
Tabelas de amostra
CREATE TABLE dbo.Accounts
(
AccountID integer NOT NULL,
Balance money NOT NULL,
CONSTRAINT PK_Accounts_ID
PRIMARY KEY CLUSTERED (AccountID)
);
CREATE TABLE dbo.Transactions
(
TransactionID integer IDENTITY NOT NULL,
AccountID integer NOT NULL,
Amount money NOT NULL,
CONSTRAINT PK_Transactions_ID
PRIMARY KEY CLUSTERED (TransactionID),
CONSTRAINT FK_Accounts
FOREIGN KEY (AccountID)
REFERENCES dbo.Accounts (AccountID)
);
Prevenção TRUNCATE TABLE
Os gatilhos não são acionados por TRUNCATE TABLE
. A tabela vazia a seguir existe apenas para impedir que a Transactions
tabela seja truncada (ser referenciada por uma chave estrangeira impede o truncamento da tabela):
CREATE TABLE dbo.PreventTransactionsTruncation
(
Dummy integer NULL,
CONSTRAINT FK_Transactions
FOREIGN KEY (Dummy)
REFERENCES dbo.Transactions (TransactionID),
CONSTRAINT CHK_NoRows
CHECK (Dummy IS NULL AND Dummy IS NOT NULL)
);
Definição de gatilho
O código de acionamento a seguir garante que apenas as entradas de conta necessárias sejam mantidas e use a SERIALIZABLE
semântica. Como efeito colateral desejável, isso também evita resultados incorretos que podem resultar se um nível de isolamento de versão de linha estiver em uso. O código também evita a execução do código acionador se nenhuma linha foi afetada pela instrução de origem. A tabela e a RECOMPILE
dica temporárias são usadas para evitar problemas no plano de execução do acionador causados por estimativas imprecisas da cardinalidade:
CREATE TRIGGER dbo.TransactionChange ON dbo.Transactions
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
IF @@ROWCOUNT = 0 OR
TRIGGER_NESTLEVEL
(
OBJECT_ID(N'dbo.TransactionChange', N'TR'),
'AFTER',
'DML'
) > 1
RETURN;
SET NOCOUNT, XACT_ABORT ON;
CREATE TABLE #Delta
(
AccountID integer PRIMARY KEY,
Amount money NOT NULL
);
INSERT #Delta
(AccountID, Amount)
SELECT
InsDel.AccountID,
Amount = SUM(InsDel.Amount)
FROM
(
SELECT AccountID, Amount
FROM Inserted
UNION ALL
SELECT AccountID, $0 - Amount
FROM Deleted
) AS InsDel
GROUP BY
InsDel.AccountID;
UPDATE A
SET Balance += D.Amount
FROM #Delta AS D
JOIN dbo.Accounts AS A WITH (SERIALIZABLE)
ON A.AccountID = D.AccountID
OPTION (RECOMPILE);
END;
Teste
O código a seguir usa uma tabela de números para criar 100.000 contas com saldo zero:
INSERT dbo.Accounts
(AccountID, Balance)
SELECT
N.n, $0
FROM dbo.Numbers AS N
WHERE
N.n BETWEEN 1 AND 100000;
O código de teste abaixo insere 10.000 transações aleatórias:
INSERT dbo.Transactions
(AccountID, Amount)
SELECT
CONVERT(integer, RAND(CHECKSUM(NEWID())) * 100000 + 1),
CONVERT(money, RAND(CHECKSUM(NEWID())) * 500 - 250)
FROM dbo.Numbers AS N
WHERE
N.n BETWEEN 1 AND 10000;
Usando a ferramenta SQLQueryStress , eu executei esse teste 100 vezes em 32 threads com bom desempenho, sem conflitos e resultados corretos. Eu ainda não recomendo isso como algo além de um exercício de aprendizado.