Должен ли я проверить, существует ли что-то в БД и быстро потерпеть неудачу, или дождаться исключения БД

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 / processing-Concurrency-Concurrency-with-the-entity-framework-in-an-asp-net-mvc-application # modify-the-Department-controller, но есть дополнительная обработка исключенийSaveChanges

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

Я сделал это из-за того, что foreign key violationисключение будет гораздо сложнее отформатировать для отображения «Дочерний объект с 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

Konrad
источник
2
Поделиться своими исследованиями помогает всем . Расскажите нам, что вы пробовали и почему это не соответствует вашим потребностям. Это свидетельствует о том, что вы потратили время, чтобы попытаться помочь себе, избавляет нас от повторения очевидных ответов и, прежде всего, помогает получить более конкретный и актуальный ответ. Также см. Как спросить
комнат
5
Как уже упоминали другие, существует возможность, что запись может быть вставлена ​​или удалена одновременно с проверкой NotFound. По этой причине проверка сначала кажется неприемлемым решением. Если вас беспокоит написание обработки исключений, специфичных для Postgres, которая не переносима на другие серверные базы данных, попробуйте структурировать обработчик исключений таким образом, чтобы базовая функциональность могла быть расширена за счет классов, специфичных для базы данных (SQL, Postgres и т. Д.)
billrichards
3
Просматривая комментарии, мне нужно сказать следующее: перестань думать в банальности . «Быстро провалиться» не является изолированным, вне контекста правилом, которому можно или нужно следовать слепо. Это эмпирическое правило. Всегда анализируйте, чего вы на самом деле пытаетесь достичь, а затем рассматривайте любую технику в свете того, помогает ли она вам достичь этой цели или нет. «Fail fast» помогает предотвратить нежелательные побочные эффекты. И, кроме того, «быстрый сбой» действительно означает «потерпеть неудачу, как только вы обнаружите, что есть проблема». Обе технологии терпят неудачу, как только проблема обнаружена, поэтому вы должны рассмотреть другие соображения.
jpmc26
1
@ Конрад, при чем тут исключения? Перестаньте думать о расовых условиях как о чем-то, что живет в вашем коде: это свойство вселенной. Ничего, ничего , что касается ресурса , он не полностью управления (например , прямого доступа к памяти, разделяемой памяти, базы данных, REST API, файловую систему и т.д. и т.п.) больше , чем один раз , и ожидает , что он будет неизменным имеет потенциальное состояние гонки. Черт возьми, мы имеем дело с этим в 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;
}

Главное, спросить себя:

"Вы ожидаете, что эта ситуация произойдет?"

Если нет, то обязательно вставьте и выбросьте исключение. Но просто обработайте исключение, как любую другую ошибку, которая может произойти.

Если вы ожидаете, что это произойдет, то это НЕ исключение, и вам следует проверить, существует ли ребенок первым, и ответить соответствующим дружеским сообщением, если это не так.

Редактировать - Кажется, есть много споров по этому поводу. Перед тем как понизить голосование рассмотрим:

А. Что, если было два ограничения ФК. Вы бы выступили за разбор сообщения об исключении, чтобы выяснить, какой объект отсутствовал?

Б. Если вы пропустите, выполняется только один оператор SQL. Это только попадания, которые требуют дополнительных затрат на второй запрос.

C. Обычно Id - это суррогатный ключ. Трудно представить ситуацию, когда вы знаете ее, и вы не уверены, что она находится в БД. Проверка была бы странной. Но что, если это естественный ключ, введенный пользователем? Это может иметь высокий шанс не присутствовать

Ewan
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
maple_shaft
1
Это совершенно неправильно и вводит в заблуждение! Именно такие ответы дают плохих профессионалов, с которыми мне всегда приходится бороться. SELECT никогда не блокирует таблицу, поэтому между SELECT и INSERT, UPDATE или DELTE запись может измениться. Так что это плохое паршивое программное обеспечение соизволило и произошел несчастный случай на производстве.
Даниэль Лобо
1
Операционная область @DanielLobo исправляет это
Эван
1
проверь, если не веришь мне
Юэн
1
@yusha У меня есть код прямо здесь
Ewan
111

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

Килиан Фот
источник
34
проверка и неудача не быстрее, чем просто «попытка» и надежда на лучшее. Прежний подразумевает 2 операции, которые должны быть реализованы и выполнены вашей системой, и 2 БД, в то время как последние подразумевают только одну из них. Проверка делегирована на сервер БД. Это также подразумевает меньшее количество переходов в сеть и одну меньшую задачу, которую должна выполнять БД. Мы можем подумать, что еще один запрос к БД является доступным, но мы часто забываем думать в целом. Подумайте о высоком параллелизме, который вызывает запрос снова и снова. Это может дублировать весь трафик в БД. Если это имеет значение, решать вам.
Laiv
6
@Konrad Моя позиция заключается в том, что правильный выбор по умолчанию - это один запрос, который сам по себе будет неудачным, и это отдельный подход к предварительной проверке запросов , у которого есть бремя доказательства, чтобы оправдать себя. Что касается «стать проблемой»: так что вы являетесь использованием транзакций или иным образом гарантируя , что вы защищены от ошибок ToCToU , верно? Для меня это не очевидно из кода, размещенного вами, но если это не так, то это уже стало проблемой, так как бомба-тиканье становится проблемой задолго до того, как она действительно взорвется.
mtraceur
4
@Konrad EF Core не собирается неявно помещать и ваш чек, и вставку в одну транзакцию, вам придется явно запросить это. Без транзакции первая проверка не имеет смысла, так как состояние базы данных может измениться между проверкой и вставкой в ​​любом случае. Даже при транзакции вы не сможете предотвратить изменение базы данных под ногами. Несколько лет назад мы столкнулись с проблемой, используя EF с Oracle, где, хотя база данных поддерживает ее, Entity не запускала блокировку прочитанных записей в транзакции, и только вставка считалась транзакционной.
Мистер Миндор
3
«Проверка на уникальность, а затем настройка - это антипаттерн». Я бы не сказал этого. Это сильно зависит от того, можете ли вы предположить, что никаких других изменений не происходит, и от того, дает ли проверка более полезный результат (даже просто сообщение об ошибке, которое фактически что-то значит для читателя), когда он не существует. С базой данных, обрабатывающей параллельные веб-запросы, нет, вы не можете гарантировать, что другие изменения не происходят, но есть случаи, когда это разумное предположение.
jpmc26
5
Проверка уникальности в первую очередь не устраняет необходимость обрабатывать возможные сбои. С другой стороны, если действие потребует выполнения нескольких операций, проверка того, все ли будут выполнены успешно, перед запуском какой-либо из них, часто лучше, чем выполнение действий, которые, вероятно, необходимо откатить. Выполнение первоначальных проверок может не избежать всех ситуаций, когда потребуется откат, но это может помочь уменьшить частоту таких случаев.
суперкат
38

Я думаю, что то, что вы называете «быстро провалиться», и то, что я называю это, не то же самое

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

Эта ваша техника не быстро проваливается, это «предполетная подготовка». Иногда есть веские причины, но не при использовании базы данных.

gnasher729
источник
1
Есть случаи, когда вам нужен второй запрос, когда один класс зависит от другого, поэтому у вас нет выбора в подобных случаях.
Конрад
4
Но не здесь. И запросы к базе данных могут быть довольно умными, поэтому я вообще сомневаюсь в «нет выбора».
gnasher729
1
Я думаю, что это также зависит от приложения: если вы создаете его только для нескольких пользователей, это не должно иметь значения, и код становится более читабельным с помощью 2 запросов.
Конрад
21
Вы предполагаете, что ваша БД хранит противоречивые данные. Другими словами, похоже, что вы не доверяете своей БД и целостности данных. Если бы это было так, у вас действительно большая проблема, и ваше решение - обходной путь. Паллиативное решение обречено быть отмененным раньше, чем позже. Могут быть случаи, когда вы вынуждены использовать БД вне вашего контроля и управления. Из других приложений. В этих случаях я бы рассмотрел такие проверки. В любом случае, @gnasher прав, вы не быстро терпите неудачу, или это не то, что мы понимаем как быстро терпеть неудачу.
18:00
15

Это началось как комментарий, но стало слишком большим.

Нет, как указывалось в других ответах, этот шаблон не должен использоваться. *

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

Вам все равно нужна обработка ошибок.

В комментариях вы спросили, что если вам нужны данные из нескольких источников.
Все еще нет.

Фундаментальная проблема не исчезнет , если то , что вы хотите проверить , становится все более сложным.

Вам все равно нужна обработка ошибок в любом случае.

Даже если эта проверка была надежным способом предотвращения конкретной ошибки, от которой вы пытаетесь защититься, все равно могут возникать другие ошибки. Что произойдет, если вы потеряете соединение с базой данных, или ей не хватит места, или?

В любом случае, вам, вероятно, все равно потребуется обработка ошибок, связанных с другими базами данных. Обработка этой конкретной ошибки, вероятно, должна быть небольшой частью этого.

Если вам нужны данные, чтобы определить, что изменить, вам, очевидно, нужно будет их где-то собрать. (в зависимости от того, какие инструменты вы используете, возможно, есть более эффективные способы их сбора, чем отдельные запросы). Если, изучая собранные вами данные, вы решаете, что в конце концов вам не нужно вносить изменения, прекрасно, не делайте менять. Это определение полностью отделено от проблем обработки ошибок.

Вам все равно нужна обработка ошибок в любом случае.

Я знаю, что я повторяюсь, но я чувствую, что важно прояснить это. Я убирал этот беспорядок раньше.

Это потерпит неудачу в конце концов. Когда это не удастся, будет трудно и много времени, чтобы добраться до сути. Решать проблемы, возникающие в условиях гонки, сложно. Они не происходят последовательно, поэтому будет трудно или даже невозможно воспроизвести в изоляции. Вы не включили надлежащую обработку ошибок для начала, так что вам вряд ли придется что-либо делать: возможно, сообщение конечного пользователя о каком-то загадочном тексте (который вы пытались предотвратить с самого начала). Может быть, трассировка стека, указывающая на ту функцию, которая, когда вы смотрите на нее, явно отрицает, что ошибка должна быть возможной.

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

Mr.Mindor
источник
2

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

Я от всей души рекомендую вам:

а) показать конечному пользователю одно и то же общее сообщение об ошибке для каждой возникающей ошибки.

б) зарегистрировать фактическое исключение где-нибудь, к которому могут получить доступ только разработчики (если они находятся на сервере), или куда-то, что может быть отправлено вам с помощью инструментов сообщения об ошибках (если клиент развернут)

в) не пытайтесь форматировать подробности исключений ошибок, которые вы регистрируете, если вы не можете добавить более полезную информацию. Вы не хотите случайно «отформатировать» одну полезную информацию, которую вы могли бы использовать для отслеживания проблемы.


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

Пэдди
источник
2
msgstr "показать конечному пользователю одно и то же общее сообщение об ошибке для каждой ошибки." это было главной причиной, форматирование исключения для конечного пользователя выглядит ужасно ...
Конрад,
1
В любой разумной системе баз данных вы должны программно выяснить, почему что-то не получилось. Не должно быть необходимости анализировать сообщение об исключении. И вообще: кто говорит, что сообщение об ошибке вообще должно отображаться пользователю? Вы можете потерпеть неудачу при первой вставке и повторении в цикле, пока не добьетесь успеха (или до некоторого предела попыток или времени). И на самом деле, в любом случае вы захотите осуществить откат и повтор.
Даниэль Приден