Devo verificar se existe algo no banco de dados e falhar rapidamente ou aguardar a exceção do banco de dados

32

Tendo duas classes:

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

public class Child { ... }

Ao atribuir ChildIda 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 SaveChangesou SaveChangesAsyncchamada ... para que não haja nenhuma exceção quando você ligar FindAsyncou 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 violationexceçã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_violationerro: 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/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
fonte
2
Compartilhar sua pesquisa ajuda a todos . Conte-nos o que você tentou e por que ele não atendeu às suas necessidades. Isso demonstra que você reservou um tempo para tentar ajudar a si mesmo, evita reiterar respostas óbvias e, acima de tudo, ajuda a obter uma resposta mais específica e relevante. Veja também How to Ask
gnat
5
Como outros já mencionaram, existe a possibilidade de um registro poder ser inserido ou excluído simultaneamente com a verificação de NotFound. Por esse motivo, verificar primeiro parece uma solução inaceitável. Se você está preocupado em escrever um tratamento de exceção específico do Postgres que não seja portátil para outros back-ends de banco de dados, tente estruturar o manipulador de exceções de forma que a funcionalidade principal possa ser estendida por classes específicas de banco de dados (SQL, Postgres, etc)
billrichards
3
Examinando os comentários, preciso dizer o seguinte: pare de pensar em banalidades . "Falhar rápido" não é uma regra isolada e fora do contexto que pode ou deve ser seguida cegamente. É uma regra de ouro. Sempre analise o que você está realmente tentando alcançar e, em seguida, considere qualquer técnica à luz de se isso ajuda ou não a atingir esse objetivo. "Fail fast" ajuda a evitar efeitos colaterais indesejados. Além disso, "falhar rápido" realmente significa "falhar assim que você puder detectar que há um problema". Ambas as técnicas falham assim que um problema é detectado, portanto, você deve considerar outras considerações.
precisa saber é o seguinte
1
@ Konrad, o que as exceções têm a ver com isso? Pare de pensar nas condições de corrida como algo que vive no seu código: é uma propriedade do universo. Qualquer coisa que toque em um recurso que não controla completamente (por exemplo, acesso direto à memória, memória compartilhada, banco de dados, API REST, sistema de arquivos etc.) mais de uma vez e espera que seja inalterado tem uma condição potencial de corrida. Heck, lidamos com isso em C, que nem sequer tem exceções. Apenas nunca se ramifique no estado de um recurso que você não controla se pelo menos um dos ramos mexer com o estado desse recurso.
Jared Smith
1
@ DanielPryden Na minha pergunta, eu não disse que não quero lidar com exceções de banco de dados (eu sei que exceções são inevitáveis). Acho que muitas pessoas não entenderam, eu queria ter uma mensagem de erro amigável para a minha API da Web (para que os usuários finais leiam) Child with id {parent.ChildId} could not be found.. E formatação "Violação de chave estrangeira" eu acho que é pior neste caso.
Konrad

Respostas:

3

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

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

A alternativa que você está sugerindo é:

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

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.

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;
}

O importante é se perguntar:

"Você espera que esta situação ocorra?"

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

Ewan
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Maple_shaft
1
Isso é totalmente errado e enganoso! São respostas como essa que produzem maus profissionais contra os quais sempre tenho que lutar. SELECT nunca bloqueia uma tabela; portanto, entre SELECT e INSERT, UPDATE ou DELTE, o registro pode mudar. Portanto, é péssimo desenvolvimento de software e um acidente está esperando para acontecer na produção.
Daniel Lobo
1
@DanielLobo transactioncope corrige isso
Ewan
1
testá-lo se você não acredita em mim
Ewan
1
@yusha Eu tenho o código aqui
Ewan
111

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.

Kilian Foth
fonte
34
verificar e falhar não é mais rápido do que apenas "tentar" e esperar o melhor. O Former implica 2 operações a serem implementadas e executadas pelo seu sistema e 2 pelo DB, enquanto o último implica apenas uma delas. A verificação é delegada no servidor do banco de dados. Também implica menos um salto na rede e menos uma tarefa a ser atendida pelo banco de dados. Podemos pensar que mais uma consulta ao banco de dados é acessível, mas geralmente esquecemos de pensar em grande escala. Pense em alta simultaneidade, acionando a consulta várias vezes. Poderia enganar todo o tráfego para o banco de dados. Se isso importa, cabe a você decidir.
LAIV
6
@Konrad Minha posição é que a escolha correta e padrão é uma consulta que falhará por si própria, e é a abordagem de pré-vôo de consulta separada que tem o ônus da prova para justificar a si mesma. Quanto a "tornar-se um problema": você está usando transações ou garantindo a segurança contra erros do ToCToU , certo? Não é óbvio para mim a partir do código postado que você é, mas se não é, já se tornou um problema da maneira que uma bomba-bomba se torna um problema muito antes de realmente explodir.
Mtraceur 19/09/19
4
O Konrad EF Core não colocará implicitamente o seu cheque e a inserção em uma transação; você precisará solicitá-lo explicitamente. Sem a transação, a verificação primeiro não faz sentido, pois o estado do banco de dados pode mudar entre a verificação e a inserção de qualquer maneira. Mesmo com uma transação, você pode não impedir que o banco de dados seja alterado sob seus pés. Corremos um problema há alguns anos usando o EF com Oracle, onde, embora o banco de dados o suporte, o Entity não estava acionando o bloqueio dos registros de leitura em uma transação e apenas a inserção foi tratada como transacional.
Mr.Mindor 19/09/19
3
"Verificando a singularidade e depois definindo é um antipadrão", eu não diria isso. Depende fortemente se você não pode assumir outras modificações e se a verificação produz algum resultado mais útil (mesmo apenas uma mensagem de erro que realmente significa algo para o leitor) quando ela não existe. Com um banco de dados que lida com solicitações da Web simultâneas, não, você não pode garantir que outras modificações não ocorram, mas há casos em que é uma suposição razoável.
Jpmc26 19/09/19
5
A verificação de exclusividade primeiro não elimina a necessidade de lidar com possíveis falhas. Por outro lado, se uma ação exigir a execução de várias operações, verificar se é provável que todas tenham êxito antes de iniciar uma delas é geralmente melhor do que executar ações que provavelmente precisam ser revertidas. Fazer as verificações iniciais pode não evitar todas as situações em que uma reversão seria necessária, mas poderia ajudar a reduzir a frequência desses casos.
Supercat
38

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.

gnasher729
fonte
1
Há casos em que você precisa da 2ª consulta quando uma classe depende de outra, portanto, você não tem escolha em casos como esse.
Konrad
4
Mas não aqui. E as consultas ao banco de dados podem ser bastante inteligentes, então geralmente duvido da "não escolha".
precisa saber é o seguinte
1
Eu acho que isso também depende do aplicativo, se você o criar apenas para alguns usuários, não deverá fazer diferença e o código ficará mais legível com 2 consultas.
Konrad
21
Você está assumindo que seu banco de dados está armazenando dados inconsistentes. Em outras palavras, parece que você não confia no seu banco de dados e na consistência dos dados. Se for esse o caso, você tem um problema muito grande e sua solução é uma solução. Uma solução paliativa destinada a ser anulada mais cedo ou mais tarde. Pode haver casos em que você é forçado a consumir um banco de dados fora de seu controle e gerenciamento. De outras aplicações. Nesses casos, eu consideraria essas validações. De qualquer forma, o @gnasher está certo, o seu não está falhando rápido ou não é o que entendemos como falha rápido.
LAIV
15

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.

Mr.Mindor
fonte
2

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.

Paddy
fonte
2
"mostra ao usuário final a mesma mensagem de erro genérica para todos os erros que ocorrem." que foi a principal razão, formatação exceção para olhares para o usuário final como uma coisa horrível de se fazer ..
Konrad
1
Em qualquer sistema de banco de dados razoável, você deve descobrir programaticamente por que algo falhou. Não deve ser necessário analisar uma mensagem de exceção. E de maneira mais geral: quem disse que uma mensagem de erro precisa ser exibida para o usuário? Você pode falhar na primeira inserção e tentar novamente em um loop até obter êxito (ou até algum limite de tentativas ou tempo). E, de fato, a retirada e repetição é algo que você vai querer implementar eventualmente.
Daniel Pryden 20/09/18