Проектиране на Microsoft T-SQL Trigger
В случаите, когато създаваме проект, включващ преден край на Access и бекенд на SQL Server, се сблъскваме с този въпрос. Трябва ли да използваме спусък за нещо? Проектирането на тригер на SQL Server за приложението Access може да бъде решение, но само след внимателни съображения. Понякога това се предлага като начин за запазване на бизнес логиката в базата данни, а не в приложението. Обикновено ми харесва бизнес логиката да е дефинирана възможно най-близо до базата данни. И така, trigger ли е решението, което искаме за нашия Access front-end?
Открих, че кодирането на SQL тригер изисква допълнителни съображения и ако не сме внимателни, можем да свършим с по-голяма бъркотия, отколкото започнахме. Статията има за цел да покрие всички клопки и техники, които можем да използваме, за да гарантираме, че когато изграждаме база данни с тригери, те ще работят в наша полза, вместо просто да добавят сложност в името на сложността.
Нека разгледаме правилата...
Правило №1:Не използвайте задействане!
Сериозно. Ако посегнете към спусъка първо сутрин, тогава ще съжалявате за това през нощта. Най-големият проблем със задействанията като цяло е, че те могат ефективно да замъглят вашата бизнес логика и да пречат на процеси, които не би трябвало да се нуждаят от тригер. Видях някои предложения за изключване на тригери, когато извършвате насипно натоварване или нещо подобно. Твърдя, че това е голяма миризма на код. Не трябва да използвате тригер, ако трябва да бъде условно включен или изключен.
По подразбиране първо трябва да пишем съхранени процедури или изгледи. За повечето сценарии те ще свършат добре работата. Нека не добавяме магия тук.
Защо тогава статията за тригера?
Защото тригерите имат своите приложения. Трябва да разпознаем кога трябва да използваме тригери. Също така трябва да ги напишем по начин, който да ни помогне повече, отколкото да ни нарани.
Правило №2:Наистина ли имам нужда от спусък?
На теория тригерите звучат добре. Те ни предоставят базиран на събития модел за управление на промените веднага щом бъдат променени. Но ако всичко, от което се нуждаете, е да потвърдите някои данни или да се уверите, че някои скрити колони или таблици за регистриране са попълнени... Мисля, че ще откриете, че съхранената процедура върши работата по-ефективно и премахва магическия аспект. Освен това писането на съхранена процедура е лесно за тестване; просто настройте някои фалшиви данни и изпълнете съхранената процедура, проверете дали резултатите са това, което очаквате. Надявам се, че използвате рамка за тестване като tSQLt.
И е важно да се отбележи, че обикновено е по-ефективно да се използват ограничения на базата данни, отколкото тригер. Така че, ако просто трябва да потвърдите, че дадена стойност е валидна в друга таблица, използвайте ограничение за външен ключ. Потвърждаването, че дадена стойност е в определен диапазон, изисква ограничение за проверка. Това трябва да бъде вашият избор по подразбиране за този вид валидации.
И така, кога всъщност ще имаме нужда от спусък?
Това се свежда до случаи, когато наистина искате бизнес логиката да бъде в слоя SQL. Може би защото имате множество клиенти на различни езици за програмиране, които правят вмъквания/актуализации на таблица. Би било много объркано да се дублира бизнес логиката във всеки клиент на съответния език за програмиране и това също означава повече грешки. За сценарии, при които не е практично да се създаде слой от средно ниво, тригерите са най-добрият ви начин на действие за прилагане на бизнес правилото, което не може да бъде изразено като ограничение.
За да използвате пример, специфичен за Access. Да предположим, че искаме да наложим бизнес логиката, когато променяме данни чрез приложението. Може би имаме множество формуляри за въвеждане на данни, свързани с една и съща таблица, или може би трябва да поддържаме сложна форма за въвеждане на данни, където множество основни таблици трябва да участват в редактирането. Може би формулярът за въвеждане на данни трябва да поддържа ненормализирани записи, които след това прекомпонираме в нормализирани данни. Във всички тези случаи бихме могли просто да напишем VBA код, но това може да бъде трудно за поддържане и валидиране за всички случаи. Triggers ни помага да преместим логиката извън VBA в T-SQL. По принцип бизнес логиката, ориентирана към данните, е най-добре поставена възможно най-близо до данните.
Правило №3:Тригерът трябва да е базиран на набор, а не на ред
Досега най-честата грешка, допусната със спусъка, е да го накарате да работи на редове. Често виждаме код, подобен на този:
--Лош код! Не използвайте!CREATE TRIGGER dbo.SomeTriggerON dbo.SomeTable СЛЕД INSERTASBEGIN DECLARE @NewTotal money; ДЕКЛАРИРАНЕ @NewID int; ИЗБЕРЕТЕ ТОП 1 @NewID =SalesOrderID, @NewTotal =SalesAmount FROM е вмъкнат; АКТУАЛИЗИРАНЕ dbo.SalesOrder SET OrderTotal =OrderTotal + @NewTotal WHERE SalesOrderID =@SalesOrderIDEND;
Раздаването трябва да бъде само фактът, че имаше SELECT TOP 1 от маса вмъкнат. Това ще работи само докато вмъкнем само един ред. Но когато има повече от един ред, тогава какво се случва с онези нещастни редове, които дойдоха 2-ри и след това? Можем да подобрим това, като направим нещо подобно на това:
--Все още лош код! Не използвайте!CREATE TRIGGER dbo.SomeTriggerON dbo.SomeTable СЛЕД ВКЛЮЧВАНЕТО ЗАПОЧВА СЛИВАНЕ В dbo.SalesOrder AS s ИЗПОЛЗВАНЕ вмъкнато КАТО i ON s.SalesOrderID =i.SalesOrderID, КОГАТО СЕ СЪВПАДАТ ПОСЛЕ АКТУАЛИЗИРАНЕ НА ЗАДАВАНЕ НА ЗАДАЧИ ЗА ЗАДАЧИ +NewTotal;>Това вече е базирано на набори и следователно много подобрено, но това все още има други проблеми, които ще видим в следващите няколко правила...
Правило №4:Вместо това използвайте изглед.
Изгледът може да има прикачен тригер към него. Това ни дава предимството да избягваме проблеми, свързани със задействания на таблица. Бихме могли лесно да импортираме насипно чисти данни в таблицата, без да се налага да деактивираме никакви задействания. Освен това, задействането на изглед го прави изричен избор за избор. Ако имате свързани със сигурността функционалности или бизнес правила, които налагат стартирането на тригери, можете просто да отмените разрешенията на таблицата директно и по този начин да ги насочите към новия изглед вместо това. Това гарантира, че ще преминете през проекта и ще отбележите къде са необходими актуализации на таблицата, за да можете след това да ги проследите за евентуални грешки или проблеми.
Недостатъкът е, че изгледът може да има само прикачени тригери ВМЕСТО, което означава, че трябва изрично да извършите еквивалентните модификации на основната таблица сами в рамките на тригера. Склонен съм обаче да мисля, че така е по-добре, защото също така гарантира, че знаете точно каква ще бъде модификацията и по този начин ви дава същото ниво на контрол, което обикновено имате в рамките на съхранена процедура.
Правило №5:Спусъкът трябва да е тъпо прост.
Помните ли коментара за отстраняване на грешки и тестване на съхранена процедура? Най-добрата услуга, която можем да направим на себе си, е да запазим бизнес логиката в съхранена процедура и вместо това да накараме тригера да я извика. Никога не трябва да пишете бизнес логика директно в тригера; това ефективно излива бетон върху базата данни. Сега е замръзнал във формата и може да бъде проблематично да се тества адекватно логиката. Вашият тестов колан сега трябва да включва известна модификация на основната маса. Това не е добре за писане на прости и повтарящи се тестове. Това трябва да е най-сложното, тъй като задействането ви трябва да бъде:
СЪЗДАВАНЕ НА TRIGGER [dbo].[SomeTrigger]ON [dbo].[SomeView] ВМЕСТО ДА ВМЕСВАТЕ, АКТУАЛИЗИРАТЕ, ИЗТРИВАТЕ ЗАПОЧВАНЕТЕ ДЕКЛАРИРАНЕ @SomeIDs КАТО SomeIDTableType --Извършете сливането в основната таблица MERGE INTO dbo.USING вмъкнато като t.SomeTable. КАТО i ON t.SomeID =i.SomeID, КОГАТО СЕ СЪВПАДА, ТОГА АКТУАЛИЗИРА ЗАДАДЕНО t.SomeStuff =i.SomeStuff, t.OtherStuff =i.OtherStuff, КОГАТО НЕ СЪВПАДАТ, ТОГАВА ВМЕСТЕ (SometherStuff, OtherStuff) i.SomeStuff (SomeStuff) (SomeStuff) ИЗХОД е вмъкнат.SomeID INTO @SomeIDs(SomeID); ИЗТРИВАНЕ ОТ dbo.SomeTable ИЗХОД е изтрит.SomeID В @SomeIDs(SomeID) КЪДЕ СЪЩЕСТВУВА ( ИЗБЕРЕТЕ NULL ОТ изтрито КАТО d WHERE d.SomeID =SomeTable.SomeID ) И НЕ СЪЩЕСТВУВА (ИЗБЕРЕТЕ КАТО NULL ОТ ИЗБИРАТЕ. Някой е вмъкнат. SomeID ); EXEC dbo.uspUpdateSomeStuff @SomeIDs;END;Първата част от тригера е основно да извърши действителните модификации на основната таблица, защото това е ВМЕСТО тригер, така че трябва да извършим всички модификации, които ще бъдат различни в зависимост от таблиците, които трябва да управляваме. Струва си да се подчертае, че модификациите трябва да бъдат предимно дословни. Ние не преизчисляваме и не трансформираме нито една от данните. Спестяваме цялата тази допълнителна работа в края, където всичко, което правим в рамките на тригера, е да попълваме списък със записи, които са били променени от тригера, и да предоставяме на съхранена процедура с помощта на параметър с таблица. Имайте предвид, че ние дори не обмисляме какви записи са били променени, нито как са били променени. Всичко това може да се направи в рамките на съхранената процедура.
Правило №6:Задействането трябва да е идемпотентно, когато е възможно.
Най-общо казано, тригерите ТРЯБВА бъдете идемпотентни. Това важи независимо дали е базиран на таблица или базиран на изглед тригер. Това се отнася особено за тези, които трябва да променят данните в базовите таблици, откъдето се наблюдава тригера. Защо? Защото, ако хората променят данните, които ще бъдат взети от спусъка, те може да разберат, че са направили грешка, редактирали са го отново или може би просто редактират същия запис и го запазят 3 пъти. Те няма да се зарадват, ако установят, че отчетите се променят всеки път, когато правят редакция, която не би трябвало да променя изхода за отчета.
За да бъдем по-ясни, може да е изкушаващо да опитате да оптимизирате задействането, като направите нещо подобно на това:
С SourceData КАТО (ИЗБЕРЕТЕ идентификатор на поръчка, SUM(SalesAmount) КАТО NewSaleTotal ОТ вмъкната ГРУПА BY OrderID)СЛЕВАЙТЕ В dbo.SalesOrder КАТО ИЗПОЛЗВАЙТЕ SourceData КАТО dON o.OrderID =d.OrderID, КОГАТО СЕ СЪВТОРИ, ПОСЛЕ АКТУАЛИЗИРАТЕ SET o.OrderTotal =o.OrderTotal =o.OrderTotal. + d.NewSaleTotal;Можем да избегнем повторното изчисляване на новия сбор, като просто прегледаме модифицираните редове във вмъкнатата таблица, нали? Но когато потребителят редактира записа, за да коригира печатна грешка в името на клиента, какво ще се случи? В крайна сметка получаваме фалшива сума и спусъкът вече работи срещу нас.
Досега трябва да разберете защо правилото #4 ни помага, като избутва само първичните ключове към съхранената процедура, вместо да се опитвате да прехвърлите каквито и да е данни в съхранената процедура или да го правите директно вътре в тригера, както би направила извадката .
Вместо това искаме да имаме код, подобен на този, в рамките на съхранена процедура:
СЪЗДАВАНЕ НА ПРОЦЕДУРА dbo.uspUpdateSalesTotal ( @SalesOrders SalesOrderTableType САМО ЧЕТЕНЕ) ЗАПОЧВАТЕ С SourceData КАТО ( ИЗБЕРЕТЕ s.OrderID, SUM(s.SalesAmount) КАТО NewSaleTotal ОТ dbo.SalesOrder КАТО FROM dbo.SalesOrder КАТО s xSELECT КАТО s xNULEX WHERE xSELECT .SalesOrderID =s.SalesOrderID ) ГРУПА ПО OrderID ) СЛИВАНЕ В dbo.SalesOrder КАТО o ИЗПОЛЗВАНЕ НА SourceData КАТО d ON o.OrderID =d.OrderID, КОГАТО СЕ СЪВТОРИ СЛЕД АКТУАЛИЗИРАНЕ ЗАДАДЕНО o.OrderTotal =d.NewSaleTotal>END;END;END;Използвайки @SalesOrders, ние все още можем селективно да актуализираме само редовете, които са били засегнати от тригера, и също така можем да преизчислим изцяло новата обща сума и да я направим новата сума. Така че дори ако потребителят е направил печатна грешка в името на клиента и го е редактирал, всяко записване ще даде същия резултат за този ред.
По-важното е, че този подход ни предоставя и лесен начин да коригираме общите суми. Да предположим, че трябва да направим групово импортиране и импортирането не съдържа общата сума, така че трябва да я изчислим сами. Можем да напишем съхранената процедура, която да записва директно в таблицата. След това можем да извикаме горепосочената съхранена процедура, предавайки идентификаторите от импортирането, и всички сме добре. По този начин логиката, която използваме, не е обвързана със спусъка зад изгледа. Това помага, когато логиката не е необходима за груповото импортиране, което извършваме.
Ако откриете, че имате проблем да направите своя тригер идемпотентен, това е силен знак, че може да се наложи да използвате съхранена процедура вместо това и да я извикате директно от приложението си, вместо да разчитате на тригери. Едно забележително изключение от това правило е, когато тригерът е предназначен предимно да бъде одиторски тригер. В този случай наистина искате да напишете нов ред в таблицата за одит за всяка редакция, включително всички правописни грешки, които потребителят прави. Това е добре, защото в този случай няма промени в данните, с които взаимодейства потребителят. От POV на потребителя резултатът все още е същият. Но винаги, когато тригерът трябва да манипулира същите данни, с които работи потребителят, е много по-добре, когато е идемпотент.
Приключване
Надяваме се, че вече можете да видите колко по-трудно може да бъде проектирането на добре работещ тригер. Поради тази причина трябва внимателно да прецените дали можете да го избегнете напълно и да използвате директни извиквания със съхранена процедура. Но ако сте стигнали до заключението, че трябва да имате тригери за управление на модификациите, направени чрез изгледи, надявам се, че правилата ще ви помогнат. Създаването на спусъка на базата на набор е достатъчно лесно с някои настройки. За да го направите идемпотент, обикновено изисква повече мисли за това как ще приложите своите съхранени процедури.
Ако имате още предложения или правила за споделяне, пуснете ги в коментарите!