Comment puis-je empêcher Entity Framework d'essayer d'enregistrer / d'insérer des objets enfants?

100

Lorsque j'enregistre une entité avec un cadre d'entité, j'ai naturellement supposé qu'il essaierait uniquement de sauvegarder l'entité spécifiée. Cependant, il essaie également de sauvegarder les entités enfants de cette entité. Cela pose toutes sortes de problèmes d'intégrité. Comment forcer EF à enregistrer uniquement l'entité que je souhaite enregistrer et donc à ignorer tous les objets enfants?

Si je définis manuellement les propriétés sur null, j'obtiens une erreur "L'opération a échoué: la relation n'a pas pu être modifiée car une ou plusieurs propriétés de clé étrangère ne peuvent pas être nulles." Ceci est extrêmement contre-productif car j'ai défini l'objet enfant sur null spécifiquement afin qu'EF le laisse seul.

Pourquoi est-ce que je ne veux pas enregistrer / insérer les objets enfants?

Étant donné que cela est discuté dans les deux sens dans les commentaires, je vais vous expliquer pourquoi je veux que mes objets enfants soient laissés seuls.

Dans l'application que je construis, le modèle d'objet EF n'est pas chargé à partir de la base de données, mais utilisé comme objets de données que je remplis lors de l'analyse d'un fichier plat. Dans le cas des objets enfants, beaucoup d'entre eux font référence à des tables de recherche définissant diverses propriétés de la table parent. Par exemple, l'emplacement géographique de l'entité principale.

Depuis que j'ai rempli ces objets moi-même, EF suppose qu'il s'agit de nouveaux objets et qu'ils doivent être insérés avec l'objet parent. Cependant, ces définitions existent déjà et je ne souhaite pas créer de doublons dans la base de données. J'utilise uniquement l'objet EF pour effectuer une recherche et remplir la clé étrangère dans mon entité de table principale.

Même avec les objets enfants qui sont des données réelles, je dois d'abord enregistrer le parent et obtenir une clé primaire ou EF semble juste faire un gâchis. J'espère que cela donne une explication.

Mark Micallef
la source
Autant que je sache, vous devrez annuler les objets enfants.
Johan
Salut Johan. Ça ne marche pas. Il jette des erreurs si j'annule la collection. Selon la façon dont je le fais, il se plaint que les clés sont nulles ou que ma collection a été modifiée. De toute évidence, ces choses sont vraies, mais je l'ai fait exprès pour laisser les objets qu'il n'est pas censé toucher.
Mark Micallef
Euphorique, c'est complètement inutile.
Mark Micallef
@Euphoric Même en ne modifiant pas les objets enfants, EF tente toujours de les insérer par défaut et de ne pas les ignorer ni de les mettre à jour.
Johan
Ce qui m'ennuie vraiment, c'est que si je fais tout mon possible pour annuler réellement ces objets, il se plaint alors plutôt que de réaliser que je veux qu'il les laisse tranquilles. Étant donné que ces objets enfants sont tous facultatifs (nullables dans la base de données), existe-t-il un moyen de forcer EF à oublier que j'avais ces objets? ie purger son contexte ou cache en quelque sorte?
Mark Micallef

Réponses:

55

Autant que je sache, vous avez deux options.

Option 1)

Null tous les objets enfants, cela garantira à EF de ne rien ajouter. Il ne supprimera pas non plus quoi que ce soit de votre base de données.

Option 2)

Définissez les objets enfants comme détachés du contexte à l'aide du code suivant

 context.Entry(yourObject).State = EntityState.Detached

Notez que vous ne pouvez pas détacher un List/ Collection. Vous devrez parcourir votre liste et détacher chaque élément de votre liste comme ceci

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
Johan
la source
Salut Johan, j'ai essayé de détacher l'une des collections et cela a généré l'erreur suivante: Le type d'entité HashSet`1 ne fait pas partie du modèle pour le contexte actuel.
Mark Micallef
@MarkyMark Ne détachez pas la collection. Vous devrez boucler sur la collection et la détacher objet par objet (je vais mettre à jour ma réponse maintenant).
Johan
11
Malheureusement, même avec une liste de boucles pour tout détacher, EF semble toujours essayer de s'insérer dans certaines des tables associées. À ce stade, je suis prêt à extraire EF et à revenir à SQL qui au moins se comporte de manière raisonnable. Quelle douleur.
Mark Micallef
2
Puis-je utiliser context.Entry (yourObject) .State avant même de l'ajouter?
Thomas Klammer
1
@ mirind4 Je n'ai pas utilisé l'état. Si j'insère un objet, je m'assure que tous les enfants sont nuls. Lors de la mise à jour, j'obtiens d'abord l'objet sans les enfants.
Thomas Klammer
41

Pour faire court: utilisez la clé étrangère et cela vous sauvera la journée.

Supposons que vous ayez une entité École et une entité Ville , et il s'agit d'une relation plusieurs-à-un où une Ville a plusieurs Écoles et une École appartient à une Ville. Et supposons que les villes existent déjà dans la table de recherche afin que vous ne vouliez PAS qu'elles soient à nouveau insérées lors de l'insertion d'une nouvelle école.

Au départ, vous pouvez définir vos entités comme ceci:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

Et vous pouvez faire l' insertion de l' école comme ceci (supposons que vous avez déjà attribué la propriété City au newItem ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

L'approche ci-dessus peut parfaitement fonctionner dans ce cas, cependant, je préfère l' approche de clé étrangère qui pour moi est plus claire et flexible. Voir la solution mise à jour ci-dessous:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

De cette manière, vous définissez explicitement que l' école a une clé étrangère City_Id et qu'elle fait référence à l' entité City . Donc quand il s'agit de l'insertion de l' école , vous pouvez faire:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

Dans ce cas, vous spécifiez explicitement le City_Id du nouvel enregistrement et supprimez la ville du graphique afin qu'EF ne prenne pas la peine de l'ajouter au contexte avec School .

Bien qu'à la première impression, l'approche de la clé étrangère semble plus compliquée, mais croyez-moi, cette mentalité vous fera gagner beaucoup de temps lorsqu'il s'agira d'insérer une relation plusieurs-à-plusieurs (imaginer que vous avez une relation école-étudiant, et l'étudiant a une propriété City) et ainsi de suite.

J'espère que ça te sera utile.

lenglei
la source
Excellente réponse, m'a beaucoup aidé! Mais ne serait-il pas préférable d'indiquer la valeur minimale de 1 pour City_Id à l'aide de l' [Range(1, int.MaxValue)]attribut?
Dan Rayson
Comme un rêve!! Mille mercis!
CJH
Cela supprimerait la valeur de l'objet School dans l'appelant. SaveChanges recharge-t-il la valeur des propriétés de navigation nulles? Sinon, l'appelant doit recharger l'objet City après avoir appelé la méthode Insert () s'il a besoin de ces informations. C'est le modèle que j'ai souvent utilisé, mais je suis toujours ouvert à de meilleurs modèles si quelqu'un en a un bon.
Déséquilibré
1
Cela m'a vraiment aidé. EntityState.Unchaged est exactement ce dont j'avais besoin pour attribuer un objet qui représente une clé étrangère de table de consultation dans un grand graphique d'objets que j'enregistrais en une seule transaction. EF Core jette un message d'erreur moins qu'intuitif IMO. J'ai mis en cache mes tables de recherche qui changent rarement pour des raisons de performances. Vous supposez que, parce que le PK de l'objet de table de recherche est le même que ce qui se trouve déjà dans la base de données, il sait qu'il est inchangé et qu'il doit simplement affecter le FK à l'élément existant. Au lieu d'essayer d'insérer l'objet de table de recherche comme nouveau.
tnk479 le
Aucune combinaison d'annulation ou de définition de l'état inchangé ne fonctionne pour moi. EF insiste sur le fait qu'il doit insérer un nouvel enregistrement enfant lorsque je souhaite simplement mettre à jour un champ scalaire dans le parent.
BobRz
21

Si vous souhaitez simplement stocker les modifications apportées à un objet parent et éviter de stocker les modifications apportées à l'un de ses objets enfants , alors pourquoi ne pas procéder comme suit:

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

La première ligne attache l'objet parent et l'ensemble du graphique de ses objets enfants dépendants au contexte en Unchangedétat.

La deuxième ligne modifie l'état de l'objet parent uniquement, laissant ses enfants dans l' Unchangedétat.

Notez que j'utilise un contexte nouvellement créé, donc cela évite d'enregistrer d'autres modifications dans la base de données.

keykey76
la source
2
Cette réponse a l'avantage que si quelqu'un vient plus tard et ajoute un objet enfant, elle ne cassera pas le code existant. Il s'agit d'une solution «opt in» où les autres vous obligent à exclure explicitement les objets enfants.
Jim
Cela ne fonctionne plus dans le noyau. «Joindre: attache chaque entité accessible, sauf lorsqu'une entité accessible a une clé générée par le magasin et qu'aucune valeur de clé n'est attribuée; celles-ci seront marquées comme ajoutées.» Si les enfants sont nouveaux, ils seront ajoutés.
mmix
14

L'une des solutions suggérées consiste à affecter la propriété de navigation à partir du même contexte de base de données. Dans cette solution, la propriété de navigation attribuée depuis l'extérieur du contexte de base de données serait remplacée. Veuillez consulter l'exemple suivant à titre d'illustration.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Enregistrement dans la base de données:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft enrôle cela comme une fonctionnalité, mais je trouve cela ennuyeux. Si l'objet de service associé à l'objet société a un ID qui existe déjà dans la base de données, pourquoi EF n'associe-t-il pas simplement l'objet société à l'objet de base de données? Pourquoi devrions-nous prendre soin de l'association par nous-mêmes? Prendre soin de la propriété de navigation lors de l'ajout d'un nouvel objet revient à déplacer les opérations de base de données de SQL vers C #, ce qui est fastidieux pour les développeurs.

Bikash Bishwokarma
la source
Je suis d'accord à 100%, il est ridicule qu'EF tente de créer un nouvel enregistrement lorsque l'ID est fourni. S'il y avait un moyen de remplir les options de la liste de sélection avec l'objet réel, nous serions en affaires.
T3.0
10

Vous devez d'abord savoir qu'il existe deux façons de mettre à jour l'entité dans EF.

  • Objets attachés

Lorsque vous modifiez la relation des objets attachés au contexte d'objet à l'aide de l'une des méthodes décrites ci-dessus, Entity Framework doit conserver les clés étrangères, les références et les collections synchronisées.

  • Objets déconnectés

Si vous travaillez avec des objets déconnectés, vous devez gérer manuellement la synchronisation.

Dans l'application que je construis, le modèle d'objet EF n'est pas chargé à partir de la base de données, mais utilisé comme objets de données que je remplis lors de l'analyse d'un fichier plat.

Cela signifie que vous travaillez avec un objet déconnecté, mais il n'est pas clair si vous utilisez une association indépendante ou une association de clé étrangère .

  • Ajouter

    Lors de l'ajout d'une nouvelle entité avec un objet enfant existant (objet qui existe dans la base de données), si l'objet enfant n'est pas suivi par EF, l'objet enfant sera réinséré. Sauf si vous attachez d'abord manuellement l'objet enfant.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Mettre à jour

    Vous pouvez simplement marquer l'entité comme modifiée, puis toutes les propriétés scalaires seront mises à jour et les propriétés de navigation seront simplement ignorées.

      db.Entity(entity).State = EntityState.Modified;

Graphique Diff

Si vous souhaitez simplifier le code lorsque vous travaillez avec un objet déconnecté, vous pouvez essayer de représenter graphiquement la bibliothèque de différences .

Voici l'introduction, Présentation de GraphDiff pour Entity Framework Code First - Permettre les mises à jour automatiques d'un graphique d'entités détachées .

Exemple de code

  • Insérez l'entité si elle n'existe pas, sinon mettez à jour.

      db.UpdateGraph(entity);
  • Insérez l'entité si elle n'existe pas, sinon mettez à jour ET insérez l'objet enfant s'il n'existe pas, sinon mettez à jour.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
Yuliam Chandra
la source
3

La meilleure façon de le faire est de remplacer la fonction SaveChanges dans votre datacontext.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }
Wouter Schut
la source
2

J'ai le même problème lorsque j'essaie de sauvegarder le profil, je table déjà des saluts et des nouveaux pour créer un profil. Lorsque j'insère un profil, il s'insère également dans la salutation. J'ai donc essayé comme ça avant savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;

ZweHtatNaing
la source
1

Cela a fonctionné pour moi:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;
Klaus
la source
0

Ce que nous avons fait, c'est avant d'ajouter le parent au dbset, déconnecter les collections enfants du parent, en veillant à pousser les collections existantes vers d'autres variables pour permettre de les utiliser plus tard, puis en remplaçant les collections enfants actuelles par de nouvelles collections vides. La définition des collections enfants sur null / rien a semblé échouer pour nous. Après cela, ajoutez le parent au dbset. De cette façon, les enfants ne sont pas ajoutés tant que vous ne le souhaitez pas.

Shaggie
la source
0

Je sais que c'est un ancien message, mais si vous utilisez l'approche du code d'abord, vous pouvez obtenir le résultat souhaité en utilisant le code suivant dans votre fichier de mappage.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Cela indiquera essentiellement à EF d'exclure la propriété "ChildObjectOrCollection" du modèle afin qu'elle ne soit pas mappée à la base de données.

Syed danois
la source
Dans quel contexte "Ignorer" est-il utilisé? Il ne semble pas exister dans le contexte présumé.
T3.0
0

J'ai eu un défi similaire en utilisant Entity Framework Core 3.1.0, ma logique de dépôt est assez générique.

Cela a fonctionné pour moi:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Veuillez noter que "ParentEntityId" est le nom de la colonne de clé étrangère sur l'entité enfant. J'ai ajouté la ligne de code mentionnée ci-dessus sur cette méthode:

protected override void OnModelCreating(ModelBuilder builder)...
Manelisi Nodada
la source