Designa en Microsoft T-SQL-utlösare
Vid tillfällen när vi bygger ett projekt som involverar en Access-gränssnitt och en SQL Server-backend, har vi stött på den här frågan. Ska vi använda en trigger för något? Att designa en SQL Server-utlösare för Access-applikation kan vara en lösning men bara efter noggranna överväganden. Ibland föreslås detta som ett sätt att behålla affärslogiken i databasen, snarare än applikationen. Normalt sett gillar jag att ha affärslogiken definierad så nära databasen som möjligt. Så, är trigger den lösning vi vill ha för vårt Access-gränssnitt?
Jag har upptäckt att kodning av en SQL-utlösare kräver ytterligare överväganden och om vi inte är försiktiga kan vi sluta med en större röra än vi började. Artikeln syftar till att täcka alla fallgropar och tekniker vi kan använda för att säkerställa att när vi bygger en databas med utlösare kommer de att fungera till vår fördel, snarare än att bara lägga till komplexitet för komplexitetens skull.
Låt oss överväga reglerna...
Regel #1:Använd inte en utlösare!
Allvarligt. Om du först når utlösaren på morgonen kommer du att ångra dig på natten. Det största problemet med triggers i allmänhet är att de effektivt kan fördunkla din affärslogik och störa processer som inte borde behöva en trigger. Jag har sett några förslag för att stänga av triggers när du gör en bulkbelastning eller något liknande. Jag hävdar att detta är en stor kodlukt. Du bör inte använda en utlösare om den måste slås på eller av villkorligt.
Som standard bör vi först skriva lagrade procedurer eller vyer. För de flesta scenarier kommer de att göra jobbet bra. Låt oss inte lägga till magi här.
Så varför artikeln om trigger då?
Eftersom triggers har sina användningsområden. Vi måste känna igen när vi ska använda triggers. Vi måste också skriva dem på ett sätt så att det hjälper oss mer än att skada oss.
Regel #2:Behöver jag verkligen en utlösare?
I teorin låter triggers bra. De förser oss med en händelsebaserad modell för att hantera ändringar så snart de ändras. Men om allt du behöver är att validera vissa data, eller se till att några dolda kolumner eller loggningstabeller är ifyllda... Jag tror att du kommer att upptäcka att en lagrad procedur gör jobbet mer effektivt och tar bort den magiska aspekten. Dessutom är det lätt att testa att skriva en lagrad procedur; ställ helt enkelt in lite skendata och kör den lagrade proceduren, kontrollera att resultatet är vad du förväntade dig. Jag hoppas att du använder ett testramverk som tSQLt.
Och det är viktigt att notera att det vanligtvis är mer effektivt att använda databasbegränsningar än en utlösare. Så om du bara behöver validera att ett värde är giltigt i en annan tabell, använd en främmande nyckel-begränsning. Att validera att ett värde ligger inom ett visst intervall kräver en kontrollbegränsning. De bör vara ditt standardval för den typen av valideringar.
Så när kommer vi att behöva en trigger?
Det kokar ner till fall där du verkligen vill att affärslogiken ska finnas i SQL-lagret. Kanske för att du har flera klienter på olika programmeringsspråk som gör infogningar/uppdateringar till en tabell. Det skulle vara väldigt rörigt att duplicera affärslogiken över varje klient i deras respektive programmeringsspråk och detta innebär också fler buggar. För scenarier där det inte är praktiskt att skapa ett mellanskikt, är triggers din bästa åtgärd för att upprätthålla affärsregeln som inte kan uttryckas som en begränsning.
För att använda ett exempel specifikt för Access. Anta att vi vill upprätthålla affärslogik när vi modifierar data via applikationen. Kanske har vi flera datainmatningsformulär bundna till en och samma tabell, eller så kanske vi behöver stödja komplexa datainmatningsformulär där flera bastabeller måste delta i redigeringen. Kanske behöver datainmatningsformuläret stödja icke-normaliserade poster som vi sedan återkomponerar till normaliserade data. I alla dessa fall kunde vi bara skriva VBA-kod men det kan vara svårt att underhålla och validera för alla fall. Triggers hjälper oss att flytta ut logiken från VBA och in i T-SQL. Den datacentrerade affärslogiken är i allmänhet bäst placerad så nära data som möjligt.
Regel #3:Utlösaren måste vara uppsättningsbaserad, inte radbaserad
Det absolut vanligaste misstaget som görs med en trigger är att få den att köras på rader. Ofta ser vi kod som liknar denna:
--Dålig kod! Använd inte!CREATE TRIGGER dbo.SomeTriggerON dbo.SomeTable EFTER INSERTAS BÖRJA DECLARERA @NewTotal money; DECLARE @NewID int; VÄLJ TOP 1 @NewID =SalesOrderID, @NewTotal =SalesAmount FROM infogat; UPPDATERA dbo.SalesOrder SET OrderTotal =OrderTotal + @NewTotal WHERE SalesOrderID =@SalesOrderIDEND;
Giveaway bör vara det faktum att det fanns en SELECT TOP 1 från ett bord insatt. Detta fungerar bara så länge vi bara infogar en rad. Men när det är mer än en rad, vad händer då med de olyckliga raderna som kom tvåa och efter? Vi kan förbättra det genom att göra något liknande detta:
--Fortfarande dålig kod! Använd inte!CREATE TRIGGER dbo.SomeTriggerON dbo.SomeTable EFTER INSERTASBEGIN MERGE INTO dbo.SalesOrder AS s ANVÄNDA insatt AS i ON s.SalesOrderID =i.SalesOrderID NÄR MATCHED DÅ UPPDATERAS SET OrderTotal =Detta är nu set-baserat och därmed mycket förbättrat men detta har fortfarande andra problem som vi kommer att se i de kommande reglerna...
Regel #4:Använd en vy istället.
En vy kan ha en trigger kopplad till sig. Detta ger oss fördelen att undvika problem i samband med en tabellutlösare. Vi skulle enkelt kunna massimportera ren data till tabellen utan att behöva inaktivera några triggers. En trigger som visas gör det dessutom till ett explicit val. Om du har säkerhetsrelaterade funktioner eller affärsregler som kräver körning av utlösare, kan du helt enkelt återkalla behörigheterna på bordet direkt och på så sätt kanalisera dem mot den nya vyn istället. Det säkerställer att du går igenom projektet och noterar var uppdateringar av tabellen behövs så att du sedan kan spåra dem för eventuella buggar eller problem.
Nackdelen är att en vy bara kan ha en ISTADEN FÖR triggers kopplade, vilket innebär att du uttryckligen måste utföra motsvarande modifieringar på basbordet själv inom triggern. Jag tenderar dock att tro att det är bättre på det sättet eftersom det också säkerställer att du vet exakt vad ändringen kommer att vara, och därmed ger dig samma nivå av kontroll som du normalt har inom en lagrad procedur.
Regel #5:Utlösaren ska vara dum enkel.
Kommer du ihåg kommentaren om att felsöka och testa en lagrad procedur? Den bästa tjänsten vi kan göra mot oss själva är att behålla affärslogiken i en lagrad procedur och låta triggern anropa den istället. Du bör aldrig skriva affärslogik direkt i avtryckaren; som effektivt häller betong på databasen. Den är nu frusen till formen och det kan vara problematiskt att testa logiken på ett adekvat sätt. Din testsele måste nu innebära någon modifiering av bastabellen. Detta är inte bra för att skriva enkla och repeterbara tester. Detta borde vara det mest komplicerade eftersom din trigger bör tillåtas vara:
SKAPA TRIGGER [dbo].[SomeTrigger]PÅ [dbo].[SomeView] I STÄLLET FÖR INFOGA, UPPDATERA, DELETEAS BÖRJA DEKLARERA @SomeIDs AS SomeIDTableType --Utför sammanfogningen till bastabellen MERGE INTO dbo.SomeTable AS t USING inser SOM i ON t.SomeID =i.SomeID NÄR MATCHED DÅ UPPDATERAS STÄLL IN t.SomeStuff =i.SomeStuff, t.OtherStuff =i.OtherStuff NÄR INTE MATCHED INFOGA ( SomeStuff, OtherStuff ) VÄRDEN ( i.SomeStuff, i.OtherStuff, i. 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 WHEREETable. SomeID ); EXEC dbo.uspUpdateSomeStuff @SomeIDs;END;Den första delen av triggern är att i princip utföra de faktiska ändringarna på basbordet eftersom det är en ISTÄLLET FÖR trigger, så vi måste utföra alla ändringar som kommer att vara olika beroende på tabellerna vi behöver hantera. Det är värt att betona att ändringar huvudsakligen bör vara ordagrant. Vi räknar inte om eller transformerar någon av datan. Vi sparar allt det extra arbetet i slutet, där allt vi gör inom triggern är att fylla i en lista med poster som modifierades av triggern och tillhandahålla en lagrad procedur med en tabellvärderad parameter. Observera att vi inte ens överväger vilka poster som ändrades eller hur de ändrades. Allt som kan göras inom den lagrade proceduren.
Regel #6:Utlösaren ska vara idempotent när det är möjligt.
Generellt sett MÅSTE utlösarna vara idempotent. Detta gäller oavsett om det är en tabellbaserad eller en vybaserad utlösare. Det gäller särskilt de som behöver modifiera data i bastabellerna varifrån triggern övervakar. Varför? För om människor ändrar data som kommer att fångas upp av utlösaren, kan de inse att de gjorde ett misstag, redigerade det igen eller kanske helt enkelt redigerar samma post och sparar det 3 gånger. De kommer inte att bli glada om de upptäcker att rapporterna ändras varje gång de gör en redigering som inte är tänkt att ändra resultatet för rapporten.
För att vara mer tydlig kan det vara frestande att försöka optimera triggern genom att göra något liknande detta:
WITH SourceData AS (VÄLJ OrderID, SUM(SalesAmount) AS NewSaleTotal FROM inserted GROUP BY OrderID)MERGE INTO dbo.SalesOrder AS AUS SourceData AS dON o.OrderID =d.OrderIDNÄR MATCHED DÅ UPPDATERAS SET o.OrderTotal =o.OrderTotal =o.OrderTotal + d.NewSaleTotal;Vi får undvika att räkna om den nya summan genom att bara granska de modifierade raderna i den infogade tabellen, eller hur? Men vad händer när användaren redigerar posten för att rätta till ett stavfel i kundens namn? Vi slutar med en falsk summa, och utlösaren jobbar nu mot oss.
Vid det här laget borde du se varför regel #4 hjälper oss genom att bara trycka ut de primära nycklarna till den lagrade proceduren, snarare än att försöka skicka någon data till den lagrade proceduren eller göra det direkt inuti triggern som provet skulle ha gjort .
Istället vill vi ha någon kod som liknar denna inom en lagrad procedur:
SKAPA PROCEDUR dbo.uspUpdateSalesTotal ( @SalesOrders SalesOrderTableType READONLY) ASBÖRJAR MED SourceData AS (VÄLJ s.OrderID, SUM(s.SalesAmount) SOM NewSaleTotal FROM dbo.SalesOrder SOM s F @SaSELECTNULLS EXISTERS AS (WHERELESNULLS AS) .SalesOrderID =s.SalesOrderID ) GRUPPER EFTER OrderID ) SLUT INTO dbo.SalesOrder AS o ANVÄNDER SourceData AS d ON o.OrderID =d.OrderID NÄR MATCHED UPDATERAS SET o.OrderTotal =d.NewSaleTotal;END;Med hjälp av @SalesOrders kan vi fortfarande selektivt uppdatera endast de rader som påverkades av utlösaren, och vi kan också räkna om den nya summan helt och hållet och göra den till den nya summan. Så även om användaren gjorde ett stavfel i kundnamnet och redigerade det, kommer varje lagring att ge samma resultat för den raden.
Ännu viktigare, detta tillvägagångssätt ger oss också ett enkelt sätt att fixa summan. Anta att vi måste göra massimport och importen inte innehåller summan så vi måste beräkna det själva. Vi kan skriva den lagrade proceduren för att skriva till tabellen direkt. Vi kan sedan anropa den lagrade proceduren ovan och skicka in ID:n från importen, och vi är alla bra. Således är logiken vi använder inte bunden i avtryckaren bakom vyn. Det hjälper när logiken är onödig för den massimport vi utför.
Om du upptäcker att du har problem med att göra din trigger idempotent, är det en stark indikation på att du kan behöva använda en lagrad procedur istället och anropa den direkt från din applikation istället för att förlita dig på triggers. Ett anmärkningsvärt undantag från denna regel är när triggern i första hand är avsedd att vara en revisionstrigger. I det här fallet vill du skriva en ny rad till granskningstabellen för varje redigering, inklusive alla stavfel som användaren gör. Detta är OK eftersom det i så fall inte sker några förändringar av data som användaren interagerar med. Från användarens POV är det fortfarande samma resultat. Men när utlösaren behöver manipulera samma data som användaren arbetar med, är det mycket bättre när det är idempotent.
Avsluta
Förhoppningsvis kan du vid det här laget se hur mycket svårare det kan vara att designa en väluppfostrad trigger. Av den anledningen bör du noga överväga om du kan undvika det helt och hållet och använda direkta anrop med lagrad procedur. Men om du har kommit fram till att du måste ha utlösare för att hantera ändringarna som görs via vyer, hoppas jag att reglerna kommer att hjälpa dig. Att göra triggeruppsättningen baserad är tillräckligt enkelt med några justeringar. Att göra det idempotent kräver vanligtvis mer tankar om hur du ska implementera dina lagrade procedurer.
Om du har några fler förslag eller regler att dela, skjut iväg i kommentarerna!