Est SELECT * ok dans un déclencheur. Ou est-ce que je demande des ennuis?

8

Je suis pris dans un débat au travail et j'ai besoin de conseils sur les pièges que je pourrais ignorer.

Imaginez un scénario dans lequel un déclencheur est utilisé pour copier des enregistrements supprimés dans une table d'audit. Le déclencheur utilise SELECT *. Tout le monde pointe et crie et nous dit à quel point c'est mauvais.

Cependant, si une modification est apportée à la structure de la table principale et que la table d'audit est ignorée, le déclencheur générera une erreur permettant aux utilisateurs de savoir que la table d'audit doit également être modifiée.

L'erreur sera détectée lors des tests sur nos serveurs DEV. Mais nous devons nous assurer que les correspondances de production DEV, nous autorisons donc SELECT * dans les systèmes de production (déclencheurs uniquement).

Donc, ma question est: on me pousse à supprimer le SELECT *, mais je ne sais pas comment garantir que nous capturons automatiquement les erreurs de développement de cette nature, des idées ou est-ce la meilleure pratique?

J'ai rassemblé un exemple ci-dessous:

--Create Test Table
CREATE TABLE dbo.Test(ID INT IDENTITY(1,1), Person VARCHAR(255))
--Create Test Audit Table
CREATE TABLE dbo.TestAudit(AuditID INT IDENTITY(1,1),ID INT, Person VARCHAR(255))

--Create Trigger on Test
CREATE TRIGGER [dbo].[trTestDelete] ON [dbo].[Test] AFTER DELETE
NOT FOR REPLICATION
AS
BEGIN
    SET NOCOUNT ON;
    INSERT  dbo.TestAudit([ID], [Person])
    SELECT  *
    FROM    deleted
END

--Insert Test Data into Test
INSERT INTO dbo.Test VALUES
('Scooby')
,('Fred')
,('Shaggy')

--Perform a delete
DELETE dbo.Test WHERE Person = 'Scooby'

MISE À JOUR (reformuler la question):

Je suis un DBA et je dois m'assurer que les développeurs ne fournissent pas de scripts de déploiement mal pensés en contribuant à notre documentation des meilleures pratiques. SELECT * provoque une erreur dans DEV lorsque le développeur ignore la table d'audit (il s'agit d'un filet de sécurité), de sorte que l'erreur est détectée au début du processus de développement. Mais quelque part dans la Constitution SQL - 2ème amendement, il est écrit "Tu ne dois pas utiliser SELECT *". Il y a donc maintenant un effort pour se débarrasser du filet de sécurité.

Comment remplacer le filet de sécurité ou devrais-je considérer que c'est la meilleure pratique pour les déclencheurs?

MISE À JOUR 2: (solution)

Merci pour toutes vos contributions, je ne suis pas sûr d'avoir une réponse claire car cela semble être un sujet très gris. Mais collectivement, vous avez fourni des points de discussion qui peuvent aider nos développeurs à avancer dans la définition de leurs meilleures pratiques.

Merci Daevinpour votre contribution, votre réponse fournit les bases de certains mécanismes de test que nos développeurs peuvent implémenter. +1

Merci CM_Dayton, vos suggestions contribuant aux meilleures pratiques peuvent être utiles à toute personne qui développe des déclencheurs d'audit. +1

Merci ypercubebeaucoup, vous avez beaucoup réfléchi aux problèmes concernant les tables subissant différentes formes de modifications de définition. +1

En conclusion:

Is Select * ok in a tigger? Oui, c'est une zone grise, ne suivez pas aveuglément l'idéologie "Select * is Bad".

Am I asking for Trouble? Oui, nous faisons plus que simplement ajouter de nouvelles colonnes aux tableaux.

pacifiquement
la source
vous vous répondez dans la question. sélectionnez * se cassera si la table source est modifiée. Pour vous assurer que dev et prod sont identiques, utilisez une forme de contrôle de code source.
Bob Klimes
question légèrement plus large, à quelle fréquence supprimez-vous des enregistrements et combien en tant que pourcentage total du tableau? une alternative aux déclencheurs serait d'avoir un indicateur de bit qui marque les lignes comme supprimées et un travail d'agent qui s'exécute selon une planification pour les déplacer vers une table de journal. Vous pouvez intégrer les vérifications de travail de l'agent pour voir si le schéma de table correspond et le travail échouera simplement s'il y a un problème avec cette étape jusqu'à ce qu'il soit corrigé.
Tanner
Je suis généralement d'accord avec le fait d' SELECT *être paresseux, mais puisque vous avez une raison légitime de l'utiliser, c'est plus gris que noir et blanc. Ce que vous devriez essayer de faire est quelque chose comme ça , mais ajustez-le non seulement pour avoir le même nombre de colonnes, mais aussi pour que les noms de colonnes et les types de données soient les mêmes (car quelqu'un pourrait changer les types de données et toujours causer des problèmes dans la base de données qui ne sont pas normalement interceptés avec votre SELECT *«filet de sécurité»
Daevin
3
J'aime l'idée de l'utiliser SELECT *comme filet de sécurité, mais elle n'attrapera pas tous les cas. Par exemple, si vous déposez une colonne et l'ajoutez à nouveau. Cela modifiera l'ordre des colonnes et (sauf si toutes les colonnes sont du même type), les insertions dans la table d'audit échoueront ou entraîneront une perte de données en raison des conversions implicites de types.
ypercubeᵀᴹ
2
Je me demande également comment fonctionnera votre conception d'audit lorsqu'une colonne est supprimée d'une table. Supprimez-vous également la colonne de la table d'audit (et perdez toutes les données d'audit précédentes)?
ypercubeᵀᴹ

Réponses:

10

En règle générale, il est considéré comme une programmation «paresseuse».

Étant donné que vous insérez spécifiquement deux valeurs dans votre TestAudittableau ici, je veillerais à ce que votre sélection obtienne également exactement deux valeurs. Parce que si, pour une raison quelconque, cette Testtable a ou obtient une troisième colonne, ce déclencheur échouera.

Pas directement lié à votre question, mais si vous configurez une table d'audit, j'ajouterais également des colonnes supplémentaires à votre TestAudittable pour ...

  • suivre l'action que vous auditez (supprimer dans ce cas, vs insertions ou mises à jour)
  • colonne de date / heure pour suivre le moment où l'événement d'audit s'est produit
  • colonne d'ID utilisateur pour suivre qui a effectué l'action que vous auditez.

Cela se traduit donc par une requête comme:

INSERT dbo.TestAudit([ID], [Person], [AuditAction], [ChangedOn], [ChangedBy])
SELECT [ID], [Person], 
   'Delete', -- or a 'D' or a numeric lookup to an audit actions table...
   GetDate(), -- or SYSDATETIME() for greater precision
   SYSTEM_USER -- or some other value for WHO made the deletion
FROM deleted

De cette façon, vous obtenez les colonnes exactes dont vous avez besoin et vous auditez sur quoi / quand / pourquoi / qui est l'événement d'audit.

Came
la source
"ID utilisateur" Celui-ci est délicat avec l'audit. En règle générale, les comptes de base de données ne correspondent pas aux utilisateurs réels. Plus souvent, ils correspondent à une seule application Web ou à un autre type de composant, avec un seul ensemble d'informations d'identification utilisées par ce composant. (Et parfois, les composants partagent également des informations d'identification.) Les informations d'identification de la base de données sont donc assez inutiles pour identifier qui a fait quoi, sauf si vous êtes simplement intéressé par quel composant l'a fait. Mais pour autant que je sache, transmettre des données d'application qui identifient le "qui" n'est pas exactement facile avec une fonction de déclenchement.
jpmc26
voir la mise à jour de la question.
pacreely
Un autre problème qui pourrait survenir avec SELECT * en général (mais probablement pas dans votre exemple) est que si les colonnes de la table sous-jacente ne sont pas dans le même ordre que vos colonnes d'insertion, l'insertion échouera.
CaM
3

J'ai commenté cela sur votre question, mais j'ai pensé que j'essaierais de présenter une solution de code.

Je suis généralement d'accord pour SELECT *être paresseux, mais comme vous avez une raison légitime de l'utiliser, c'est plus gris que noir et blanc.

Ce que vous devriez (à mon avis) essayer de faire est quelque chose comme ça , mais ajustez-le pour vous assurer que les noms de colonne et les types de données sont les mêmes (car quelqu'un pourrait changer les types de données et toujours causer des problèmes dans la base de données qui ne sont pas normalement pris en compte avec votre SELECT *sécurité) net'.

Vous pouvez même créer une fonction qui vous permettra de vérifier rapidement si la version d'audit de la table correspond à la version non audit:

-- The lengths are, I believe, max values for the corresponding db objects. If I'm wrong, someone please correct me
CREATE FUNCTION TableMappingComparer(
    @TableCatalog VARCHAR(85) = NULL,
    @TableSchema VARCHAR(32) = NULL,
    @TableName VARCHAR(128) = NULL) RETURNS BIT
AS
BEGIN
    DECLARE @ReturnValue BIT = NULL;
    DECLARE @VaryingColumns INT = NULL;

    IF (@TableCatalog IS NOT NULL
            AND @TableSchema IS NOT NULL
            AND @TableName IS NOT NULL)
        SELECT @VaryingColumns = COUNT(COLUMN_NAME)
            FROM (SELECT COLUMN_NAME,
                        DATA_TYPE -- Add more columns that you want to ensure are identical
                    FROM INFORMATION_SCHEMA.COLUMNS
                    WHERE TABLE_CATALOG = @TableCatalog
                        AND TABLE_SCHEMA = @TableSchema
                        AND TABLE_NAME = @TableName
                EXCEPT
                    SELECT COLUMN_NAME,
                            DATA_TYPE -- Add more columns that you want to ensure are identical
                        FROM INFORMATION_SCHEMA.COLUMNS
                        WHERE (TABLE_CATALOG = @TableCatalog
                            AND TABLE_SCHEMA = @TableSchema
                            AND TABLE_NAME = @TableName + 'Audit')
                            AND (COLUMN_NAME != 'exclude your audit table specific columns')) adt;
    IF @VaryingColumns = 0
        SET @ReturnValue = 1
    ELSE IF @VaryingColumns > 0
        SET @ReturnValue = 0

    RETURN @ReturnValue;
END;

Le SELECT ... EXCEPT SELECT ...Auditvous montrera quelles colonnes du tableau ne se trouvent pas dans le tableau d'audit. Vous pouvez même modifier la fonction pour renvoyer le nom de colonnes qui ne sont pas les mêmes au lieu de simplement mapper ou non, ou même lever une exception.

Vous pouvez ensuite l'exécuter avant de passer de DEVaux PRODUCTIONserveurs pour chaque table de la base de données, en utilisant un curseur sur:

SELECT TABLE_NAME
    FROM INFORMATION_SCHEMA.TABLES
    WHERE NOT (TABLE_NAME LIKE '%Audit')
Daevin
la source
1
voir la mise à jour de la question
pacreely
Heureux d'avoir pu aider. Nous vous remercions d'avoir lu toutes les réponses et de les avoir rapportées à votre équipe pour des suggestions; l'adaptabilité et la volonté de s'améliorer sont les moyens par lesquels les départements techniques assurent le bon fonctionnement des entreprises et leur bon fonctionnement! : D
Daevin
0

L'instruction qui signalera le déclencheur échouera et le déclencheur échouera. Il serait préférable de documenter le déclencheur et la piste d'audit afin que vous sachiez modifier la requête pour ajouter les colonnes au lieu de spécifier le *.

À tout le moins, vous devez modifier le déclencheur afin qu'il puisse échouer correctement lors de la journalisation des erreurs dans une table et peut-être placer une alerte sur la table dans laquelle le déclencheur enregistre les erreurs.

Cela vous rappelle également que vous pouvez mettre un déclencheur ou une alerte lorsque quelqu'un modifie la table et ajoute plus de colonnes ou supprime des colonnes, pour vous informer d'ajouter le déclencheur.

En termes de performances, je crois * que cela ne change rien, cela augmente simplement les risques d'échecs sur la route lorsque les choses changent et peut également provoquer une latence du réseau lorsque vous récupérez plus d'informations sur le réseau lorsque vous en avez besoin. Il y a un temps et un lieu pour *, mais je pense que comme décrit ci-dessus, vous avez de meilleures solutions et outils à essayer à la place.

Shaulinator
la source
0

Si vos structures de table d'origine ou d'audit changent, vous garantissez que vous rencontrerez un problème avec votre select *.

INSERT INTO [AuditTable]
(Col1,Col2,Col3)
SELECT * 
FROM [OrigTable] or [deleted];

Si l'un ou l'autre change, le déclencheur produira une erreur.

Vous pourriez faire:

INSERT INTO [AuditTable]
SELECT * 
FROM [OrigTable];

Mais comme le dit CM_Dayton, c'est une programmation paresseuse et ouvre la porte à d'autres incohérences. Pour que ce scénario fonctionne, vous devez vous assurer que vous mettez à jour la structure des deux tables en même temps.

MguerraTorres
la source