Access
 sql >> Database >  >> RDS >> Access

Progettazione di un trigger Microsoft T-SQL

Progettazione di un trigger Microsoft T-SQL

In alcune occasioni durante la creazione di un progetto che coinvolge un front-end di Access e un back-end di SQL Server, ci siamo imbattuti in questa domanda. Dovremmo usare un trigger per qualcosa? La progettazione di un trigger di SQL Server per l'applicazione Access può essere una soluzione, ma solo dopo attente considerazioni. A volte questo viene suggerito come un modo per mantenere la logica aziendale all'interno del database, piuttosto che nell'applicazione. Normalmente, mi piace avere la logica aziendale definita il più vicino possibile al database. Quindi, trigger è la soluzione che desideriamo per il nostro front-end di accesso?

Ho scoperto che la codifica di un trigger SQL richiede considerazioni aggiuntive e se non stiamo attenti possiamo finire con un pasticcio più grande di quello che abbiamo iniziato. L'articolo mira a coprire tutte le insidie ​​e le tecniche che possiamo utilizzare per garantire che quando creiamo un database con trigger, funzioneranno a nostro vantaggio, piuttosto che aggiungere complessità per motivi di complessità.

Consideriamo le regole...

Regola n. 1:non utilizzare un trigger!

Sul serio. Se stai raggiungendo il grilletto per prima cosa al mattino, te ne pentirai di notte. Il problema più grande con i trigger in generale è che possono offuscare efficacemente la logica aziendale e interferire con processi che non dovrebbero richiedere un trigger. Ho visto alcuni suggerimenti per disattivare i trigger quando si esegue un caricamento di massa o qualcosa di simile. Affermo che questo è un grande odore di codice. Non dovresti utilizzare un trigger se deve essere attivato o disattivato in modo condizionale.

Per impostazione predefinita, dovremmo prima scrivere stored procedure o viste. Per la maggior parte degli scenari, faranno il lavoro bene. Non aggiungiamo la magia qui.

Allora perché l'articolo su trigger allora?

Perché i trigger hanno i loro usi. Dobbiamo riconoscere quando dovremmo usare i trigger. Dobbiamo anche scriverli in un modo che ci aiuti più che ferirci.

Regola n. 2:ho davvero bisogno di un trigger?

In teoria, i trigger suonano bene. Ci forniscono un modello basato sugli eventi per gestire le modifiche non appena vengono modificate. Ma se tutto ciò di cui hai bisogno è convalidare alcuni dati o assicurarti che alcune colonne nascoste o tabelle di registrazione siano popolate…. Penso che scoprirai che una procedura memorizzata svolge il lavoro in modo più efficiente e rimuove l'aspetto magico. Inoltre, la scrittura di una stored procedure è facile da testare; è sufficiente impostare alcuni dati fittizi ed eseguire la procedura memorizzata, verificare che i risultati siano quelli previsti. Spero che tu stia utilizzando un framework di test come tSQLt.

Ed è importante notare che in genere è più efficiente utilizzare i vincoli del database rispetto a un trigger. Quindi, se hai solo bisogno di convalidare che un valore è valido in un'altra tabella, usa un vincolo di chiave esterna. La convalida che un valore rientri in un determinato intervallo richiede un vincolo di controllo. Dovrebbero essere la tua scelta predefinita per questo tipo di convalide.

Quindi, quando avremo effettivamente bisogno di un trigger?

Si riduce ai casi in cui si desidera davvero che la logica aziendale sia nel livello SQL. Forse perché hai più client in diversi linguaggi di programmazione che eseguono inserimenti/aggiornamenti in una tabella. Sarebbe molto complicato duplicare la logica aziendale su ciascun client nel rispettivo linguaggio di programmazione e questo significa anche più bug. Per gli scenari in cui non è pratico creare un livello di livello intermedio, i trigger sono la migliore linea d'azione per far rispettare la regola aziendale che non può essere espressa come un vincolo.

Per usare un esempio specifico per Access. Si supponga di voler applicare la logica aziendale durante la modifica dei dati tramite l'applicazione. Forse abbiamo più moduli di immissione dati associati a una stessa tabella, o forse abbiamo bisogno di supportare moduli di immissione dati complessi in cui più tabelle di base devono partecipare alla modifica. Forse il modulo di immissione dei dati deve supportare voci non normalizzate che poi ricomponiamo in dati normalizzati. In tutti questi casi, potremmo semplicemente scrivere codice VBA, ma può essere difficile da mantenere e convalidare per tutti i casi. Triggers ci aiuta a spostare la logica da VBA a T-SQL. La logica aziendale incentrata sui dati generalmente è posizionata meglio vicino ai dati possibile.

Regola n. 3:il trigger deve essere basato su set, non su riga

L'errore di gran lunga più comune commesso con un trigger è di farlo funzionare su righe. Spesso vediamo codice simile a questo:

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

L'omaggio dovrebbe essere il semplice fatto che c'era un SELECT TOP 1 su un tavolo inserito. Funzionerà solo finché inseriamo solo una riga. Ma quando è più di una riga, cosa succede a quelle sfortunate file che sono arrivate 2a e dopo? Possiamo migliorarlo facendo qualcosa di simile a questo:

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

Questo è ora basato su set e quindi molto migliorato, ma ha ancora altri problemi che vedremo nelle prossime regole...

Regola n. 4:usa invece una vista.

A una vista può essere associato un trigger. Questo ci dà il vantaggio di evitare problemi associati ai trigger di una tabella. Saremmo in grado di importare facilmente in blocco dati puliti nella tabella senza dover disabilitare alcun trigger. Inoltre, un trigger in vista lo rende una scelta esplicita di opt-in. Se disponi di funzionalità relative alla sicurezza o regole aziendali che richiedono l'esecuzione di trigger, puoi semplicemente revocare le autorizzazioni sulla tabella direttamente e quindi incanalarle verso la nuova visualizzazione. Ciò garantisce che tu esaminerai il progetto e annoterai dove sono necessari aggiornamenti alla tabella in modo da poterli monitorare per eventuali bug o problemi.

Lo svantaggio è che una vista può avere solo un INSTEAD OF trigger collegato, il che significa che è necessario eseguire esplicitamente le modifiche equivalenti sulla tabella di base all'interno del trigger. Tuttavia, tendo a pensare che sia meglio così perché ti assicura anche di sapere esattamente quale sarà la modifica e quindi ti dà lo stesso livello di controllo che hai normalmente all'interno di una procedura memorizzata.

Regola n. 5:il trigger dovrebbe essere stupido e semplice.

Ricordi il commento sul debug e il test di una procedura memorizzata? Il miglior favore che possiamo fare a noi stessi è mantenere la logica aziendale in una procedura memorizzata e fare in modo che il trigger la invochi invece. Non dovresti mai scrivere la logica aziendale direttamente nel trigger; che sta effettivamente versando cemento sul database. Ora è congelato nella forma e può essere problematico testare adeguatamente la logica. Il tuo cablaggio di test ora deve comportare alcune modifiche alla tabella di base. Questo non va bene per scrivere test semplici e ripetibili. Questo dovrebbe essere il più complicato in quanto dovrebbe essere consentito al tuo trigger:

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 prima parte del trigger consiste sostanzialmente nell'eseguire le modifiche effettive sulla tabella di base perché è un trigger INVECE DI, quindi dobbiamo eseguire tutte le modifiche che saranno diverse a seconda delle tabelle che dobbiamo gestire. Vale la pena sottolineare che le modifiche dovrebbero essere principalmente letterali. Non ricalcoliamo né trasformiamo nessuno dei dati. Salviamo tutto quel lavoro extra alla fine, dove tutto ciò che stiamo facendo all'interno del trigger è compilare un elenco di record che sono stati modificati dal trigger e fornire a una procedura memorizzata utilizzando un parametro con valori di tabella. Nota che non stiamo nemmeno considerando quali record sono stati modificati né come è stato modificato. Tutto ciò che può essere fatto all'interno della stored procedure.

Regola n. 6:l'attivatore dovrebbe essere idempotente quando possibile.

In generale, i trigger DEVONO essere idempotente. Ciò vale indipendentemente dal fatto che si tratti di un trigger basato su tabella o basato su visualizzazione. Si applica in particolare a coloro che devono modificare i dati sulle tabelle di base da cui è in corso il monitoraggio del trigger. Come mai? Perché se gli esseri umani stanno modificando i dati che verranno raccolti dal trigger, potrebbero rendersi conto di aver commesso un errore, di averlo modificato di nuovo o forse semplicemente modificare lo stesso record e salvarlo 3 volte. Non saranno contenti se scoprono che i rapporti cambiano ogni volta che apportano una modifica che non dovrebbe modificare l'output per il rapporto.

Per essere più espliciti, potrebbe essere allettante provare a ottimizzare il trigger facendo qualcosa di simile a questo:

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;

Evitiamo di ricalcolare il nuovo totale semplicemente rivedendo le righe modificate nella tabella inserita, giusto? Ma quando l'utente modifica il record per correggere un errore di battitura nel nome del cliente, cosa accadrà? Finiamo con un totale fasullo e il grilletto ora funziona contro di noi.

A questo punto, dovresti capire perché la regola n. 4 ci aiuta spingendo fuori solo le chiavi primarie nella procedura memorizzata, piuttosto che provare a passare qualsiasi dato nella procedura memorizzata o farlo direttamente all'interno del trigger come avrebbe fatto l'esempio .

Invece, vogliamo avere del codice simile a questo all'interno di una procedura memorizzata:

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;

Utilizzando @SalesOrders, possiamo ancora aggiornare selettivamente solo le righe interessate dal trigger e possiamo anche ricalcolare del tutto il nuovo totale e renderlo il nuovo totale. Quindi, anche se l'utente ha commesso un errore di battitura sul nome del cliente e l'ha modificato, ogni salvataggio produrrà lo stesso risultato per quella riga.

Ancora più importante, questo approccio ci fornisce anche un modo semplice per correggere i totali. Supponiamo di dover eseguire l'importazione in blocco e l'importazione non contiene il totale, quindi dobbiamo calcolarlo noi stessi. Possiamo scrivere la procedura memorizzata per scrivere direttamente nella tabella. Possiamo quindi invocare la procedura memorizzata di cui sopra passando gli ID dall'importazione e siamo tutti a posto. Pertanto, la logica che utilizziamo non è legata al trigger dietro la vista. Questo aiuta quando la logica non è necessaria per l'importazione in blocco che stiamo eseguendo.

Se riscontri problemi a rendere idempotente il tuo trigger, è una forte indicazione che potresti dover utilizzare una stored procedure invece e chiamarla direttamente dalla tua applicazione invece di fare affidamento sui trigger. Una notevole eccezione a questa regola è quando il trigger è inteso principalmente come trigger di controllo. In questo caso, vuoi scrivere una nuova riga nella tabella di controllo per ogni modifica, inclusi tutti gli errori di battitura che l'utente fa. Questo va bene perché in tal caso, non ci sono modifiche ai dati con cui l'utente sta interagendo. Dal punto di vista dell'utente, è sempre lo stesso risultato. Ma ogni volta che il trigger deve manipolare gli stessi dati con cui l'utente sta lavorando, è molto meglio quando è idempotente.

Concludendo

Si spera che ormai tu possa vedere quanto può essere più difficile progettare un trigger ben educato. Per questo motivo, dovresti considerare attentamente se puoi evitarlo del tutto e utilizzare chiamate dirette con stored procedure. Ma se hai concluso che devi disporre di trigger per gestire le modifiche apportate tramite visualizzazioni, spero che le regole ti aiutino. Rendere il trigger basato sul set è abbastanza facile con alcune regolazioni. Renderlo idempotente di solito richiede più riflessioni su come implementerai le tue procedure archiviate.

Se hai altri suggerimenti o regole da condividere, fai fuoco nei commenti!