web-dev-qa-db-fra.com

Dois-je vérifier si quelque chose existe dans la base de données et échouer rapidement ou attendre une exception db

Avoir deux classes:

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

public class Child { ... }

Lors de l'attribution de ChildId à Parent dois-je d'abord vérifier s'il existe dans la base de données ou attendre que la base de données lève une exception?

Par exemple (en utilisant Entity Framework Core):

[~ # ~] note [~ # ~] ces types de contrôles sont PARTOUT SUR INTERNET même sur les documents officiels de Microsoft: https://docs.Microsoft.com/en-us/aspnet/mvc/overview/getting-started/getting-started-with-ef-using- mvc/handling-concurrency-with-the-entity-framework-in-an-asp-net-mvc-application # modify-the-department-controller mais il existe une gestion des exceptions supplémentaire pour SaveChanges

notez également que l'objectif principal de cette vérification était de renvoyer un message convivial et un état HTTP connu à l'utilisateur de l'API et de ne pas ignorer complètement les exceptions de base de données. Et le seul endroit où une exception est levée est à l'intérieur de SaveChanges ou SaveChangesAsync appel ... donc il n'y aura pas d'exception lorsque vous appelez FindAsync ou Any . Donc, si l'enfant existe mais a été supprimé avant SaveChangesAsync, une exception de concurrence sera levée.

Je l'ai fait parce que foreign key violation l'exception sera beaucoup plus difficile à formater pour afficher "L'enfant avec l'ID {parent.ChildId} est introuvable."

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

contre:

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

Le deuxième exemple de Postgres lancera 23503 foreign_key_violation erreur: https://www.postgresql.org/docs/9.4/static/errcodes-appendix.html

L'inconvénient de gérer les exceptions de cette façon dans ORM comme EF est qu'il ne fonctionnera qu'avec un backend de base de données spécifique. Si vous avez déjà voulu passer à SQL Server ou autre chose, cela ne fonctionnera plus car le code d'erreur changera.

Ne pas formater correctement l'exception pour l'utilisateur final pourrait exposer certaines choses que vous ne voulez que les développeurs voient.

En relation:

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

32
Konrad

Plutôt une question confuse, mais OUI vous devez d'abord vérifier et ne pas simplement gérer une exception de base de données.

Tout d'abord, dans votre exemple, vous êtes au niveau de la couche de données, en utilisant EF directement sur la base de données pour exécuter SQL. Votre code équivaut à courir

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

L'alternative que vous proposez est:

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

L'utilisation de l'exception pour exécuter la logique conditionnelle est lente et universellement désapprouvée.

Vous avez une condition de concurrence et devez utiliser une transaction. Mais cela peut être entièrement fait dans le code.

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

L'essentiel est de se demander:

"Vous attendez-vous à ce que cette situation se produise?"

Si ce n'est pas le cas, assurez-vous d'insérer et de lever une exception. Mais il suffit de gérer l'exception comme toute autre erreur qui pourrait se produire.

Si vous vous attendez à ce que cela se produise, ce n'est PAS exceptionnel et vous devriez vérifier si l'enfant existe d'abord, en répondant avec le message amical approprié si ce n'est pas le cas.

Edit - Il y a beaucoup de controverse à ce sujet, semble-t-il. Avant de downvote, pensez à:

A. Et s'il y avait deux contraintes FK. Recommanderiez-vous d'analyser le message d'exception pour déterminer quel objet manquait?

B. En cas d'erreur, une seule instruction SQL est exécutée. Ce ne sont que les hits qui entraînent les frais supplémentaires d'une seconde requête.

C. Habituellement, Id serait une clé de substitution, il est difficile d'imaginer une situation où vous en connaissez une et vous n'êtes pas sûr que ce soit sur la base de données. Vérifier serait étrange. Mais que faire si c'est une clé naturelle que l'utilisateur a tapée? Cela pourrait avoir une forte chance de ne pas être présent

4
Ewan

Vérifier l'unicité, puis définir est un contre-modèle; il peut toujours arriver que l'ID soit inséré simultanément entre l'heure de vérification et l'heure d'écriture. Les bases de données sont équipées pour traiter ce problème grâce à des mécanismes tels que les contraintes et les transactions; la plupart des langages de programmation ne le sont pas. Par conséquent, si vous appréciez la cohérence des données, laissez-le à l'expert (la base de données), c'est-à-dire effectuez l'insertion et interceptez une exception si elle se produit.

111
Kilian Foth

Je pense que ce que vous appelez "échouer rapidement" et ce que j'appelle ce n'est pas la même chose.

Dire à la base de données d'effectuer un changement et gérer l'échec, que est rapide. Votre chemin est compliqué, lent et peu fiable.

Cette technique n'est pas un échec rapide, c'est un "contrôle en amont". Il y a parfois de bonnes raisons, mais pas lorsque vous utilisez une base de données.

38
gnasher729

Cela a commencé comme un commentaire mais est devenu trop important.

Non, comme les autres réponses l'ont indiqué, ce modèle ne doit pas être utilisé. *

Lorsqu'il s'agit de systèmes qui utilisent des composants asynchrones, il y aura toujours une condition de concurrence critique dans laquelle la base de données (ou le système de fichiers ou un autre système asynchrone) peut changer entre la vérification et la modification. Une vérification de ce type n'est tout simplement pas un moyen fiable d'éviter le type d'erreur que vous ne souhaitez pas gérer.
Pire que de ne pas être suffisant, en un coup d'œil, cela donne l'impression qu'il devrait empêcher l'erreur d'enregistrement en double donnant un faux sentiment de sécurité .

Vous avez quand même besoin de la gestion des erreurs.

Dans les commentaires, vous avez demandé si vous avez besoin de données provenant de plusieurs sources.
Toujours pas.

Le problème fondamental ne disparaît pas si ce que vous voulez vérifier devient plus complexe.

Vous avez quand même toujours besoin de la gestion des erreurs.

Même si cette vérification était un moyen fiable de prévenir l'erreur particulière contre laquelle vous essayez de vous prémunir, d'autres erreurs peuvent toujours se produire. Que se passe-t-il si vous perdez la connexion à la base de données, si elle manque d'espace, ou?

Vous avez très probablement encore besoin d'autres gestion des erreurs liées à la base de données. La gestion de cette erreur particulière devrait probablement en être un petit morceau.

Si vous avez besoin de données pour déterminer quoi changer, vous devrez évidemment les collecter quelque part. (selon les outils que vous utilisez, il existe probablement de meilleurs moyens que des requêtes distinctes pour les collecter) Si, en examinant les données que vous avez collectées, vous déterminez que vous n'avez pas besoin de faire le changement après tout, super, ne faites pas le changement. Cette détermination est complètement distincte des problèmes de gestion des erreurs.

Vous avez quand même toujours besoin de la gestion des erreurs.

Je sais que je suis répétitif, mais je pense qu'il est important de clarifier les choses. J'ai déjà nettoyé ce gâchis.

Il échouera finalement. En cas d'échec, il sera difficile et long de se rendre au fond des choses. Il est difficile de résoudre les problèmes liés aux conditions de course. Ils ne se produisent pas de manière cohérente, il sera donc difficile, voire impossible, de se reproduire isolément. Vous n'avez pas mis la gestion des erreurs appropriée au début, donc vous n'aurez probablement pas grand-chose à faire: peut-être le rapport d'un utilisateur final d'un texte crypté (que vous essayiez d'empêcher de voir en premier lieu.) Peut-être qu'une trace de pile qui pointe vers cette fonction qui, lorsque vous la regardez, nie ouvertement l'erreur devrait même être possible.

* Il peut y avoir des raisons commerciales valables d'effectuer ces vérifications existantes, par exemple pour empêcher l'application de dupliquer un travail coûteux, mais ce n'est pas un remplacement approprié pour une gestion correcte des erreurs.

15
Mr.Mindor

Je pense qu'une chose secondaire à noter ici - l'une des raisons pour lesquelles vous voulez que ce soit pour que vous puissiez formater un message d'erreur pour que l'utilisateur puisse le voir.

Je vous recommande vivement:

a) montrer à l'utilisateur final le même message d'erreur générique pour l'erreur every qui se produit.

b) enregistrer l'exception réelle quelque part à laquelle seuls les développeurs peuvent accéder (si sur un serveur) ou quelque part qui peut vous être envoyé par des outils de rapport d'erreurs (si le client est déployé)

c) n'essayez pas de formater les détails de l'exception d'erreur que vous enregistrez, sauf si vous pouvez ajouter des informations plus utiles. Vous ne voulez pas avoir accidentellement "formaté" la seule information utile que vous auriez pu utiliser pour suivre un problème.


En bref - les exceptions regorgent d'informations techniques très utiles. Rien de tout cela ne devrait être pour l'utilisateur final et vous perdez ces informations à vos risques et périls.

2
Paddy