Synchronisation à l'aide de déclencheurs


11

J'ai une exigence similaire aux discussions précédentes à:

J'ai deux tables [Account].[Balance]et [Transaction].[Amount]:

CREATE TABLE Account (
      AccountID    INT
    , Balance      MONEY
);

CREATE TABLE Transaction (
      TransactionID INT
     , AccountID    INT
    , Amount      MONEY
);

Lorsqu'il y a une insertion, une mise à jour ou une suppression par rapport au [Transaction]tableau, le [Account].[Balance]doit être mis à jour en fonction du [Amount].

Actuellement, j'ai un déclencheur pour faire ce travail:

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

Bien que cela semble fonctionner, j'ai des questions:

  1. Le déclencheur suit-il le principe ACID de la base de données relationnelle? Y a-t-il une chance qu'une insertion puisse être validée mais le déclencheur échoue?
  2. Mes déclarations IFet UPDATEsemblent étranges. Existe-t-il un meilleur moyen de mettre à jour la bonne [Account]ligne?

Réponses:


13

1. Le déclencheur suit-il le principe ACID de la base de données relationnelle? Y a-t-il une chance qu'une insertion puisse être validée mais le déclencheur échoue?

Cette question trouve une réponse partielle dans une question connexe à laquelle vous êtes lié. Le code de déclenchement est exécuté dans le même contexte transactionnel que l'instruction DML qui l'a provoqué, en préservant la partie atomique des principes ACID que vous mentionnez. L'instruction de déclenchement et le code de déclenchement réussissent ou échouent en tant qu'unité.

Les propriétés ACID garantissent également que la transaction entière (y compris le code de déclenchement) laissera la base de données dans un état qui ne viole aucune contrainte explicite ( cohérente ) et tout effet engagé récupérable survivra à un crash de la base de données ( durable ).

Sauf si la transaction environnante (peut-être implicite ou auto-commit) s'exécute au SERIALIZABLEniveau d'isolement , la propriété Isolated n'est pas automatiquement garantie. D'autres activités de base de données simultanées peuvent interférer avec le bon fonctionnement de votre code de déclenchement. Par exemple, le solde du compte peut être modifié par une autre session après l'avoir lu et avant de le mettre à jour - une condition de concurrence classique.

2. Mes instructions IF et UPDATE semblent étranges. Existe-t-il un meilleur moyen de mettre à jour la ligne [Compte] correcte?

Il y a de très bonnes raisons pour lesquelles l'autre question à laquelle vous avez lié n'offre aucune solution basée sur les déclencheurs. Le code de déclenchement conçu pour maintenir une structure dénormalisée synchronisée peut être extrêmement difficile à obtenir correctement et à tester correctement. Même les personnes très avancées de SQL Server avec de nombreuses années d'expérience ont du mal avec cela.

Le maintien de bonnes performances tout en préservant l'exactitude dans tous les scénarios et en évitant les problèmes tels que les blocages ajoute des dimensions de difficulté supplémentaires. Votre code de déclenchement est loin d'être robuste et met à jour le solde de chaque compte même si une seule transaction est modifiée. Il existe toutes sortes de risques et de défis avec une solution basée sur les déclencheurs, ce qui rend la tâche profondément inappropriée pour une personne relativement nouvelle dans ce domaine technologique.

Pour illustrer certains des problèmes, je montre un exemple de code ci-dessous. Ce n'est pas une solution rigoureusement testée (les déclencheurs sont difficiles!) Et je ne vous suggère pas de l'utiliser comme autre chose qu'un exercice d'apprentissage. Pour un système réel, les solutions sans déclencheur présentent des avantages importants, vous devez donc examiner attentivement les réponses à l'autre question et éviter complètement l'idée de déclencheur.

Exemples de tableaux

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)
);

Prévenir TRUNCATE TABLE

Les déclencheurs ne sont pas déclenchés par TRUNCATE TABLE. La table vide suivante existe uniquement pour empêcher la Transactionstable d'être tronquée (le fait d'être référencé par une clé étrangère empêche la troncature de la table):

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)
);

Définition du déclencheur

Le code de déclenchement suivant garantit que seules les entrées de compte nécessaires sont conservées et utilise la SERIALIZABLEsémantique à cet endroit. En tant qu'effet secondaire souhaitable, cela évite également les résultats incorrects qui pourraient se produire si un niveau d'isolement de versionnage de ligne est utilisé. Le code évite également l'exécution du code de déclenchement si aucune ligne n'a été affectée par l'instruction source. La table temporaire et l' RECOMPILEindicateur sont utilisés pour éviter les problèmes de plan d'exécution du déclencheur causés par des estimations de cardinalité inexactes:

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;

Essai

Le code suivant utilise une table de nombres pour créer 100 000 comptes avec un solde nul:

INSERT dbo.Accounts
    (AccountID, Balance)
SELECT
    N.n, $0
FROM dbo.Numbers AS N
WHERE
    N.n BETWEEN 1 AND 100000;

Le code de test ci-dessous insère 10 000 transactions aléatoires:

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;

À l'aide de l' outil SQLQueryStress , j'ai exécuté ce test 100 fois sur 32 threads avec de bonnes performances, aucun blocage et des résultats corrects. Je ne recommande toujours pas cela comme autre chose qu'un exercice d'apprentissage.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.