SELEZIONA * ok in un trigger. O sto chiedendo problemi?


8

Sono coinvolto in un dibattito sul lavoro e ho bisogno di alcuni consigli su possibili insidie ​​che potrei trascurare.

Immagina uno scenario in cui viene utilizzato un trigger per copiare i record eliminati in una tabella di controllo. Il trigger utilizza SELECT *. Tutti indicano e gridano e ci dicono quanto sia brutto.

Tuttavia, se viene apportata una modifica alla struttura della tabella principale e la tabella di controllo viene trascurata, il trigger genererà un errore che consente alle persone di sapere che anche la tabella di controllo deve essere modificata.

L'errore verrà rilevato durante i test sui nostri server DEV. Ma dobbiamo garantire che DEVIA corrisponda alla produzione, quindi consentiamo SELECT * nei sistemi di produzione (solo trigger).

Quindi la mia domanda è: mi viene spinto a rimuovere SELECT *, ma non sono sicuro di come altro per garantire che stiamo acquisendo automaticamente errori di sviluppo di questa natura, qualche idea o questa è la migliore pratica?

Ho messo un esempio qui sotto:

--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'

AGGIORNAMENTO (riformula domanda):

Sono un DBA e devo assicurarmi che gli sviluppatori non forniscano script di implementazione poco ponderati contribuendo alla nostra documentazione sulle migliori pratiche. SELECT * provoca un errore in DEV quando lo sviluppatore trascura la tabella di audit (questa è una rete di sicurezza), quindi l'errore viene rilevato all'inizio del processo di sviluppo. Ma da qualche parte nella Costituzione SQL - 2 ° emendamento si legge "Non utilizzare SELECT *". Quindi ora c'è una spinta per sbarazzarsi della rete di sicurezza.

Come sostituiresti la rete di sicurezza o dovrei considerare questa come la migliore pratica per i trigger?

AGGIORNAMENTO 2: (soluzione)

Grazie per tutto il tuo contributo, non sono sicuro di avere una risposta chiara perché sembra essere un argomento molto grigio. Ma collettivamente hai fornito punti di discussione che possono aiutare i nostri sviluppatori ad andare avanti nel definire le loro migliori pratiche.

Grazie Daevinper il tuo contributo, la tua risposta fornisce le basi per alcuni meccanismi di test che i nostri sviluppatori possono implementare. +1

Grazie CM_Dayton, i tuoi suggerimenti che contribuiscono alle migliori pratiche possono essere utili per chiunque stia sviluppando Audit Trigger. +1

Grazie mille ypercube, hai sollevato molte riflessioni sulle questioni relative alle tabelle che subiscono diverse forme di modifica delle definizioni. +1

In conclusione:

Is Select * ok in a tigger? Sì, è un'area grigia, non seguire ciecamente l'ideologia "Seleziona * è cattivo".

Am I asking for Trouble? Sì, facciamo molto di più che aggiungere nuove colonne alle tabelle.


ti rispondi nella domanda. selezionare * si interromperà se la tabella di origine viene modificata. Per assicurarti che dev e prod siano gli stessi, usa una qualche forma di controllo del codice sorgente.
Bob Klimes,

domanda leggermente più ampia, con quale frequenza elimini i record e quante percentuali totali della tabella? un'alternativa ai trigger sarebbe quella di avere un bit flag che contrassegna le righe come cancellate e un lavoro dell'agente che viene eseguito su una pianificazione per spostarle in una tabella di registro. È possibile incorporare i controlli del lavoro dell'agente per vedere se lo schema della tabella corrisponde e il lavoro semplicemente fallirà se c'è un problema con quel passaggio fino a quando non viene risolto.
Tanner,

Di solito concordo con l' SELECT *essere pigro, ma poiché hai un motivo legittimo per usarlo è più grigio che in bianco e nero. Che cosa si dovrebbe provare a fare è qualcosa di simile questo , ma regolarlo avere non solo lo stesso numero di colonne, ma che i nomi delle colonne e tipi di dati sono gli stessi (dal momento che qualcuno potrebbe cambiare i tipi di dati e ancora causare problemi nel db normalmente non catturati con la tua SELECT *"rete di sicurezza".
Daevin,

3
Mi piace l'idea di utilizzare SELECT *come rete di sicurezza, ma non catturerà tutti i casi. Ad esempio, se si rilascia una colonna e la si aggiunge di nuovo. Ciò cambierà l'ordine delle colonne e (a meno che tutte le colonne non siano dello stesso tipo) gli inserimenti nella tabella di controllo falliranno o comporteranno la perdita di dati a causa delle conversioni di tipo implicite.
ypercubeᵀᴹ

2
Mi chiedo anche come funzionerà il tuo progetto di audit quando una colonna viene eliminata da una tabella. Elimina anche la colonna dalla tabella di controllo (e perdi tutti i dati di controllo precedenti)?
ypercubeᵀᴹ

Risposte:


10

In genere, è considerata una programmazione "pigra".

Dato che stai inserendo specificamente due valori nella tua TestAudittabella qui, starei attento a assicurarti che anche la tua selezione ottenga esattamente due valori. Perché se, per qualche ragione, quella Testtabella ha o ottiene una terza colonna, questo trigger fallirà.

Non direttamente correlato alla tua domanda, ma se stai impostando una tabella di controllo, aggiungerei anche alcune colonne aggiuntive alla tua TestAudittabella per ...

  • traccia l'azione che stai controllando (elimina in questo caso, vs inserti o aggiornamenti)
  • colonna data / ora per tenere traccia dell'evento di controllo
  • colonna ID utente per tenere traccia di chi ha eseguito l'azione che si sta verificando.

Ciò si traduce in una query come:

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

In questo modo stai ottenendo le colonne esatte di cui hai bisogno e stai controllando cosa / quando / perché / a chi si riferisce l'evento di controllo.


"ID utente" Questo è difficile con l'auditing. In genere, gli account del database non corrispondono agli utenti effettivi. Molto più spesso, corrispondono a una singola applicazione Web o altro tipo di componente, con un unico set di credenziali utilizzate da quel componente. (E a volte, i componenti condivideranno anche le credenziali.) Quindi le credenziali del database sono piuttosto inutili come identificatore di chi ha fatto cosa, a meno che non sia interessato a quale componente lo abbia fatto. Ma trasmettere dati applicativi che identificano il "chi" non è esattamente facile con una funzione di trigger, per quanto ne so.
jpmc26,

vedi aggiornamento alla domanda.
pacificamente

Un altro problema che potrebbe presentarsi con SELECT * in generale (anche se probabilmente non nel tuo esempio) è che se le colonne della tabella sottostante non sono nello stesso ordine delle colonne di inserimento, l'inserimento fallirà.
CaM

3

Ho commentato questo sulla tua domanda, ma ho pensato che avrei provato a presentare una soluzione di codice.

Di solito concordo con l' SELECT *essere pigro, ma poiché hai un motivo legittimo per usarlo è più grigio che in bianco e nero.

Che cosa si dovrebbe (a mio parere) prova a fare è qualcosa di simile questo , ma regolarlo per garantire i nomi delle colonne e tipi di dati sono gli stessi (dal momento che qualcuno potrebbe cambiare i tipi di dati e ancora causare problemi nel db normalmente non catturati con la tua SELECT *'sicurezza netto'.

Puoi persino creare una funzione che ti consenta di verificare rapidamente se la versione di controllo della tabella corrisponde alla versione non di controllo:

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

Il SELECT ... EXCEPT SELECT ...Auditvi mostrerà ciò che le colonne della tabella non sono nella tabella di controllo. È anche possibile modificare la funzione per restituire il nome di colonne che non sono uguali anziché semplicemente mappare o meno, o anche sollevare un'eccezione.

È quindi possibile eseguirlo prima di passare DEVai PRODUCTIONserver per ciascuna tabella nel db, utilizzando un cursore sopra:

SELECT TABLE_NAME
    FROM INFORMATION_SCHEMA.TABLES
    WHERE NOT (TABLE_NAME LIKE '%Audit')

1
vedi aggiornamento alla domanda
pacificamente

Sono contento di poterti aiutare. Ringraziatevi per aver letto tutte le risposte e averle riportate al vostro team per suggerimenti; adattabilità e volontà di miglioramento sono i modi in cui i dipartimenti tecnologici mantengono le aziende in funzione e senza intoppi! : D
Daevin,

0

L'istruzione che indicherà il trigger avrà esito negativo e il trigger avrà esito negativo. Sarebbe meglio documentare il trigger e l'audit trail in modo da sapere come modificare la query per aggiungere le colonne invece di specificare *.

Come minimo dovresti modificare il trigger in modo che possa fallire con garbo durante la registrazione degli errori su una tabella e magari mettere un avviso sulla tabella in cui il trigger sta registrando gli errori.

Ciò ricorda anche che è possibile inserire un trigger o un avviso quando qualcuno modifica la tabella e aggiunge più colonne o rimuove colonne, per avvisare l'utente di aggiungere il trigger.

Per quanto riguarda le prestazioni, credo * non cambia nulla, aumenta solo le possibilità di guasti lungo la strada quando le cose cambiano e può anche causare la latenza della rete quando si raccolgono più informazioni attraverso la rete quando è necessario. C'è un tempo e un posto per *, ma mi sento come descritto sopra hai invece soluzioni e strumenti migliori da provare.


0

Se la struttura della tua tabella di controllo originale o di controllo cambia, stai assicurando che potresti riscontrare un problema con la tua selezione *.

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

Se una delle due modifiche, il trigger si guiderà.

Potresti fare:

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

Ma come dice CM_Dayton, questa è una programmazione pigra e apre la porta ad altre incongruenze. Affinché questo scenario funzioni, dovresti assolutamente assicurarti di aggiornare la struttura di entrambe le tabelle contemporaneamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.