Access
 sql >> Base de données >  >> RDS >> Access

Conception d'un déclencheur Microsoft T-SQL

Concevoir un déclencheur Microsoft T-SQL

À certaines occasions, lors de la création d'un projet impliquant un frontal Access et un backend SQL Server, nous avons rencontré cette question. Devrions-nous utiliser un déclencheur pour quelque chose ? La conception d'un déclencheur SQL Server pour l'application Access peut être une solution, mais uniquement après mûre réflexion. Parfois, cela est suggéré comme moyen de conserver la logique métier dans la base de données, plutôt que dans l'application. Normalement, j'aime que la logique métier soit définie aussi près que possible de la base de données. Alors, est-ce que trigger est la solution que nous voulons pour notre front-end d'accès ?

J'ai trouvé que le codage d'un déclencheur SQL nécessite des considérations supplémentaires et si nous ne faisons pas attention, nous pouvons nous retrouver avec un plus gros gâchis que nous avons commencé. L'article vise à couvrir tous les pièges et techniques que nous pouvons utiliser pour nous assurer que lorsque nous construisons une base de données avec des déclencheurs, ils fonctionneront à notre avantage, plutôt que de simplement ajouter de la complexité pour la complexité.

Considérons les règles…

Règle n° 1 :n'utilisez pas de déclencheur !

Sérieusement. Si vous atteignez le déclencheur dès le matin, vous allez le regretter la nuit. Le plus gros problème avec les déclencheurs en général est qu'ils peuvent effectivement obscurcir votre logique métier et interférer avec des processus qui ne devraient pas avoir besoin d'un déclencheur. J'ai vu quelques suggestions pour désactiver les déclencheurs lorsque vous effectuez un chargement en masse ou quelque chose de similaire. J'affirme que c'est une grosse odeur de code. Vous ne devriez pas utiliser un déclencheur s'il doit être activé ou désactivé de manière conditionnelle.

Par défaut, nous devrions d'abord écrire des procédures stockées ou des vues. Pour la plupart des scénarios, ils feront très bien le travail. N'ajoutons pas de magie ici.

Alors pourquoi cet article sur la gâchette ?

Parce que les déclencheurs ont leurs utilisations. Nous devons reconnaître quand nous devons utiliser des déclencheurs. Nous devons également les écrire de manière à ce qu'elles nous aident plus qu'elles ne nous blessent.

Règle n° 2 :Ai-je vraiment besoin d'un déclencheur ?

En théorie, les déclencheurs sonnent bien. Ils nous fournissent un modèle basé sur les événements pour gérer les changements dès qu'ils sont modifiés. Mais si tout ce dont vous avez besoin est de valider certaines données ou de vous assurer que certaines colonnes masquées ou tables de journalisation sont remplies…. Je pense que vous constaterez qu'une procédure stockée fait le travail plus efficacement et supprime l'aspect magique. De plus, l'écriture d'une procédure stockée est facile à tester; configurez simplement des données fictives et exécutez la procédure stockée, vérifiez que les résultats correspondent à vos attentes. J'espère que vous utilisez un framework de test comme tSQLt.

Et il est important de noter qu'il est généralement plus efficace d'utiliser des contraintes de base de données qu'un déclencheur. Donc, si vous avez juste besoin de valider qu'une valeur est valide dans une autre table, utilisez une contrainte de clé étrangère. Valider qu'une valeur se trouve dans une certaine plage appelle une contrainte de vérification. Ceux-ci devraient être votre choix par défaut pour ce type de validations.

Alors, quand aurons-nous réellement besoin d'un déclencheur ?

Cela se résume aux cas où vous voulez vraiment que la logique métier soit dans la couche SQL. Peut-être parce que vous avez plusieurs clients dans différents langages de programmation qui effectuent des insertions/mises à jour dans une table. Il serait très désordonné de dupliquer la logique métier sur chaque client dans leur langage de programmation respectif, ce qui signifie également plus de bogues. Pour les scénarios où il n'est pas pratique de créer une couche de niveau intermédiaire, les déclencheurs constituent votre meilleur plan d'action pour appliquer la règle métier qui ne peut pas être exprimée sous forme de contrainte.

Pour utiliser un exemple spécifique à Access. Supposons que nous voulions appliquer la logique métier lors de la modification des données via l'application. Peut-être avons-nous plusieurs formulaires de saisie de données liés à une même table, ou peut-être devons-nous prendre en charge un formulaire de saisie de données complexe dans lequel plusieurs tables de base doivent participer à la modification. Peut-être que le formulaire de saisie de données doit prendre en charge des entrées non normalisées que nous recomposons ensuite en données normalisées. Dans tous ces cas, nous pourrions simplement écrire du code VBA, mais cela peut être difficile à maintenir et à valider dans tous les cas. Les déclencheurs nous aident à déplacer la logique de VBA vers T-SQL. La logique métier centrée sur les données est généralement mieux placée près des données que possible.

Règle n° 3 :le déclencheur doit être basé sur un ensemble et non sur une ligne

L'erreur de loin la plus courante commise avec un déclencheur est de le faire fonctionner sur des lignes. Nous voyons souvent un code similaire à celui-ci :

--Bad code! Do not use!
CREATE TRIGGER dbo.SomeTrigger
ON dbo.SomeTable AFTER INSERT
AS
BEGIN
  DECLARE @NewTotal money;
  DECLARE @NewID int;

  SELECT TOP 1
    @NewID = SalesOrderID,
    @NewTotal = SalesAmount
  FROM inserted;

  UPDATE dbo.SalesOrder
  SET OrderTotal = OrderTotal + @NewTotal
  WHERE SalesOrderID = @SalesOrderID
END;

Le cadeau devrait être le simple fait qu'il y avait un SELECT TOP 1 sur une table inséré. Cela ne fonctionnera que tant que nous n'insérerons qu'une seule ligne. Mais quand il y a plus d'une rangée, qu'arrive-t-il à ces rangées malchanceuses qui sont arrivées en 2e et après ? Nous pouvons améliorer cela en faisant quelque chose de similaire :

--Still bad code! Do not use!
CREATE TRIGGER dbo.SomeTrigger
ON dbo.SomeTable AFTER INSERT
AS
BEGIN
  MERGE INTO dbo.SalesOrder AS s
  USING inserted AS i
  ON s.SalesOrderID = i.SalesOrderID
  WHEN MATCHED THEN UPDATE SET
    OrderTotal = OrderTotal + @NewTotal
  ;
END;

Ceci est maintenant basé sur les ensembles et donc beaucoup amélioré, mais cela a encore d'autres problèmes que nous verrons dans les prochaines règles…

Règle n° 4 :Utilisez une vue à la place.

Une vue peut être associée à un déclencheur. Cela nous donne l'avantage d'éviter les problèmes liés aux déclencheurs d'une table. Nous pourrions facilement importer en bloc des données propres dans la table sans avoir à désactiver aucun déclencheur. De plus, un déclencheur à la vue en fait un choix explicite d'opt-in. Si vous avez des fonctionnalités liées à la sécurité ou des règles métier qui nécessitent l'exécution de déclencheurs, vous pouvez simplement révoquer directement les autorisations sur la table et ainsi les diriger vers la nouvelle vue à la place. Cela garantit que vous parcourrez le projet et noterez où des mises à jour du tableau sont nécessaires afin que vous puissiez ensuite les suivre pour tout bogue ou problème éventuel.

L'inconvénient est qu'une vue ne peut avoir qu'un déclencheur INSTEAD OF attaché, ce qui signifie que vous devez explicitement effectuer vous-même les modifications équivalentes sur la table de base dans le déclencheur. Cependant, j'ai tendance à penser que c'est mieux ainsi, car cela garantit également que vous savez exactement quelle sera la modification, et vous donne ainsi le même niveau de contrôle que vous avez normalement dans une procédure stockée.

Règle n° 5 :le déclencheur doit être simple et stupide.

Vous souvenez-vous du commentaire sur le débogage et le test d'une procédure stockée ? La meilleure faveur que nous puissions nous rendre est de conserver la logique métier dans une procédure stockée et de faire en sorte que le déclencheur l'invoque à la place. Vous ne devez jamais écrire de logique métier directement dans le déclencheur; c'est effectivement couler du béton sur la base de données. Il est maintenant figé à la forme et il peut être problématique de tester correctement la logique. Votre harnais de test doit maintenant impliquer une modification de la table de base. Ce n'est pas bon pour écrire des tests simples et reproductibles. Cela devrait être le plus compliqué car votre déclencheur devrait être autorisé :

CREATE TRIGGER [dbo].[SomeTrigger]
ON [dbo].[SomeView] INSTEAD OF INSERT, UPDATE, DELETE
AS
BEGIN
  DECLARE @SomeIDs AS SomeIDTableType

  --Perform the merge into the base table
  MERGE INTO dbo.SomeTable AS t
  USING inserted AS i
  ON t.SomeID = i.SomeID
  WHEN MATCHED THEN UPDATE SET
    t.SomeStuff = i.SomeStuff,
    t.OtherStuff = i.OtherStuff
  WHEN NOT MATCHED THEN INSERT (
    SomeStuff,
    OtherStuff
  ) VALUES (
    i.SomeStuff,
    i.OtherStuff
  )
  OUTPUT inserted.SomeID 
  INTO @SomeIDs(SomeID);

  DELETE FROM dbo.SomeTable
  OUTPUT deleted.SomeID 
  INTO @SomeIDs(SomeID)
  WHERE EXISTS (
    SELECT NULL
    FROM deleted AS d
    WHERE d.SomeID = SomeTable.SomeID
  ) AND NOT EXISTS (
    SELECT NULL
    FROM inserted AS i
    WHERE i.SomeID = SomeTable.SomeID
  );

  EXEC dbo.uspUpdateSomeStuff @SomeIDs;
END;

La première partie du déclencheur consiste essentiellement à effectuer les modifications réelles sur la table de base car il s'agit d'un déclencheur INSTEAD OF, nous devons donc effectuer toutes les modifications qui seront différentes selon les tables que nous devons gérer. Il convient de souligner que les modifications doivent être principalement textuelles. Nous ne recalculons ni ne transformons aucune des données. Nous économisons tout ce travail supplémentaire à la fin, où tout ce que nous faisons dans le déclencheur est de remplir une liste d'enregistrements qui ont été modifiés par le déclencheur et de les fournir à une procédure stockée à l'aide d'un paramètre table. Notez que nous ne considérons même pas quels enregistrements ont été modifiés ni comment ils ont été modifiés. Tout cela peut être fait dans la procédure stockée.

Règle n° 6 :le déclencheur doit être idempotent dans la mesure du possible.

De manière générale, les déclencheurs DOIVENT être idempotent. Cela s'applique, qu'il s'agisse d'un déclencheur basé sur une table ou sur une vue. Cela s'applique particulièrement à ceux qui ont besoin de modifier les données sur les tables de base à partir desquelles le déclencheur surveille. Pourquoi? Parce que si les humains modifient les données qui seront récupérées par le déclencheur, ils pourraient se rendre compte qu'ils ont fait une erreur, les modifier à nouveau ou peut-être simplement modifier le même enregistrement et le sauvegarder 3 fois. Ils ne seront pas contents s'ils constatent que les rapports changent chaque fois qu'ils apportent une modification qui n'est pas censée modifier la sortie du rapport.

Pour être plus explicite, il peut être tentant d'essayer d'optimiser le déclencheur en procédant comme suit :

WITH SourceData AS (
  SELECT OrderID, SUM(SalesAmount) AS NewSaleTotal
  FROM inserted
  GROUP BY OrderID
)
MERGE INTO dbo.SalesOrder AS o
USING SourceData AS d
ON o.OrderID = d.OrderID
WHEN MATCHED THEN UPDATE SET
  o.OrderTotal = o.OrderTotal + d.NewSaleTotal;

Nous arrivons à éviter de recalculer le nouveau total en examinant simplement les lignes modifiées dans le tableau inséré, n'est-ce pas ? Mais lorsque l'utilisateur modifie l'enregistrement pour corriger une faute de frappe dans le nom du client, que se passe-t-il ? Nous nous retrouvons avec un faux total, et le déclencheur joue maintenant contre nous.

À présent, vous devriez voir pourquoi la règle n ° 4 nous aide en ne poussant que les clés primaires de la procédure stockée, plutôt que d'essayer de transmettre des données dans la procédure stockée ou de le faire directement à l'intérieur du déclencheur comme l'exemple l'aurait fait .

Au lieu de cela, nous voulons avoir un code similaire à celui-ci dans une procédure stockée :

CREATE PROCEDURE dbo.uspUpdateSalesTotal (
  @SalesOrders SalesOrderTableType READONLY
) AS
BEGIN
  WITH SourceData AS (
    SELECT s.OrderID, SUM(s.SalesAmount) AS NewSaleTotal
    FROM dbo.SalesOrder AS s
    WHERE EXISTS (
      SELECT NULL
      FROM @SalesOrders AS x
      WHERE x.SalesOrderID = s.SalesOrderID
    )
    GROUP BY OrderID
  )
  MERGE INTO dbo.SalesOrder AS o
  USING SourceData AS d
  ON o.OrderID = d.OrderID
  WHEN MATCHED THEN UPDATE SET
    o.OrderTotal = d.NewSaleTotal;
END;

En utilisant @SalesOrders, nous pouvons toujours mettre à jour de manière sélective uniquement les lignes qui ont été affectées par le déclencheur, et nous pouvons également recalculer le nouveau total et en faire le nouveau total. Ainsi, même si l'utilisateur a fait une faute de frappe sur le nom du client et l'a modifié, chaque sauvegarde donnera le même résultat pour cette ligne.

Plus important encore, cette approche nous fournit également un moyen simple de corriger les totaux. Supposons que nous devions effectuer une importation en masse et que l'importation ne contienne pas le total, nous devons donc le calculer nous-mêmes. Nous pouvons écrire la procédure stockée pour écrire directement dans la table. Nous pouvons ensuite invoquer la procédure stockée ci-dessus en transmettant les ID de l'importation, et tout va bien. Ainsi, la logique que nous utilisons n'est pas liée au déclencheur derrière la vue. Cela aide lorsque la logique n'est pas nécessaire pour l'importation en masse que nous effectuons.

Si vous rencontrez des problèmes pour rendre votre déclencheur idempotent, cela indique fortement que vous devrez peut-être utiliser une procédure stockée à la place et l'appeler directement à partir de votre application au lieu de vous fier à des déclencheurs. Une exception notable à cette règle est lorsque le déclencheur est principalement destiné à être un déclencheur d'audit. Dans ce cas, vous souhaitez écrire une nouvelle ligne dans la table d'audit pour chaque modification, y compris toutes les fautes de frappe effectuées par l'utilisateur. C'est OK car dans ce cas, il n'y a aucun changement dans les données avec lesquelles l'utilisateur interagit. Depuis le POV de l'utilisateur, c'est toujours le même résultat. Mais chaque fois que le déclencheur doit manipuler les mêmes données que celles avec lesquelles l'utilisateur travaille, c'est bien mieux quand il est idempotent.

Conclusion

J'espère que maintenant, vous pouvez voir à quel point il peut être difficile de concevoir un déclencheur qui se comporte bien. Pour cette raison, vous devez examiner attentivement si vous pouvez l'éviter complètement et utiliser des invocations directes avec une procédure stockée. Mais si vous en avez conclu qu'il fallait des déclencheurs pour gérer les modifications faites via les vues, j'espère que les règles vous aideront. Il est assez facile de baser le déclencheur sur l'ensemble avec quelques ajustements. Le rendre idempotent nécessite généralement plus de réflexion sur la façon dont vous allez implémenter vos procédures stockées.

Si vous avez d'autres suggestions ou règles à partager, lancez-vous dans les commentaires !