Чи слід перевірити, чи є щось у db та швидко вийти з ладу, або чекати виключення db


32

Маючи два класи:

public class Parent 
{
    public int Id { get; set; }
    public int ChildId { get; set; }
}

public class Child { ... }

При призначенні ChildIdна Parentя повинен перевірити перший , якщо він існує в БД або чекати DB кинути виняток?

Наприклад (використання Entity Framework Core):

ПРИМІТКА. Ці види перевірок ВСІ НАД ІНТЕРНЕТ навіть на офіційних документах Microsoft: https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using- mvc / handling-concurrency-with-the-object-Framework-in-as-asp-net-mvc-application # modify-the-отдел-контролер, але є додаткова обробка виключень дляSaveChanges

також зауважте, що основною метою цієї перевірки було повернути користувачеві API дружнє повідомлення та відомий статус HTTP та не повністю ігнорувати винятки з бази даних. І єдине місце , при виникненні виключення всередині SaveChangesабо SaveChangesAsyncвиклик ... так що не буде якихось - яких винятків , коли ви телефонуєте FindAsyncабо Any. Тож, якщо дитина існує, але її було видалено до SaveChangesAsyncцього, буде викинуто виняток з одночасності.

Я зробив це через те, що foreign key violationвиняток буде набагато складніше форматувати для відображення "Child with id {parent.ChildId} не вдалося знайти".

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    // is this code redundant?
   // NOTE: its probably better to use Any isntead of FindAsync because FindAsync selects *, and Any selects 1
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null)
       return NotFound($"Child with id {parent.ChildId} could not be found.");

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        

    return parent;
}

проти:

public async Task<ActionResult<Parent>> CreateParent(Parent parent)
{
    _db.Parents.Add(parent);
    await _db.SaveChangesAsync();  // handle exception somewhere globally when child with the specified id doesn't exist...  

    return parent;
}

Другий приклад у Postgres призведе до 23503 foreign_key_violationпомилки: https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html

Недоліком обробки винятків таким чином в ORM, як EF, є те, що він буде працювати лише з певним резервним бази даних. Якщо ви коли-небудь хотіли перейти на SQL-сервер чи щось інше, це більше не працюватиме, оскільки код помилки зміниться.

Неправильне форматування винятку для кінцевого користувача може викрити деякі речі, які не хочуть бачити ніхто, крім розробників.

Пов'язані:

https://stackoverflow.com/questions/6171588/preventing-race-condition-of-if-exists-update-else-insert-in-entity-framework

https://stackoverflow.com/questions/4189954/implementing-if-not-exists-insert-using-entity-framework-without-race-conditions

https://stackoverflow.com/questions/308905/should-there-be-a-transaction-for-read-queries


2
Обмін дослідженнями допомагає всім . Розкажіть, що ви пробували і чому це не відповідало вашим потребам. Це свідчить про те, що ви знайшли час, щоб спробувати допомогти собі, це позбавляє нас від повторення очевидних відповідей, а найбільше це допомагає вам отримати більш конкретну та релевантну відповідь. Також дивіться Як просити
gnat

5
Як уже згадували інші, існує можливість, що запис можна було б вставити або видалити одночасно під час перевірки NotFound. З цієї причини перевірка спочатку здається неприйнятним рішенням. Якщо ви стурбовані тим, як писати обробку винятків для Postgres, яка не переноситься на інші бази даних, спробуйте структурувати обробник винятків таким чином, щоб функціональну основу можна було розширити на класи, специфічні для бази даних (SQL, Postgres тощо)
billrichards

3
Переглядаючи коментарі, мені потрібно сказати це: перестаньте думати в довершеності . "Невдалий збій" не є ізольованим, поза контекстним правилом, якого можна або слід сліпо слідувати. Це правило. Завжди аналізуйте, чого ви насправді намагаєтесь досягти, а потім розглядайте будь-яку техніку з урахуванням того, чи допомагає вона досягти цієї мети чи ні. "Невдача невдачі" допоможе вам запобігти випадкові побічні ефекти. І крім того, "невдача швидко" насправді означає "невдача, як тільки ви зможете виявити, що є проблема". Обидві методи виходять з ладу, як тільки виявляється проблема, тому ви повинні переглянути інші міркування.
jpmc26

1
@Konrad які винятки пов’язані з цим? Перестаньте думати про умови перегонів як про щось, що живе у вашому коді: це властивість Всесвіту. Будь- що, що стосується ресурсу, яким він повністю не керує (наприклад, прямий доступ до пам'яті, спільна пам'ять, база даних, API REST, файлова система тощо тощо) не один раз, і очікує, що воно буде незмінним, має потенційну умову перегонів. Чорт, ми маємо справу з цим в C, який навіть не має винятків. Просто ніколи не розгалужуйте стан ресурсу, яким ви не керуєте, якщо хоча б одна з гілок псується зі станом цього ресурсу.
Джаред Сміт

1
@DanielPryden У своєму запитанні я не сказав, що не хочу обробляти винятки з бази даних (я знаю, що винятки неминучі). Я думаю, що багато людей неправильно зрозуміли, я хотів, щоб дружнє повідомлення про помилку для мого веб-API (для кінцевих користувачів, яке вони могли читати), як Child with id {parent.ChildId} could not be found.. І форматування "Порушення закордонних ключів" я думаю, що в цьому випадку гірше.
Конрад

Відповіді:


3

Швидше заплутане питання, але ТАК слід спочатку перевірити, а не просто обробляти виняток з БД.

Перш за все, у вашому прикладі ви знаходитесь на рівні даних, використовуючи EF безпосередньо в базі даних для запуску SQL. Ваш код еквівалентний запущеному

select * from children where id = x
//if no results, perform logic
insert into parents (blah)

Альтернатива, яку ви пропонуєте:

insert into parents (blah)
//if exception, perform logic

Використання винятку для виконання умовної логіки повільно і загально нахмурене.

У вас є умова перегонів, і ви повинні використовувати транзакцію. Але це можна повністю зробити в коді.

using (var transaction = new TransactionScope())
{
    var child = await _db.Children.FindAsync(parent.ChildId);
    if (child == null) 
    {
       return NotFound($"Child with id {parent.ChildId} could not be found.");
    }

    _db.Parents.Add(parent);    
    await _db.SaveChangesAsync();        
    transaction.Complete();

    return parent;
}

Ключове - запитати себе:

"Ви очікуєте, що така ситуація станеться?"

Якщо ні, то обов'язково вставляйте і кидайте виняток. Але просто обробляйте виняток, як будь-яку іншу помилку, яка може статися.

Якщо ви дійсно очікуєте, що це відбудеться, то це НЕ є винятковим, і вам слід перевірити, чи є дитина першим, відповівши відповідним дружнім повідомленням, якщо цього немає.

Редагувати - Існує багато суперечок з цього приводу. Перш ніж звернутись до голосування, розгляньте:

А. Що робити, якщо були два обмеження ФК. Чи рекомендуєте ви проаналізувати повідомлення про виключення, щоб визначити, який об’єкт відсутній?

B. Якщо у вас є промах, запускається лише один оператор SQL. Це лише звернення, які несуть додаткові витрати на другий запит.

C. Зазвичай Id - це сурогатний ключ, Важко уявити ситуацію, коли ви її знаєте, і ви не дуже впевнені, що це в БД. Перевірка була б дивною. Але що робити, якщо користувач ввів його природний ключ? Це може мати високий шанс не бути присутнім


Коментарі не для розширеного обговорення; ця розмова перенесена в чат .
maple_shaft

1
Це абсолютно неправильно і вводить в оману! Саме такі відповіді дають поганих професіоналів, проти яких мені завжди доводиться боротися. SELECT ніколи не блокує таблицю, тому між SELECT та INSERT, UPDATE або DELTE запис може змінюватися. Тож це поганий паршивий дизайн програмного забезпечення та аварія, яка чекає на виробництво.
Даніель Лобо

1
@DanielLobo угоди виправляє, що
Еван

1
випробуй це, якщо ти мені не повіриш
Еван

1
@yusha У мене тут код
Еван

111

Перевірка унікальності, а потім встановлення - це антипатерн; завжди може статися, що ідентифікатор вставляється одночасно між часом перевірки та часом запису. Бази даних обладнані для вирішення цієї проблеми за допомогою таких механізмів, як обмеження та транзакції; більшість мов програмування не є. Тому, якщо ви цінуєте узгодженість даних, залиште це експерту (базі даних), тобто зробіть вставку та виберіть виняток, якщо воно відбудеться.


34
перевірка і невдача - не швидше, ніж просто "спроба" і сподівання на краще. Раніше має на увазі 2 операції, які потрібно впровадити та виконати вашою системою, і 2 - БД, тоді як остання передбачає лише одну з них. Перевірка делегована серверу БД. Це також передбачає один менший скачок у мережу та одне менше завдання, яке має брати участь у БД. Ми можемо подумати, що ще один запит до БД є доступним, але ми часто забуваємо думати по-великому. Подумайте про високу конкурентоспроможність, що викликає запит більш ніж у сто разів. Це може дублювати весь трафік до БД. Якщо це важливо, вирішувати вам.
Лаїв

6
@Konrad Моя позиція полягає в тому , що по замовчуванню, правильний вибір по це один запит , який не зможе сам по собі, і це окремий запит попередньо пульсуючою підхід , який несе тягар доведення , щоб виправдати себе. Що стосується «стати проблемою»: так що ви є використанням транзакцій або іншим чином гарантуючи , що ви захищені від помилок ToCToU , вірно? Мені не очевидно з опублікованого коду, що ви є, але якщо ви цього не зробите, то це вже стало проблемою в тому, що тикача бомба стає проблемою задовго до того, як вона насправді вибухне.
mtraceur

4
@Konrad EF Core не збирається чітко ставити як ваш чек, так і вкладиш в одну транзакцію, вам доведеться явно вимагати цього. Без транзакції перевірка спочатку є безглуздою, оскільки стан бази даних може змінюватися між чеком і вставленням у будь-якому випадку. Навіть за допомогою транзакції ви можете не завадити базі даних змінюватися під вашими ногами. Ми стикалися з проблемою кілька років тому, використовуючи EF з Oracle, де, хоча db підтримує це, Entity не запускає блокування записів прочитаних в рамках транзакції, і лише вставка розглядалася як транзакційна.
Містер Міндор

3
"Перевірка унікальності, а потім встановлення - це антипатерн", я б цього не сказав. Це сильно залежить від того, чи можна припустити, що ніяких інших модифікацій не відбувається, і від того, чи перевірка дає якийсь корисніший результат (навіть просто повідомлення про помилку, що насправді щось означає для читача), коли його не існує. Ні, якщо база даних обробляє одночасні веб-запити, ні, ви не можете гарантувати, що інші зміни не відбуваються, але є випадки, коли це обгрунтоване припущення.
jpmc26

5
Перевірка на унікальність спочатку не усуває необхідності впоратися з можливими відмовами. З іншого боку, якщо дія буде вимагати виконання декількох операцій, перевіряючи , чи всі мають велику ймовірність успіху , перш ніж почати будь - якої з них часто краще , ніж виконання дій , які можуть , ймовірно , повинні бути згорнуті. Виконання первинних перевірок може не уникнути всіх ситуацій, коли необхідно відкинути, але це може допомогти зменшити частоту таких випадків.
supercat

38

Я думаю, що те, що ви називаєте "невдалий швидкий", і те, що я називаю, це не те саме.

Скажіть базі даних про внесення змін і усунення несправності, тобто швидко. Ваш шлях складний, повільний і не особливо надійний.

Ця техніка не виходить з ладу, вона є «передпольотною». Іноді є вагомі причини, але не тоді, коли ви використовуєте базу даних.


1
Бувають випадки, коли вам потрібен другий запит, коли один клас залежить від іншого, тому у подібних випадках у вас немає вибору.
Конрад

4
Але не тут. І запити до баз даних можуть бути досить розумними, тому я взагалі сумніваюся у "виборі немає".
gnasher729

1
Я думаю, що це також залежить від програми, якщо ви створюєте її лише для кількох користувачів, то вона не повинна змінювати, і код є більш читабельним за допомогою двох запитів.
Конрад

21
Ви припускаєте, що ваша БД зберігає непослідовні дані. Іншими словами, виглядає так, що ви не довіряєте своїй БД та узгодженості даних. Якби це було так, у вас справді велика проблема, і ваше рішення - це прохід. Паліативне рішення, як вважалося, було відмінено раніше, ніж пізніше. Можуть бути випадки, коли вас змушують споживати БД поза вашим контролем та управлінням. З інших програм. У тих випадках я вважав би такі перевірки. У будь-якому випадку, @gnasher має рацію, ваше не швидко провалюється, або це не те, що ми розуміємо як невдалий швидкий.
Лаїв

15

Це почалося як коментар, але стало занадто великим.

Ні, як було сказано в інших відповідях, цю модель не слід використовувати. *

При роботі з системами, які використовують асинхронні компоненти, завжди буде гоночна умова, коли база даних (або файлова система, або інша система асинхронізації) може змінюватися між перевіркою та зміною. Перевірка такого типу просто не є надійним способом запобігання типу помилок, з якими ви не хочете звертатися.
Гірше, ніж бути недостатнім, з першого погляду створюється враження, що воно повинно бути першого запобігати помилкам дублювання запису, що створює помилкове відчуття безпеки.

У будь-якому випадку вам потрібна помилка обробки.

У коментарях ви запитали, що, якщо вам потрібні дані з кількох джерел.
Все ще ні.

фундаментальне питання не зникне , якщо то , що ви хочете перевірити , стає все більш складним.

Ви все одно потребуєте поводження з помилками.

Навіть якщо ця перевірка була надійним способом запобігти конкретній помилці, яку ви намагаєтеся захистити, інші помилки все ж можуть виникнути. Що станеться, якщо ви втратите зв’язок із базою даних, або у неї не вистачає місця, або?

Вам, ймовірно, все ще потрібні інші помилки, пов'язані з базою даних. Поводження з цією конкретною помилкою, ймовірно, має бути її невеликим шматком.

Якщо вам потрібні дані, щоб визначити, що потрібно змінити, вам, очевидно, потрібно буде збирати їх звідкись. (залежно від того, якими інструментами ви користуєтесь, мабуть, кращі способи, ніж окремі запити для їх збирання) Якщо, вивчаючи зібрані вами дані, ви визначаєте, що вам не потрібно вносити зміни все-таки чудово, не робіть змінити. Це визначення є повністю відокремленим від проблем щодо помилок.

Ви все одно потребуєте поводження з помилками.

Я знаю, що я повторююсь, але вважаю, що важливо зробити це зрозумілим. Я раніше прибирав цей безлад.

Зрештою, це не вдасться. Якщо це не вдасться, дістатися до дна важко і забирає багато часу. Важко вирішити питання, що виникають із умов перегонів. Вони не трапляються послідовно, тому важко або навіть неможливо відтворити ізольовано. Ви не почали належну обробку помилок для початку, тому вам, швидше за все, не доведеться багато чого: Можливо, звіт кінцевого користувача про якийсь криптований текст (який ви намагалися не допустити, щоб побачити в першу чергу.) Може бути, слід стека, який вказує на ту функцію, яка, дивлячись на неї, відверто заперечує помилку, повинна бути навіть можливою.

* Можуть бути поважні ділові причини для здійснення цих існуючих перевірок, наприклад, щоб уникнути копіювання додатком дорогих робіт, але це не є підходящою заміною для правильного поводження з помилками.


2

Я думаю, що тут слід зазначити другорядну річ - одна з причин, чому ви цього хочете, - це те, що ви можете відформатувати повідомлення про помилку, яке бачить користувач.

Я б щиро рекомендував вам:

а) показати кінцевому користувачеві те саме загальне повідомлення про помилку для кожної помилки.

b) запишіть фактичний виняток десь, до якого можуть отримати доступ лише розробники (якщо на сервері) або десь, які можуть бути надіслані вам інструментами повідомлення про помилки (якщо клієнт розгорнуто)

в) не намагайтеся відформатувати дані про виключення помилок, які ви реєструєте, якщо ви не можете додати більше корисної інформації. Ви не хочете, щоб випадково "відформатували" ту частину корисної інформації, яку ви могли б використати для відстеження проблеми.


Якщо коротко - винятки рясніють дуже корисною технічною інформацією. Нічого з цього не повинно бути кінцевим користувачем, і ви втрачаєте цю інформацію загрозу.


2
"показати кінцевому користувачеві однакове загальне повідомлення про помилку для кожної помилки." це було основною причиною, форматування винятку для кінцевого користувача виглядає як жахливе. ..
Конрад,

1
У будь-якій розумній системі баз даних ви маєте змогу програмно з'ясувати, чому щось не вдалося. Не повинно бути необхідним для розбору повідомлення про виключення. І взагалі: хто каже, що повідомлення про помилку потрібно взагалі відображати користувачеві? Ви можете провалити першу вставку та повторити спробу в циклі, поки ви не досягнете успіху (або до деякої межі спроб або часу). І насправді, пошук і повторне намагання - це те, що ви хочете все-таки реалізувати.
Даніель Приден
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.