Термін "попередній перегляд" в більшості випадків може бути досить оманливим, залежно від характеру даних, якими оперують (і що змінюється від операції до операції). Що потрібно забезпечити, щоб поточні дані, якими оперують, перебуватимуть у тому самому стані між часом збирання даних "попереднього перегляду" та коли користувач повернеться через 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
передані значення з "Попереднього перегляду" з поточними значеннями (для кожної клавіші) і продовжуєте лише з UPDATE
if ВСЕвідповідних значень. Перевага тут полягає в тому, що вам не потрібно обчислювати хеш, який має потенціал, навіть якщо це малоймовірно, для помилкових негативів, і займає деяку кількість часу кожен раз, коли ви робите це SELECT
. З іншого боку, ROWVERSION
значення збільшується автоматично лише при зміні, тому нічого, про що ніколи не потрібно турбуватися. Тим не менш, ROWVERSION
тип - 8 байт, які можна скласти під час роботи з багатьма таблицями та / або багатьма рядками.
Кожен з цих двох методів для виявлення невідповідних станів, пов'язаних з UPDATE
операціями, має плюси і мінуси , тож вам слід визначити, який метод має більше "pro", ніж "con" для вашої системи. Але в будь-якому випадку ви можете уникнути затримки між створенням попереднього перегляду та виконанням операції, що не спричинить поведінку поза очікуванням кінцевого користувача.
Якщо ви працюєте з режимом "Попередній перегляд", орієнтованим на кінцевого користувача, то крім того, щоб фіксувати стан записів у час вибору, проходити разом та перевіряти в час модифікації, включайте DATETIME
для SelectTime
або заповнення через GETDATE()
або щось подібне. Передайте це разом із рівнем програми, щоб його можна було повернути до збереженої процедури (здебільшого, як єдиний вхідний параметр), щоб його можна було перевірити в Збереженій процедурі. Тоді ви можете визначити, що якщо операція не є режимом "Попередній перегляд", то @SelectTime
значення повинно бути не більше X хвилин до поточного значення GETDATE()
. Може 2 хвилини? 5 хвилин? Швидше за все, не більше 10 хвилин. Введіть помилку, якщо значення DATEDIFF
MINUTES перевищує поріг.