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

Design af en Microsoft T-SQL Trigger

Design af en Microsoft T-SQL Trigger

Ved lejligheder, når vi bygger et projekt, der involverer en Access-frontend og en SQL Server-backend, er vi stødt på dette spørgsmål. Skal vi bruge en trigger til noget? At designe en SQL Server-trigger for Access-applikation kan være en løsning, men kun efter nøje overvejelser. Nogle gange bliver dette foreslået som en måde at holde forretningslogikken i databasen snarere end applikationen. Normalt kan jeg godt lide at have forretningslogikken defineret så tæt på databasen som muligt. Så er trigger den løsning, vi ønsker til vores Access-frontend?

Jeg har fundet ud af, at kodning af en SQL-trigger kræver yderligere overvejelser, og hvis vi ikke er forsigtige, kan vi ende med et større rod, end vi startede. Artiklen har til formål at dække alle de faldgruber og teknikker, vi kan bruge til at sikre, at når vi bygger en database med triggere, vil de arbejde til vores fordel, snarere end blot at tilføje kompleksitet for kompleksitetens skyld.

Lad os overveje reglerne...

Regel #1:Brug ikke en trigger!

Helt seriøst. Hvis du først griber udløseren om morgenen, så kommer du til at fortryde det om natten. Det største problem med triggere generelt er, at de effektivt kan sløre din forretningslogik og forstyrre processer, der ikke burde have brug for en trigger. Jeg har set nogle forslag til at slå triggere fra, når du laver en massebelastning eller noget lignende. Jeg påstår, at dette er en stor kode lugt. Du bør ikke bruge en trigger, hvis den skal være betinget tændt eller slukket.

Som standard bør vi først skrive lagrede procedurer eller visninger. For de fleste scenarier vil de klare opgaven fint. Lad os ikke tilføje magi her.

Så hvorfor artiklen om trigger så?

Fordi triggere har deres anvendelser. Vi skal erkende, hvornår vi skal bruge triggere. Vi skal også skrive dem på en måde, så det hjælper os mere end at såre os.

Regel #2:Har jeg virkelig brug for en trigger?

I teorien lyder triggere godt. De giver os en begivenhedsbaseret model til at håndtere ændringer, så snart de bliver ændret. Men hvis alt hvad du behøver er at validere nogle data eller sikre, at nogle skjulte kolonner eller logningstabeller er udfyldt... Jeg tror, ​​du vil opdage, at en lagret procedure gør arbejdet mere effektivt og fjerner det magiske aspekt. Ydermere er det nemt at teste at skrive en lagret procedure; Du skal blot opsætte nogle mock-data og køre den lagrede procedure, kontrollere, at resultaterne er, hvad du forventede. Jeg håber, du bruger en testramme som tSQLt.

Og det er vigtigt at bemærke, at det normalt er mere effektivt at bruge databasebegrænsninger end en trigger. Så hvis du bare skal validere, at en værdi er gyldig i en anden tabel, skal du bruge en fremmednøgle-begrænsning. At validere, at en værdi er inden for et bestemt område, kræver en kontrolbegrænsning. Disse bør være dit standardvalg for den slags valideringer.

Så hvornår har vi egentlig brug for en trigger?

Det bunder i tilfælde, hvor du virkelig ønsker, at forretningslogikken skal være i SQL-laget. Måske fordi du har flere klienter på forskellige programmeringssprog, der laver indsættelser/opdateringer til en tabel. Det ville være meget rodet at duplikere forretningslogikken på tværs af hver klient i deres respektive programmeringssprog, og det betyder også flere fejl. I scenarier, hvor det ikke er praktisk at oprette et mellemlag, er triggere din bedste fremgangsmåde til at håndhæve forretningsreglen, der ikke kan udtrykkes som en begrænsning.

For at bruge et eksempel specifikt for Access. Antag, at vi ønsker at håndhæve forretningslogik, når vi ændrer data via applikationen. Måske har vi flere dataindtastningsformularer bundet til en samme tabel, eller måske skal vi understøtte komplekse dataindtastningsformularer, hvor flere basistabeller skal deltage i redigeringen. Måske skal dataindtastningsformularen understøtte ikke-normaliserede indtastninger, som vi derefter omkomponerer til normaliserede data. I alle disse tilfælde kunne vi bare skrive VBA-kode, men det kan være svært at vedligeholde og validere for alle tilfælde. Triggers hjælper os med at flytte logikken ud af VBA og ind i T-SQL. Den datacentrerede forretningslogik er generelt bedst placeret tæt på dataene som muligt.

Regel #3:Trigger skal være sæt-baseret, ikke række-baseret

Langt den mest almindelige fejl med en trigger er at få den til at køre på rækker. Ofte ser vi kode, der ligner denne:

--Dårlig kode! Brug ikke!CREATE TRIGGER dbo.SomeTriggerON dbo.SomeTable EFTER INSERTASBEGIN DECLARE @NewTotal money; DECLARE @NewID int; VÆLG TOP 1 @NewID =SalesOrderID, @NewTotal =SalesAmount FROM indsat; OPDATERING dbo.SalesOrder SET OrderTotal =OrderTotal + @NewTotal WHERE SalesOrderID =@SalesOrderIDEND;

Giveawayen skulle være det faktum, at der var en VÆLG TOP 1 på et bord indsat. Dette vil kun virke, så længe vi kun indsætter en række. Men når det er mere end én række, hvad sker der så med de uheldige rækker, der kom på andenpladsen og efter? Vi kan forbedre det ved at gøre noget lignende dette:

--Stadig dårlig kode! Brug ikke!CREATE TRIGGER dbo.SomeTriggerON dbo.SomeTable EFTER INSERTASBEGIN FLÉT INTO dbo.SalesOrder AS s BRUG indsat AS i ON s.SalesOrderID =i.SalesOrderID NÅR MATCHED SÅ OPDATERES SET OrderTotal = 

Dette er nu sæt-baseret og dermed meget forbedret, men dette har stadig andre problemer, som vi vil se i de næste par regler...

Regel #4:Brug en visning i stedet.

En visning kan have en trigger knyttet til sig. Dette giver os fordelen ved at undgå problemer forbundet med en tabeludløser. Vi ville nemt kunne masseimportere rene data til tabellen uden at skulle deaktivere nogen triggere. Ydermere gør en trigger på visning det til et eksplicit opt-in valg. Hvis du har sikkerhedsrelaterede funktioner eller forretningsregler, der nødvendiggør kørsel af triggere, kan du blot tilbagekalde tilladelserne på bordet direkte og dermed kanalisere dem til den nye visning i stedet. Det sikrer, at du vil gennemgå projektet og notere, hvor opdateringer til tabellen er nødvendige, så du derefter kan spore dem for eventuelle fejl eller problemer.

Ulempen er, at en visning kun kan have en I STEDET FOR triggere tilknyttet, hvilket betyder, at du eksplicit skal udføre de tilsvarende ændringer på basisbordet selv inden for triggeren. Jeg har dog en tendens til at tro, at det er bedre på den måde, fordi det også sikrer, at du ved præcis, hvad ændringen vil være, og dermed giver dig samme niveau af kontrol, som du normalt har inden for en lagret procedure.

Regel #5:Udløseren skal være dum enkel.

Kan du huske kommentaren om fejlretning og test af en lagret procedure? Den bedste tjeneste, vi kan gøre mod os selv, er at beholde forretningslogikken i en lagret procedure og få triggeren til at påkalde den i stedet. Du bør aldrig skrive forretningslogik direkte ind i udløseren; der effektivt hælder beton på databasen. Det er nu frosset til formen, og det kan være problematisk at teste logikken tilstrækkeligt. Din testsele skal nu involvere nogle ændringer af basistabellen. Dette er ikke godt til at skrive simple og gentagelige tests. Dette burde være det mest komplicerede, da din trigger bør have lov til at være:

OPRET TRIGGER [dbo].[SomeTrigger]PÅ [dbo].[SomeView] I STEDET FOR INSERT, UPDATE, DELETEASBEGIN DECLARE @SomeIDs AS SomeIDTableType --Udfør fletningen til basistabellen FLÉT INTO dbo.SomeTable AS t USING inser AS i ON t.SomeID =i.SomeID NÅR MATCHED SÅ OPDATERES SET t.SomeStuff =i.SomeStuff, t.OtherStuff =i.OtherStuff NÅR IKKE MATCHED SÅ INDSÆT ( SomeStuff, OtherStuff ) VÆRDIER (i.SomeStuff, i.OtherStuff, i. OUTPUT indsat.SomeID INTO @SomeIDs(SomeID); SLET FRA dbo.SomeTable OUTPUT slettet.SomeID INTO @SomeIDs(SomeID) WHERE EXISTS ( SELECT NULL FROM deleted AS d WHERE d.SomeID =SomeTable.SomeID ) AND NOT EXISTS ( SELECT NULL FROM indsat AS I WHEREETable. SomeID ); EXEC dbo.uspUpdateSomeStuff @SomeIDs;END;

Den første del af triggeren er grundlæggende at udføre de faktiske ændringer på basisbordet, fordi det er en I STEDET FOR trigger, så vi skal udføre alle de ændringer, som vil være forskellige afhængigt af de tabeller, vi skal administrere. Det er værd at understrege, at ændringer hovedsagelig skal være ordret. Vi genberegner eller transformerer ikke nogen af ​​dataene. Vi gemmer alt det ekstra arbejde til sidst, hvor alt, hvad vi gør inden for triggeren, er at udfylde en liste over poster, der blev ændret af triggeren, og levere til en lagret procedure ved hjælp af en tabelværdiparameter. Bemærk, at vi ikke engang overvejer, hvilke poster der blev ændret, eller hvordan de blev ændret. Alt det kan gøres inden for den lagrede procedure.

Regel #6:Udløseren skal være idempotent, når det er muligt.

Generelt udløserne være idempotent. Dette gælder uanset om det er en tabelbaseret eller en visningsbaseret trigger. Det gælder især dem, der skal ændre dataene på basistabellerne, hvorfra triggeren overvåger. Hvorfor? For hvis mennesker ændrer de data, der vil blive opfanget af udløseren, kan de indse, at de har lavet en fejl, redigeret det igen eller måske blot redigeret den samme post og gemme det 3 gange. De vil ikke være glade, hvis de opdager, at rapporterne ændres, hver gang de foretager en redigering, der ikke skal ændre outputtet for rapporten.

For at være mere eksplicit kan det være fristende at prøve at optimere udløseren ved at gøre noget lignende dette:

MED SourceData AS ( SELECT OrderID, SUM(SalesAmount) AS NewSaleTotal FROM inserted GROUP BY OrderID)FLETT INTO dbo.SalesOrder AS VED BRUG AF SourceData AS dON o.OrderID =d.OrderIDNÅR MATCHED SÅ OPDATERES SET o.OrderTotal =o.OrderTotal =o.OrderTotal + d.NewSaleTotal;

Vi slipper for at genberegne den nye total ved blot at gennemgå de ændrede rækker i den indsatte tabel, ikke? Men når brugeren redigerer posten for at rette en tastefejl i kundens navn, hvad sker der så? Vi ender med en falsk total, og udløseren arbejder nu imod os.

Nu burde du se, hvorfor reglen #4 hjælper os ved kun at skubbe de primære nøgler ud til den lagrede procedure, i stedet for at forsøge at overføre data til den lagrede procedure eller gøre det direkte inde i triggeren, som prøven ville have gjort .

I stedet ønsker vi at have en kode, der ligner denne i en lagret procedure:

OPRET PROCEDURE dbo.uspUpdateSalesTotal ( @SalesOrders SalesOrderTableType LÆSEKUN) SOM BEGYNDER MED SourceData AS (VÆLG s.OrderID, SUM(s.SalesAmount) SOM NewSaleTotal FRA dbo.SalesOrder SOM s F @SaSELECT EXISTSOR X .SalesOrderID =s.SalesOrderID ) GRUPPER EFTER OrderID ) FLOT TIL dbo.SalesOrder AS o BRUG SourceData AS d ON o.OrderID =d.OrderID NÅR MATCHED SÅ OPDATERES SET o.OrderTotal =d.NewSaleTotal;END;

Ved at bruge @SalesOrders kan vi stadig kun selektivt opdatere de rækker, der blev påvirket af triggeren, og vi kan også genberegne den nye total og gøre den til den nye total. Så selvom brugeren lavede en tastefejl på kundenavnet og redigerede det, vil hver lagring give det samme resultat for den række.

Endnu vigtigere, denne tilgang giver os også en nem måde at rette op på totalerne. Antag, at vi skal lave masseimport, og importen ikke indeholder totalen, så vi må selv beregne det. Vi kan skrive den lagrede procedure for at skrive direkte til tabellen. Vi kan derefter påberåbe os ovenstående lagrede procedure ved at sende ID'erne fra importen, og vi er alle i orden. Den logik, vi bruger, er således ikke bundet op på aftrækkeren bag visningen. Det hjælper, når logikken er unødvendig for den masseimport, vi udfører.

Hvis du oplever, at du har problemer med at gøre din trigger idempotent, er det en stærk indikation af, at du måske skal bruge en lagret procedure i stedet og kalde den direkte fra din applikation i stedet for at stole på triggere. En bemærkelsesværdig undtagelse fra denne regel er, når udløseren primært er beregnet til at være en revisionsudløser. I dette tilfælde vil du skrive en ny række til revisionstabellen for hver redigering, inklusive alle de tastefejl, som brugeren laver. Dette er OK, fordi der i så fald ikke er nogen ændringer i de data, som brugeren interagerer med. Fra brugerens POV er det stadig det samme resultat. Men når triggeren skal manipulere de samme data, som brugeren arbejder med, er det meget bedre, når det er idempotent.

Afslutning

Forhåbentlig kan du nu se, hvor meget sværere det kan være at designe en velopdragen trigger. Af den grund bør du nøje overveje, om du kan undgå det helt og bruge direkte påkald med lagret procedure. Men hvis du har konkluderet, at du skal have triggere til at administrere ændringerne, der er foretaget via visninger, håber jeg, at reglerne vil hjælpe dig. At gøre triggersættet-baseret er nemt nok med nogle justeringer. At gøre det idempotent kræver normalt flere tanker om, hvordan du vil implementere dine lagrede procedurer.

Hvis du har flere forslag eller regler at dele, så skyd løs i kommentarerne!


  1. Sammenligning af SQL, forespørgselsbyggere og ORM'er

  2. Låsning og ydeevne

  3. FORALL-erklæring med nedre og øvre grænse i Oracle-databasen

  4. Hvad jeg gerne vil se i Amazon EC2 for Database Management