Термін "попередній перегляд" в більшості випадків може бути досить оманливим, залежно від характеру даних, якими оперують (і що змінюється від операції до операції). Що потрібно забезпечити, щоб поточні дані, якими оперують, перебуватимуть у тому самому стані між часом збирання даних "попереднього перегляду" та коли користувач повернеться через 15 хвилин - після схоплення кави, виходу на вулицю для куріння, прогулянки навколо блоку, повертаючись і перевіряючи щось на eBay - і розуміє, що вони не натискали кнопку "ОК", щоб насправді виконати операцію, і, нарешті, натискають кнопку?
У вас є обмеження часу на продовження операції після створення попереднього перегляду? Або, можливо, спосіб визначити, що дані перебувають у тому ж стані на час модифікації, як це було у початковий SELECTчас?
Це незначний момент, оскільки приклад коду міг би бути зроблений поспішно і не представляти справжній випадок використання, але навіщо існувати "Попередній перегляд" для INSERTоперації? Це може мати сенс, коли вставляти кілька рядків через щось подібне, INSERT...SELECTі може бути вставлена змінна кількість рядків, але це не має особливого сенсу для однотонної операції.
це небажано через ... відносно низьку ступінь впевненості, що дані попереднього перегляду насправді є точним відображенням того, що буде з оновленням.
Звідки саме ця «низька ступінь впевненості»? Незважаючи на те, що можна оновити іншу кількість рядків, ніж відображатись у випадку, SELECTколи декілька таблиць приєднані, і в наборі результатів є дублювання рядків, це не повинно бути проблемою. Будь-які рядки, на які має вплинути а, UPDATEможна вибрати самостійно. Якщо є невідповідність, ви робите запит неправильно.
І ті ситуації, коли є дублювання через таблицю JOINed, яка відповідає декільком рядкам у таблиці, яка буде оновлена, не є ситуаціями, коли буде створено "Попередній перегляд". І якщо є такий випадок, коли це так, то потрібно пояснити користувачеві, що вони оновлюють підмножину звіту, яка повторюється у звіті, щоб не виявлялося помилок, якщо хтось лише дивлячись на кількість уражених рядів.
Для повноти (хоча інші відповіді згадували про це), ви не використовуєте TRY...CATCHконструкцію, тому ви могли легко стикатися з проблемами при вкладенні цих викликів (навіть якщо не використовуєте Save Points і навіть якщо не використовуєте Transaction). Будь ласка, дивіться мою відповідь на наступне запитання, тут, на DBA.SE, для шаблону, який обробляє транзакції через вкладені виклики збереженої процедури:
Чи потрібно нам обробляти транзакції в коді C #, а також у збереженій процедурі
ВИНАСЛЯ, якщо проблеми, зазначені вище, були враховані, все ще існує критичний недолік: за короткий проміжок часу операція виконується (тобто до початку ROLLBACK), будь-які запити, що читаються брудно (чи запити з використанням WITH (NOLOCK)або SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED), можуть захоплювати дані, які хіба немає хвилини пізніше Хоча кожен, хто використовує запити з читанням, повинен уже знати про це і прийняв таку можливість, такі операції значно збільшують шанси на введення аномалій даних, які дуже важко налагодити (тобто: скільки часу ви хочете витратити на спроби знайти проблему, яка не має явної прямої причини?).
Така модель також погіршує продуктивність системи, збільшуючи блокування, знімаючи більше блокувань, і генеруючи більше активності журналу транзакцій. (Я бачу, що @MartinSmith також згадував ці 2 випуски у коментарі до питання.)
Крім того, якщо на таблицях, що змінюються, є тригери, це може бути непотрібною додатковою обробкою (читання процесора та фізичного / логічного характеру). Тригери також додатково збільшать шанси на аномалії даних, отримані внаслідок брудного читання.
Зв'язане з пунктом, зазначеним вище - збільшенням блокування - використання транзакції збільшує ймовірність попадання в тупикові місця, особливо якщо задіяні тригери.
Менш гостра проблема, яка має стосуватися лише менш вірогідного сценарію INSERTоперацій: дані "Попередній перегляд" можуть бути не такими, як ті, що вставляються щодо значень стовпців, визначених DEFAULTобмеженнями ( Sequences/ NEWID()/ NEWSEQUENTIALID()) та IDENTITY.
Немає необхідності в додатковому накладному введенні вмісту змінної таблиці у тимчасову таблицю. Це ROLLBACKне вплине на дані в табличній змінній (саме тому ви сказали, що в першу чергу використовуєте табличні змінні), тому було б більше сенсу просто SELECT FROM @output_to_return;в кінці, а потім навіть не турбуватися про створення тимчасової Таблиця.
Про всяк випадок, коли цей нюанс Save Points не відомий (важко сказати з прикладу коду, оскільки він показує лише єдину збережену процедуру): вам потрібно використовувати унікальні імена Save Point, щоб ROLLBACK {save_point_name}операція вела себе так, як ви цього очікували. Якщо ви повторно використовуєте імена, ROLLBACK відкатить останню точку збереження цього імені, яка може бути не на тому ж рівні вкладеності, з ROLLBACKякого викликається. Будь ласка, подивіться перший приклад блоку коду в наступній відповіді, щоб побачити цю поведінку в дії: транзакція у збереженій процедурі
Робота "Попереднього перегляду" не має великого сенсу для операцій, орієнтованих на користувачів. Я роблю це часто для операцій з технічного обслуговування, щоб я міг побачити, що буде видалено / зібраний сміття, якщо я продовжую операцію. Я додаю необов'язковий параметр, який називається, @TestModeі роблю IFоператор, який або робить a, SELECTколи @TestMode = 1ще це робить DELETE. Я іноді додаю @TestModeпараметр до Збережених процедур, викликаних програмою, щоб я (та інші) могли провести тестування, не впливаючи на стан даних, але цей параметр ніколи не використовується додатком.
Про всяк випадок, якщо цього не було зрозуміло з верхнього розділу "питань":
Якщо вам потрібен / хочете режим "Попередній перегляд" / "Тест", щоб побачити, на що слід вплинути, якщо слід виконати оператор DML, тоді НЕ використовуйте для цього операції (тобто BEGIN TRAN...ROLLBACKшаблон). Це закономірність, яка, в кращому випадку, реально працює лише для однокористувацької системи, і навіть не є хорошою ідеєю в цій ситуації.
Повторення основної маси запиту між двома гілками IFвисловлювання справді представляє потенційну проблему необхідності оновлювати їх обидва щоразу, коли є якісь зміни. Однак, відмінності між двома запитами, як правило, досить прості, щоб знайти їх в огляді коду та легко виправити. З іншого боку, такі проблеми, як розбіжність у державі та брудне читання, набагато важче знайти та виправити. І проблему зниження продуктивності системи неможливо виправити. Нам потрібно визнати і прийняти, що SQL не є об'єктно-орієнтованою мовою, і інкапсуляція / зменшення дублюваного коду не була ціллю дизайну SQL, як це було у багатьох інших мов.
Якщо запит досить довгий / складний, його можна інкапсулювати за допомогою функції вбудованої таблиці. Тоді ви можете зробити простий SELECT * FROM dbo.MyTVF(params);для режиму "Попередній перегляд" та ПРИЄДНАЙТЕСЬ до ключового значення для режиму "зроби це". Наприклад:
UPDATE tab
SET tab.Col2 = tvf.ColB
...
FROM dbo.Table tab
INNER JOIN dbo.MyTVF(params) tvf
ON tvf.ColA = tab.Col1;
Якщо це сценарій звіту, як ви згадували, це може бути, тоді запуск початкового звіту - це «Попередній перегляд». Якщо хтось хоче змінити те, що вони бачать у звіті (можливо, стан), це не потребує додаткового попереднього попереднього перегляду, оскільки очікується зміна відображуваних в даний час даних.
Якщо операція може змінити суму ставки на певний% або бізнес-правило, то це можна вирішити в презентаційному шарі (JavaScript?).
Якщо вам дійсно потрібно зробити "Попередній перегляд" для операції , орієнтованої на кінцевого користувача , тоді вам потрібно спочатку зафіксувати стан даних (можливо, хеш усіх полів у наборі результатів для UPDATEоперацій або ключові значення для DELETEоперацій), а потім, перш ніж виконувати операцію, порівняйте інформацію про захоплений стан із поточною інформацією - в межах транзакції, роблячи HOLDблокування на таблиці, щоб нічого не змінилося після цього порівняння - і якщо є ЯКЩО різниця, киньте помилка та ROLLBACKскоріше робити , ніж продовжувати UPDATEабо DELETE.
Для виявлення відмінностей для UPDATEоперацій альтернативою обчислення хешу у відповідних полях буде додавання стовпця типу ROWVERSION . Значення ROWVERSIONтипу даних автоматично змінюється щоразу, коли відбувається зміна цього рядка. Якби у вас був такий стовпець, ви б додали SELECTйого разом з іншими даними "Попередній перегляд", а потім передаєте їх разом із кроком "обов'язково, продовжуйте і виконайте оновлення" разом із ключовими значеннями та значеннями. змінювати. Потім ви порівнюєте ці ROWVERSIONпередані значення з "Попереднього перегляду" з поточними значеннями (для кожної клавіші) і продовжуєте лише з UPDATEif ВСЕвідповідних значень. Перевага тут полягає в тому, що вам не потрібно обчислювати хеш, який має потенціал, навіть якщо це малоймовірно, для помилкових негативів, і займає деяку кількість часу кожен раз, коли ви робите це SELECT. З іншого боку, ROWVERSIONзначення збільшується автоматично лише при зміні, тому нічого, про що ніколи не потрібно турбуватися. Тим не менш, ROWVERSIONтип - 8 байт, які можна скласти під час роботи з багатьма таблицями та / або багатьма рядками.
Кожен з цих двох методів для виявлення невідповідних станів, пов'язаних з UPDATEопераціями, має плюси і мінуси , тож вам слід визначити, який метод має більше "pro", ніж "con" для вашої системи. Але в будь-якому випадку ви можете уникнути затримки між створенням попереднього перегляду та виконанням операції, що не спричинить поведінку поза очікуванням кінцевого користувача.
Якщо ви працюєте з режимом "Попередній перегляд", орієнтованим на кінцевого користувача, то крім того, щоб фіксувати стан записів у час вибору, проходити разом та перевіряти в час модифікації, включайте DATETIMEдля SelectTimeабо заповнення через GETDATE()або щось подібне. Передайте це разом із рівнем програми, щоб його можна було повернути до збереженої процедури (здебільшого, як єдиний вхідний параметр), щоб його можна було перевірити в Збереженій процедурі. Тоді ви можете визначити, що якщо операція не є режимом "Попередній перегляд", то @SelectTimeзначення повинно бути не більше X хвилин до поточного значення GETDATE(). Може 2 хвилини? 5 хвилин? Швидше за все, не більше 10 хвилин. Введіть помилку, якщо значення DATEDIFFMINUTES перевищує поріг.