1. Il trigger segue il principio ACID del database relazionale? È possibile che venga inserito un inserimento ma il trigger non riesce?
Questa domanda ha una risposta parziale a una domanda correlata a cui sei collegato. Il codice trigger viene eseguito nello stesso contesto transazionale dell'istruzione DML che lo ha causato, conservando la parte atomica dei principi ACID menzionati. L'istruzione di attivazione e il codice di attivazione hanno esito positivo o negativo come unità.
Le proprietà ACID garantiscono inoltre che l'intera transazione (incluso il codice trigger) lascerà il database in uno stato che non viola alcun vincolo esplicito ( coerente ) e che tutti gli effetti impegnati recuperabili sopravvivranno a un arresto anomalo del database ( durevole ).
A meno che la transazione circostante (forse implicita o con commit automatico) sia in esecuzione a SERIALIZABLE
livello di isolamento , la proprietà Isolata non viene automaticamente garantita. Altre attività di database simultanee potrebbero interferire con il corretto funzionamento del codice trigger. Ad esempio, il saldo del conto potrebbe essere modificato da un'altra sessione dopo averlo letto e prima di aggiornarlo, una condizione di gara classica.
2. Le mie dichiarazioni IF e UPDATE sembrano strane. Esiste un modo migliore per aggiornare la riga [Account] corretta?
Vi sono ottime ragioni per cui l'altra domanda a cui hai collegato non offre soluzioni basate su trigger. Il codice di trigger progettato per mantenere sincronizzata una struttura denormalizzata può essere estremamente difficile da ottenere correttamente e testare correttamente. Anche persone molto avanzate di SQL Server con molti anni di esperienza lottano con questo.
Mantenere buone prestazioni contemporaneamente a preservare la correttezza in tutti gli scenari ed evitare problemi come deadlock aggiunge ulteriori dimensioni di difficoltà. Il tuo codice trigger non è affatto vicino per essere robusto e aggiorna il saldo di ogni account anche se viene modificata una sola transazione. Esistono tutti i tipi di rischi e sfide con una soluzione basata su trigger, che rende il compito profondamente inadatto per qualcuno relativamente nuovo in questo settore tecnologico.
Per illustrare alcuni dei problemi, mostro alcuni esempi di codice di seguito. Questa non è una soluzione rigorosamente testata (i trigger sono difficili!) E non sto suggerendo di usarla come qualcosa di diverso da un esercizio di apprendimento. Per un sistema reale, le soluzioni non trigger presentano importanti vantaggi, quindi è necessario rivedere attentamente le risposte all'altra domanda ed evitare completamente l'idea del trigger.
Tabelle di esempio
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)
);
Prevenire TRUNCATE TABLE
I trigger non vengono attivati da TRUNCATE TABLE
. La seguente tabella vuota esiste esclusivamente per impedire il Transactions
troncamento della tabella (a cui fa riferimento una chiave esterna impedisce il troncamento della tabella):
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)
);
Trigger Definition
Il seguente codice di attivazione garantisce che vengano mantenute solo le voci di account necessarie e utilizza SERIALIZABLE
lì la semantica. Come effetto collaterale auspicabile, ciò evita anche i risultati errati che potrebbero derivare se si utilizza un livello di isolamento di versione delle righe. Il codice evita inoltre di eseguire il codice trigger se l'istruzione di origine non ha interessato righe. La tabella temporanea e il RECOMPILE
suggerimento vengono utilizzati per evitare problemi del piano di esecuzione del trigger causati da stime imprecise della cardinalità:
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;
analisi
Il codice seguente utilizza una tabella di numeri per creare 100.000 account con saldo zero:
INSERT dbo.Accounts
(AccountID, Balance)
SELECT
N.n, $0
FROM dbo.Numbers AS N
WHERE
N.n BETWEEN 1 AND 100000;
Il seguente codice di test inserisce 10.000 transazioni casuali:
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;
Utilizzando lo strumento SQLQueryStress , ho eseguito questo test 100 volte su 32 thread con buone prestazioni, senza deadlock e risultati corretti. Ancora non lo consiglio come qualcosa di diverso da un esercizio di apprendimento.