Рекомендуемый подход к обработке SqlException в приложениях БД - PullRequest
6 голосов
/ 12 сентября 2009

Я работаю над приложением базы данных, написанным на C #, с сервером SQL в качестве бэкэнда. А для целостности данных я стараюсь максимально усилить на уровне базы данных - отношения, проверки ограничений, триггеры.

Из-за них, если данные не согласованы, может произойти сбой сохранения / обновления / вставки, и приложение выдаст SqlException.

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

Однако есть вещи, которые действительно нельзя проверить в приложении, и которые обрабатываются БД: я имею в виду ошибки при удалении, когда нет каскадного удаления, и пользователь пытается удалить объект из основной таблицы и т. Д.

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

Я пишу код удаления в попытке ... поймать и обработать исключение, сообщая пользователю, что он не может удалить этого сотрудника. Но я хочу дать пользователю более значимую информацию - причина, по которой запись не может быть удалена. Может быть, это была просто запись сотрудника, которая была добавлена ​​в команду тестирования. Но пользователь забыл, где добавил, и если бы я мог сказать «Невозможно удалить сотрудника, потому что он является частью команды T1», пользователь будет знать, что сначала нужно перейти в Team T1, удалить пользователя, а затем попытаться удалить его снова. Это простой пример, поскольку, как я уже сказал, сотрудник может быть вовлечен во многие отношения - в моем приложении у меня есть как минимум 20.

Решение состоит в том, чтобы отобразить Сообщение, сообщаемое SqlException, но это совсем не элегантно. Во-первых, это сообщение очень техническое - оно говорит о FK, PK, Trigger, которые не имеют смысла для пользователей и будут их пугать. Во-вторых, мое приложение использует многоязычный интерфейс, и все меню и сообщения отображаются на выбранном пользователем языке (выбирается либо во время входа в систему, либо в профиле пользователя). И сообщение из SqlException является английским (если я использую английскую версию) или худшими, менее распространенными языками, такими как немецкий или голландский, если случается, что сервер SQL находится на этом языке.

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

Как вы справляетесь с этой ситуацией?

Спасибо за все ответы

(PS: простите за длинный пост)

Ответы [ 9 ]

6 голосов
/ 12 сентября 2009

К сожалению, здесь нет простого ответа.

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

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

3 голосов
/ 12 сентября 2009

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

Обычно в SqlException вы также получаете код ошибки SQL или что-то еще.

Лучшим подходом, вероятно, будет наличие отдельной таблицы в вашей базе данных, которая в основном отображает те технические ошибки SQL, которые могут случиться с более значимыми, ориентированными на пользователя сообщениями. Например. если ваша ошибка SQL говорит что-то вроде «fk protect blablabaal», вы можете иметь запись в вашей «UserErrorTable», которая сопоставляет это с сообщением пользователя, говорящим «не удалось удалить пользователя (this.and.that), скорее всего потому, что ... .. (он все еще член команды) "или что-то еще.

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

public class UserFriendlyException : Exception
{
  public string UserErrorMessage { get; set; }

  public UserFriendlyException(string message, SqlException exc) : base(message, exc)
  {
     UserErrorMessage = MapTechnicalExecptionToUserMessage(exc);
  }
}

Марк

2 голосов
/ 12 сентября 2009

Краткий ответ: «Не надо». Позвольте ошибкам пузыриться к глобальной обработке ошибок / регистрации. Правила проверки и бизнес-правила должны, как правило, исключать исключения из базы данных, поэтому вам лучше быстро и без сбоев передавать грязные данные. Помогают и транзакции.

2 голосов
/ 12 сентября 2009

Способ сделать это - написать хранимую процедуру и использовать TRY и CATCH. Используйте RAISERROR, чтобы вызвать свои собственные сообщения, и проверьте код ошибки в SqlException.

1 голос
/ 14 сентября 2009

Сообщения об ошибках не равняются исключениям. Сообщение об ошибке - это то, что пользователь должен найти информативным и наиболее важным actionable . В User Experience Guidelines есть некоторые рекомендации по сообщениям об ошибках. У Apple также есть хорошие общие рекомендации по написанию хороших предупреждающих сообщений .

Вы сразу заметите, что большинство, если не все, ошибки SQL являются , а не хорошими сообщениями конечного пользователя. 'Ограничение FKXF # 455 нарушение' - ошибка для конечного пользователя. «Filegroup is full» - ошибка для конечного пользователя. «Тупик» - тоже самое. Что хорошие приложения делают, они разделяют роли пользователей. Администраторы должны видеть эти ошибки, а не конечные пользователи. Таким образом, приложение всегда регистрирует полную ошибку SQL со всеми подробностями, в конечном итоге уведомляет администратора, а затем отображает пользователю другую ошибку, что-то вроде «Произошла системная ошибка, администратор был уведомлен».

Если ошибка SQL может быть действительной для конечного пользователя, то вы можете отобразить ему сообщение об ошибке, сообщив ему, что делать, чтобы решить проблему (например, изменить дату счета-фактуры во входных данных, чтобы удовлетворить ограничение). Но даже в этом случае в большинстве случаев вы не должны отображать ошибку SQL прямо для пользователя (вы уже видели очень вескую причину, почему нет: локализация). Я понимаю, что это создает гораздо большую нагрузку для вашей команды разработчиков. Вы должны знать, какие ошибки являются действиями пользователя в каждом случае, из множества ошибок, которые вы можете обнаружить. Это хорошо известно, и именно поэтому хорошие менеджеры программ знают, что около 80% кода обрабатывает случаи ошибок, и почему «готовое» приложение обычно означает, что оно выполнено на 20%. Вот что отличает отличные приложения от обычных: как они ведут себя, когда дела идут неправильно .

Я предлагаю начать с принципа прогрессивного раскрытия . Вывести общее сообщение об ошибке, в котором говорится, что «операция не удалась». Предлагать отображать больше деталей, если пользователь нажимает кнопку «Показать подробности ...» в диалоговом окне сообщения об ошибке, и отображать коллекцию SqlError (кстати, вы должны всегда записывать и отображать весь SqlError collection of SqlException.Errors , а не SqlException). Используйте свойство SqlError.Number , чтобы добавить логику в блок перехвата, который решает, может ли пользователь что-либо сделать с ошибкой (решить, является ли ошибка допустимой) и добавляет соответствующую информацию.

К сожалению, здесь нет пикси-пыли. Вы дотрагиваетесь до ног, вероятно, самой сложной части вашего проекта.

1 голос
/ 12 сентября 2009

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

Я склонен классифицировать ошибки как ожидаемые или непредвиденные. Если вы можете предвидеть ошибку и , которую вы хотите обработать с помощью четкого сообщения о приеме на работу, например, в ситуации «удалить сотрудника», вам придется планировать и кодировать соответственно. По их определению, вы не можете сделать это из-за непредвиденных ошибок - это обычно, когда приходит TRY / CATCH.

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

Альтернативой, основанной на TRY ... CATCH ..., будет проверка номера ошибки в блоке catch. Если это «невозможно удалить из-за внешнего ключа», вы можете запросить дочерние таблицы и сгенерировать сообщение, а если бы это была какая-то другая непредвиденная ошибка, вам пришлось бы использовать общее сообщение.

(Предостережение: иногда, когда он сталкивается с ошибкой, SQL выдает два сообщения об ошибке подряд [является ли нарушение ограничения FK одним из них?], И в этих ситуациях различные функции ERROR () возвращают данные только для второе и неизменно менее полезное сообщение. Это невероятно отягчает, но с этим ничего не поделаешь.)

1 голос
/ 12 сентября 2009

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

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

Псевдо:

Employee e;
try {

   IEnumerable<Team> teams = Team.GetTeamsByEmployee(e);
   if (teams.Count() > 0) {
       throw new Exception("Employee is a part of team "+ String.Join(",", teams.Select(o => o.Name).ToArray());
   }

   IEnumerable<Employee> managingEmployees = Employee.GetEmployeesImManaging(e);
   if (managingEmployees.Count() > 0) {
       throw new Exception("Employee is manager for the following employees "+ String.Join(",", managingEmployees.Select(o => o.Name).ToArray());
   }

   Employee.Delete(e);
} catch (Exception e) {
   // write out e
}
1 голос
/ 12 сентября 2009

Мы обычно пишем какой-то переводчик в наших проектах. Мы сравниваем сообщение об исключении SQL с некоторыми предопределенными шаблонами и показываем эквивалентное сообщение пользователю

0 голосов
/ 28 марта 2016

В заключение я бы предостерег от использования исключений и ошибок SQL. Для меня, если мы полагаемся на такие ошибки, мы накапливаем неприятности. Такие сообщения об ошибках обычно не читаются пользователем. Кроме того, разработчики пользовательского интерфейса могут сказать: «Хорошо, ребята из базы данных поймают меня, мне не нужно это проверять». Совершенно неверно!

Возможно, лучшим подходом было бы убедиться, что проверка предотвращает эти проблемы в первую очередь. например Если вы не можете удалить объект A, потому что на него ссылается объект B, тогда мы должны реализовать какую-то службу зависимости на уровне выше, чем база данных. Это зависит от того, какое приложение это.

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

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

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

Не существует подхода, который подходит всем. Мой девиз здесь будет профилактика лучше лечения! Подтвердите раньше, чем позже.

...