Tendo duas classes:
public class Parent
{
public int Id { get; set; }
public int ChildId { get; set; }
}
public class Child { ... }
Ao atribuir ChildId
a Parent
, devo verificar primeiro se ele existe no banco de dados ou aguardar que o banco de dados gere uma exceção?
Por exemplo (usando o Entity Framework Core):
OBSERVAÇÃO: esse tipo de verificação é POR TODA A INTERNET, mesmo nos documentos oficiais da Microsoft: https://docs.microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using- mvc / manipulação-simultaneidade-com-a-entidade-estrutura-em-um-asp-net-mvc-application # modify-the-department-controller mas há tratamento de exceção adicional paraSaveChanges
Além disso, observe que o principal objetivo dessa verificação era retornar mensagens amigáveis e status HTTP conhecido ao usuário da API e não ignorar completamente as exceções do banco de dados. E a única exceção lançada é dentro SaveChanges
ou SaveChangesAsync
chamada ... para que não haja nenhuma exceção quando você ligar FindAsync
ou Any
. Portanto, se o filho existir, mas foi excluído antes SaveChangesAsync
, a exceção de simultaneidade será lançada.
Fiz isso devido ao fato de que a foreign key violation
exceção será muito mais difícil de formatar para exibir "Não foi possível encontrar o filho com o 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;
}
versus:
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;
}
O segundo exemplo no Postgres gerará um 23503 foreign_key_violation
erro: https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html
A desvantagem de lidar com exceções dessa maneira no ORM, como o EF, é que ele funcionará apenas com um back-end de banco de dados específico. Se você quiser mudar para o servidor SQL ou algo mais, isso não funcionará mais porque o código de erro será alterado.
Não formatar a exceção corretamente para o usuário final pode expor algumas coisas que você não quer que ninguém, exceto os desenvolvedores, vejam.
Relacionado:
https://stackoverflow.com/questions/308905/should-there-be-a-transaction-for-read-queries
fonte
Child with id {parent.ChildId} could not be found.
. E formatação "Violação de chave estrangeira" eu acho que é pior neste caso.Respostas:
Antes, uma pergunta confusa, mas SIM, você deve verificar primeiro e não apenas lidar com uma exceção de banco de dados.
Primeiro de tudo, no seu exemplo, você está na camada de dados, usando EF diretamente no banco de dados para executar o SQL. Seu código é equivalente a execução
A alternativa que você está sugerindo é:
Usar a exceção para executar lógica condicional é lento e universalmente desaprovado.
Você tem uma condição de corrida e deve usar uma transação. Mas isso pode ser totalmente feito em código.
O importante é se perguntar:
Caso contrário, certifique-se de inserir e lançar uma exceção. Mas lide com a exceção como qualquer outro erro que possa ocorrer.
Se você espera que ocorra, NÃO é excepcional e deve verificar se a criança existe primeiro, respondendo com a mensagem amigável apropriada, se não existir.
Editar - Parece haver muita controvérsia sobre isso. Antes de reduzir o voto, considere:
A. E se houvesse duas restrições de FK. Você recomendaria analisar a mensagem de exceção para descobrir qual objeto estava faltando?
B. Se você tiver alguma falta, apenas uma instrução SQL será executada. São apenas os hits que incorrem na despesa extra de uma segunda consulta.
C. Normalmente, Id seria uma chave substituta. É difícil imaginar uma situação em que você conhece uma e não tem certeza de que está no banco de dados. Verificar seria estranho. Mas e se for uma chave natural que o usuário digitou? Isso pode ter uma grande chance de não estar presente
fonte
Verificar a exclusividade e depois definir é um antipadrão; sempre pode acontecer que o ID seja inserido simultaneamente entre o tempo de verificação e o tempo de gravação. Os bancos de dados estão equipados para lidar com esse problema por meio de mecanismos como restrições e transações; a maioria das linguagens de programação não é. Portanto, se você valoriza a consistência dos dados, deixe-o com o especialista (o banco de dados), ou seja, faça a inserção e capture uma exceção, se ocorrer.
fonte
Eu acho que o que você chama de “falha rápido” e o que eu chamo de não é o mesmo.
Dizer a base de dados para fazer uma mudança e lidar com o fracasso, que é rápido. Seu caminho é complicado, lento e não é particularmente confiável.
Essa técnica não é rápida, é "pré-comprovante". Às vezes, existem boas razões, mas não quando você usa um banco de dados.
fonte
Isso começou como um comentário, mas cresceu muito.
Não, como as outras respostas afirmaram, esse padrão não deve ser usado. *
Ao lidar com sistemas que usam componentes assíncronos, sempre haverá uma condição de corrida em que o banco de dados (ou sistema de arquivos ou outro sistema assíncrono) pode mudar entre a verificação e a alteração. Uma verificação desse tipo simplesmente não é uma maneira confiável de impedir o tipo de erro que você não deseja tratar.
Pior do que não ser suficiente, de imediato, dá a impressão de que deve evitar o erro de registro duplicado, dando uma falsa sensação de segurança.
Você precisa do tratamento de erros de qualquer maneira.
Nos comentários, você perguntou o que fazer se precisar de dados de várias fontes.
Ainda não.
A questão fundamental não desaparece se o que você deseja verificar se tornar mais complexo.
Você ainda precisa do tratamento de erros de qualquer maneira.
Mesmo se essa verificação for uma maneira confiável de impedir o erro específico que você está tentando proteger, outros erros ainda poderão ocorrer. O que acontece se você perder a conexão com o banco de dados ou ficar sem espaço ou?
Você provavelmente ainda precisará de outro tratamento de erros relacionado ao banco de dados. O tratamento desse erro específico provavelmente deve ser um pequeno pedaço dele.
Se você precisar de dados para determinar o que alterar, obviamente precisará coletá-los de algum lugar. (dependendo de quais ferramentas você está usando, provavelmente há maneiras melhores do que consultas separadas para coletá-las) Se, ao examinar os dados coletados, você determinar que não precisa fazer a alteração, ótimo, não faça o mudança. Essa determinação é completamente separada das preocupações de manipulação de erros.
Você ainda precisa do tratamento de erros de qualquer maneira.
Sei que estou sendo repetitivo, mas sinto que é importante deixar isso claro. Eu limpei essa bagunça antes.
Eventualmente falhará. Quando falhar, será difícil e demorado chegar ao fundo. Resolver problemas que surgem das condições de corrida é difícil. Eles não acontecem de forma consistente, por isso será difícil ou até impossível de reproduzir isoladamente. Você não aplicou o tratamento adequado de erros para começar, portanto provavelmente não terá muito o que continuar: talvez um relatório do usuário final sobre algum texto enigmático (ei, você estava tentando impedir de ver em primeiro lugar). Talvez um rastreamento de pilha que aponte para essa função que, quando você a observe, negue descaradamente, o erro deve ser possível.
* Pode haver razões comerciais válidas para executar essas verificações existentes, como impedir o aplicativo de duplicar trabalhos caros, mas não é um substituto adequado para o tratamento adequado de erros.
fonte
Penso que uma coisa secundária a ser observada aqui - uma das razões pelas quais você deseja isso é para que você possa formatar uma mensagem de erro para o usuário ver.
Eu recomendaria sinceramente que você:
a) mostre ao usuário final a mesma mensagem de erro genérica para cada erro que ocorrer.
b) registre a exceção real em algum lugar que somente os desenvolvedores possam acessar (se estiver em um servidor) ou em algum lugar que possa ser enviado a você por ferramentas de relatório de erros (se o cliente estiver implantado)
c) não tente formatar os detalhes da exceção de erro registrados, a menos que possa adicionar informações mais úteis. Você não deseja acidentalmente 'formatar' a única informação útil que você poderia usar para rastrear um problema.
Em resumo - as exceções estão cheias de informações técnicas muito úteis. Nada disso deve ser para o usuário final e você perde essas informações por sua conta e risco.
fonte