La relation n'a pas pu être modifiée car une ou plusieurs propriétés de clé étrangère ne peuvent pas être Nullable


192

J'obtiens cette erreur lorsque je GetById () sur une entité, puis que je définis la collection d'entités enfants sur ma nouvelle liste qui provient de la vue MVC.

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 Null. Lorsqu'une modification est apportée à une relation, la propriété de clé étrangère associée est définie sur une valeur nulle. Si la clé étrangère ne prend pas en charge les valeurs nulles, une nouvelle relation doit être définie, la propriété de clé étrangère doit se voir affecter une autre valeur non nulle ou l'objet non lié doit être supprimé.

Je ne comprends pas très bien cette ligne:

La relation n'a pas pu être modifiée car une ou plusieurs propriétés de clé étrangère ne peuvent pas être Null.

Pourquoi changerais-je la relation entre 2 entités? Il doit rester le même pendant toute la durée de vie de l'ensemble de l'application.

Le code sur lequel l'exception se produit consiste simplement à attribuer des classes enfants modifiées dans une collection à la classe parent existante. Nous espérons que cela permettrait la suppression des classes enfants, l'ajout de nouvelles classes et les modifications. J'aurais pensé qu'Entity Framework gère cela.

Les lignes de code peuvent être distillées pour:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();

J'ai trouvé ma réponse acheter en utilisant la solution n ° 2 dans l'article ci-dessous, en gros, j'ai créé ajouté une clé primaire à la table enfant pour la référence à la table parent (elle a donc 2 clés primaires (la clé étrangère pour la table parent et l'ID pour la table enfant). c-sharpcorner.com/UploadFile/ff2f08/…
yougotiger

@jaffa, j'ai trouvé ma réponse ici stackoverflow.com/questions/22858491/…
antonio

Réponses:


159

Vous devez supprimer thisParent.ChildItemsmanuellement les anciens éléments enfants un par un. Entity Framework ne fait pas cela pour vous. Il ne peut finalement pas décider ce que vous voulez faire avec les anciens éléments enfants - si vous voulez les jeter ou si vous voulez les conserver et les affecter à d'autres entités parentes. Vous devez informer Entity Framework de votre décision. Mais vous DEVEZ prendre l'une de ces deux décisions car les entités enfants ne peuvent pas vivre seules sans référence à un parent dans la base de données (en raison de la contrainte de clé étrangère). C'est essentiellement ce que dit l'exception.

Éditer

Ce que je ferais si des éléments enfants pouvaient être ajoutés, mis à jour et supprimés:

public void UpdateEntity(ParentItem parent)
{
    // Load original parent including the child item collection
    var originalParent = _dbContext.ParentItems
        .Where(p => p.ID == parent.ID)
        .Include(p => p.ChildItems)
        .SingleOrDefault();
    // We assume that the parent is still in the DB and don't check for null

    // Update scalar properties of parent,
    // can be omitted if we don't expect changes of the scalar properties
    var parentEntry = _dbContext.Entry(originalParent);
    parentEntry.CurrentValues.SetValues(parent);

    foreach (var childItem in parent.ChildItems)
    {
        var originalChildItem = originalParent.ChildItems
            .Where(c => c.ID == childItem.ID && c.ID != 0)
            .SingleOrDefault();
        // Is original child item with same ID in DB?
        if (originalChildItem != null)
        {
            // Yes -> Update scalar properties of child item
            var childEntry = _dbContext.Entry(originalChildItem);
            childEntry.CurrentValues.SetValues(childItem);
        }
        else
        {
            // No -> It's a new child item -> Insert
            childItem.ID = 0;
            originalParent.ChildItems.Add(childItem);
        }
    }

    // Don't consider the child items we have just added above.
    // (We need to make a copy of the list by using .ToList() because
    // _dbContext.ChildItems.Remove in this loop does not only delete
    // from the context but also from the child collection. Without making
    // the copy we would modify the collection we are just interating
    // through - which is forbidden and would lead to an exception.)
    foreach (var originalChildItem in
                 originalParent.ChildItems.Where(c => c.ID != 0).ToList())
    {
        // Are there child items in the DB which are NOT in the
        // new child item collection anymore?
        if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID))
            // Yes -> It's a deleted child item -> Delete
            _dbContext.ChildItems.Remove(originalChildItem);
    }

    _dbContext.SaveChanges();
}

Remarque: ceci n'est pas testé. Cela suppose que la collection d'éléments enfants est de type ICollection. (J'ai régulièrementIList et le code est un peu différent.) J'ai également supprimé toutes les abstractions du référentiel pour rester simple.

Je ne sais pas si c'est une bonne solution, mais je pense qu'un travail acharné dans ce sens doit être fait pour prendre en charge toutes sortes de changements dans la collection de navigation. Je serais également heureux de voir un moyen plus simple de le faire.


Alors que faire si certains sont seulement modifiés? Cela signifie-t-il que je dois encore les supprimer et les ajouter à nouveau?
jaffa

@Jon: Non, vous pouvez également mettre à jour des éléments existants bien sûr. J'ai ajouté un exemple de la façon dont je mettrais probablement à jour la collection enfant, voir la section Modifier ci-dessus.
Slauma le

@Slauma: Lol, si je savais que tu vas modifier ta réponse je n'écrirais pas ma réponse ...
Ladislav Mrnka

@Ladislav: Non, non, je suis heureux que vous ayez écrit votre propre réponse. Maintenant au moins, je sais que ce n'est pas complètement absurde et beaucoup trop compliqué ce que j'ai fait ci-dessus.
Slauma le

1
J'ajouterais une condition lors de la récupération de l'originalChildItem dans le foreach: ... Where (c => c.ID == childItem.ID && c.ID! = 0) sinon il renverra les enfants nouvellement ajoutés si le childItem.ID == 0.
perfect_element

116

La raison pour laquelle vous êtes confronté à cela est due à la différence entre la composition et l' agrégation .

En composition, l'objet enfant est créé lorsque le parent est créé et est détruit lorsque son parent est détruit . Sa durée de vie est donc contrôlée par son parent. Par exemple, un article de blog et ses commentaires. Si un message est supprimé, ses commentaires doivent être supprimés. Cela n'a pas de sens d'avoir des commentaires pour un article qui n'existe pas. Idem pour les commandes et les articles de commande.

Dans l'agrégation, l'objet enfant peut exister quel que soit son parent . Si le parent est détruit, l'objet enfant peut toujours exister, car il peut être ajouté ultérieurement à un autre parent. par exemple: la relation entre une playlist et les chansons de cette playlist. Si la liste de lecture est supprimée, les chansons ne doivent pas être supprimées. Ils peuvent être ajoutés à une liste de lecture différente.

La façon dont Entity Framework différencie les relations d'agrégation et de composition est la suivante:

  • Pour la composition: il s'attend à ce que l'objet enfant ait une clé primaire composite (ParentID, ChildID). C'est par conception car les identifiants des enfants doivent être à la portée de leurs parents.

  • Pour l'agrégation: il s'attend à ce que la propriété de clé étrangère dans l'objet enfant soit Nullable.

Donc, la raison pour laquelle vous rencontrez ce problème est la façon dont vous avez défini votre clé primaire dans votre table enfant. Cela devrait être composite, mais ce n'est pas le cas. Ainsi, Entity Framework considère cette association comme une agrégation, ce qui signifie que lorsque vous supprimez ou effacez les objets enfants, elle ne supprimera pas les enregistrements enfants. Il supprimera simplement l'association et définira la colonne de clé étrangère correspondante sur NULL (afin que ces enregistrements enfants puissent plus tard être associés à un parent différent). Étant donné que votre colonne n'autorise pas NULL, vous obtenez l'exception que vous avez mentionnée.

Solutions:

1- Si vous avez une bonne raison de ne pas vouloir utiliser de clé composite, vous devez supprimer explicitement les objets enfants. Et cela peut être fait plus simple que les solutions suggérées précédemment:

context.Children.RemoveRange(parent.Children);

2- Sinon, en définissant la clé primaire appropriée sur votre table enfant, votre code aura l'air plus significatif:

parent.Children.Clear();

9
J'ai trouvé cette explication très utile.
Booji Boy

7
Bonne explication de la composition par rapport à l'agrégation et de la relation entre le cadre d'entité et celui-ci.
Chrysalis

Le numéro 1 était le moins de code nécessaire pour résoudre le problème. Je vous remercie!
ryanulit

73

C'est un très gros problème. Voici ce qui se passe réellement dans votre code:

  • Vous chargez à Parentpartir de la base de données et obtenez une entité attachée
  • Vous remplacez sa collection enfant par une nouvelle collection d'enfants détachés
  • Vous enregistrez les modifications, mais pendant cette opération, tous les enfants sont considérés comme ajoutés car EF ne les connaissait pas jusqu'à présent. Ainsi EF essaie de définir null sur la clé étrangère des anciens enfants et d'insérer tous les nouveaux enfants => lignes en double.

Désormais, la solution dépend vraiment de ce que vous voulez faire et comment aimeriez-vous le faire?

Si vous utilisez ASP.NET MVC, vous pouvez essayer d'utiliser UpdateModel ou TryUpdateModel .

Si vous voulez simplement mettre à jour manuellement les enfants existants, vous pouvez simplement faire quelque chose comme:

foreach (var child in modifiedParent.ChildItems)
{
    context.Childs.Attach(child); 
    context.Entry(child).State = EntityState.Modified;
}

context.SaveChanges();

L'attachement n'est en fait pas nécessaire (définir l'état pour Modifiedattacher également l'entité) mais je l'aime parce que cela rend le processus plus évident.

Si vous souhaitez modifier l'existant, supprimer l'existant et insérer de nouveaux enfants, vous devez faire quelque chose comme:

var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well
foreach(var child in modifiedParent.ChildItems)
{
    var attachedChild = FindChild(parent, child.Id);
    if (attachedChild != null)
    {
        // Existing child - apply new values
        context.Entry(attachedChild).CurrentValues.SetValues(child);
    }
    else
    {
        // New child
        // Don't insert original object. It will attach whole detached graph
        parent.ChildItems.Add(child.Clone());
    }
}

// Now you must delete all entities present in parent.ChildItems but missing
// in modifiedParent.ChildItems
// ToList should make copy of the collection because we can't modify collection
// iterated by foreach
foreach(var child in parent.ChildItems.ToList())
{
    var detachedChild = FindChild(modifiedParent, child.Id);
    if (detachedChild == null)
    {
        parent.ChildItems.Remove(child);
        context.Childs.Remove(child); 
    }
}

context.SaveChanges();

1
Mais il y a votre remarque intéressante sur l'utilisation .Clone(). Avez-vous à l'esprit le cas où a ChildItema d'autres propriétés de navigation sous-enfant? Mais dans ce cas, ne voudrions-nous pas que tout le sous-graphe soit attaché au contexte puisque nous nous attendrions à ce que tous les sous-enfants soient de nouveaux objets si l'enfant lui-même est nouveau? (Eh bien, cela peut être différent d'un modèle à l'autre, mais supposons que les sous-enfants soient "dépendants" de l'enfant comme les enfants sont dépendants du parent.)
Slauma

Cela nécessiterait probablement un clone "intelligent".
Ladislav Mrnka

1
Et si vous ne voulez pas avoir une collection enfant dans votre contexte? http://stackoverflow.com/questions/20233994/do-i-need-to-create-a-dbset-for-every-table-so-that-i-can-persist-child-entitie
Kirsten Greed

1
parent.ChildItems.Remove (enfant); context.Childs.Remove (enfant); Cette double suppression corrigée peut poser problème, MERCI. Pourquoi avons-nous besoin des deux suppressions? Pourquoi supprimer uniquement de parent.ChildItems n'est pas suffisant puisque les enfants ne vivent que comme des enfants?
Fernando Torres

40

J'ai trouvé cette réponse beaucoup plus utile pour la même erreur. Il semble que EF ne l'aime pas lorsque vous supprimez, il préfère Supprimer.

Vous pouvez supprimer une collection d'enregistrements attachés à un enregistrement comme celui-ci.

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);

Dans l'exemple, tous les enregistrements de détail attachés à une commande ont leur état défini sur Supprimer. (En préparation de l'ajout des détails mis à jour, dans le cadre d'une mise à jour de la commande)


Je pense que c'est la bonne réponse.
desmati

solution logique et simple.
sairfan

19

Je ne sais pas pourquoi les deux autres réponses sont si populaires!

Je pense que vous aviez raison de supposer que le cadre ORM devrait le gérer - après tout, c'est ce qu'il promet de livrer. Sinon, votre modèle de domaine est corrompu par des problèmes de persistance. NHibernate gère cela avec plaisir si vous configurez correctement les paramètres de cascade. Dans Entity Framework, c'est également possible, ils s'attendent simplement à ce que vous suiviez de meilleures normes lors de la configuration de votre modèle de base de données, en particulier lorsqu'ils doivent déduire ce qui doit être fait en cascade:

Vous devez définir correctement la relation parent-enfant en utilisant une " relation d'identification ".

Si vous faites cela, Entity Framework sait que l'objet enfant est identifié par le parent et qu'il doit donc s'agir d'une situation "cascade-suppression-orphelins".

Autre que ce qui précède, vous devrez peut- être (de l'expérience NHibernate)

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

au lieu de remplacer entièrement la liste.

METTRE À JOUR

Le commentaire de @ Slauma m'a rappelé que les entités détachées sont une autre partie du problème global. Pour résoudre ce problème, vous pouvez adopter l'approche consistant à utiliser un classeur de modèles personnalisé qui construit vos modèles en essayant de le charger à partir du contexte. Ce billet de blog montre un exemple de ce que je veux dire.


La configuration comme relation d'identification n'aidera pas ici car le scénario de la question doit traiter des entités détachées ( "ma nouvelle liste qui provient de la vue MVC" ). Vous devez toujours charger les enfants d'origine de la base de données, rechercher les éléments supprimés dans cette collection en fonction de la collection détachée, puis les supprimer de la base de données. La seule différence est qu'avec une relation d'identification, vous pouvez appeler à la parent.ChildItems.Removeplace _dbContext.ChildItems.Remove. Il n'y a toujours (EF <= 6) pas de prise en charge intégrée d'EF pour éviter un code long comme celui des autres réponses.
Slauma

Je comprends votre point. Cependant, je pense qu'avec un classeur de modèle personnalisé qui charge l'entité à partir du contexte ou renvoie une nouvelle instance, l'approche ci-dessus fonctionnerait. Je mettrai à jour ma réponse pour suggérer cette solution.
Andre Luus

Oui, vous pouviez utiliser un classeur de modèles, mais vous deviez maintenant faire les choses à partir des autres réponses dans le classeur de modèles. Il déplace simplement le problème de la couche repo / service vers le classeur de modèles. Au moins, je ne vois pas de réelle simplification.
Slauma

La simplification est la suppression automatique des entités orphelines. Tout ce dont vous avez besoin dans le classeur de modèles est un équivalent générique dereturn context.Items.Find(id) ?? new Item()
Andre Luus

Bon retour pour l'équipe EF, mais la solution que vous proposez ne résout malheureusement rien dans EF land.
Chris Moschini

9

Si vous utilisez AutoMapper avec Entity Framework sur la même classe, vous pouvez rencontrer ce problème. Par exemple, si votre classe est

class A
{
    public ClassB ClassB { get; set; }
    public int ClassBId { get; set; }
}

AutoMapper.Map<A, A>(input, destination);

Cela essaiera de copier les deux propriétés. Dans ce cas, ClassBId est non Nullable. Étant donné qu'AutoMapper copiera, destination.ClassB = input.ClassB;cela posera un problème.

Définissez votre AutoMapper sur la ClassBpropriété Ignorer .

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId

Je suis confronté à un problème similaire avec AutoMapper, mais cela ne fonctionne pas pour moi: (Voir stackoverflow.com/q/41430679/613605
J86

4

J'ai juste eu la même erreur. J'ai deux tables avec une relation parent-enfant, mais j'ai configuré une "sur suppression en cascade" sur la colonne de clé étrangère dans la définition de table de la table enfant. Ainsi, lorsque je supprime manuellement la ligne parent (via SQL) dans la base de données, il supprimera automatiquement les lignes enfants.

Cependant, cela n'a pas fonctionné dans EF, l'erreur décrite dans ce fil est apparue. La raison en était que dans mon modèle de données d'entité (fichier edmx), les propriétés de l'association entre le parent et la table enfant n'étaient pas correctes. L' End1 OnDeleteoption a été configurée pour être none("End1" dans mon modèle est la fin qui a une multiplicité de 1).

J'ai changé manuellement l' End1 OnDeleteoption Cascadeet que cela a fonctionné. Je ne sais pas pourquoi EF n'est pas capable de récupérer cela, lorsque je mets à jour le modèle à partir de la base de données (j'ai un premier modèle de base de données).

Pour être complet, voici à quoi ressemble mon code à supprimer:

   public void Delete(int id)
    {
        MyType myObject = _context.MyTypes.Find(id);

        _context.MyTypes.Remove(myObject);
        _context.SaveChanges(); 
   }    

Si je n'avais pas défini de suppression en cascade, je devrais supprimer les lignes enfants manuellement avant de supprimer la ligne parent.


4

Cela se produit car l'entité enfant est marquée comme modifiée au lieu de supprimée.

Et la modification apportée par EF à l'entité enfant lors de parent.Remove(child)son exécution consiste simplement à définir la référence à son parent sur null.

Vous pouvez vérifier l'EntityState de l'enfant en tapant le code suivant dans la fenêtre d'exécution de Visual Studio lorsque l'exception se produit, après l'exécution SaveChanges():

_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity

où X doit être remplacé par l'entité supprimée.

Si vous n'avez pas accès à ObjectContextpour exécuter _context.ChildEntity.Remove(child), vous pouvez résoudre ce problème en intégrant la clé étrangère à la clé primaire de la table enfant.

Parent
 ________________
| PK    IdParent |
|       Name     |
|________________|

Child
 ________________
| PK    IdChild  |
| PK,FK IdParent |
|       Name     |
|________________|

De cette façon, si vous exécutez parent.Remove(child), EF marquera correctement l'entité comme supprimée.


2

Ce type de solution a fait l'affaire pour moi:

Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID);
db.Childs.RemoveRange(original.Childs);
updated.Childs.ToList().ForEach(c => original.Childs.Add(c));
db.Entry<Parent>(original).CurrentValues.SetValues(updated);

Il est important de dire que cela supprime tous les enregistrements et les réinsère. Mais pour mon cas (moins de 10) ça va.

J'espère que cela aide.


La réinsertion se produit-elle avec de nouveaux identifiants ou conserve-t-il les identifiants de l'enfant qu'il possédait en premier lieu?
Pepito Fernandez

2

J'ai rencontré ce problème aujourd'hui et je voulais partager ma solution. Dans mon cas, la solution était de supprimer les éléments enfants avant d'obtenir le parent de la base de données.

Auparavant, je le faisais comme dans le code ci-dessous. J'obtiendrai alors la même erreur répertoriée dans cette question.

var Parent = GetParent(parentId);
var children = Parent.Children;
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

Ce qui a fonctionné pour moi, c'est d'obtenir d'abord les éléments enfants, en utilisant le parentId (clé étrangère), puis de supprimer ces éléments. Ensuite, je peux obtenir le parent de la base de données et à ce stade, il ne devrait plus avoir d'éléments enfants et je peux ajouter de nouveaux éléments enfants.

var children = GetChildren(parentId);
foreach (var c in children )
{
     Context.Children.Remove(c);
}
Context.SaveChanges();

var Parent = GetParent(parentId);
Parent.Children = //assign new entities/items here

2

Vous devez effacer manuellement la collection ChildItems et y ajouter de nouveaux éléments:

thisParent.ChildItems.Clear();
thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

Après cela, vous pouvez appeler la méthode d'extension DeleteOrphans qui gérera les entités orphelines (elle doit être appelée entre les méthodes DetectChanges et SaveChanges).

public static class DbContextExtensions
{
    private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>();

    public static void DeleteOrphans( this DbContext source )
    {
        var context = ((IObjectContextAdapter)source).ObjectContext;
        foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
        {
            var entityType = entry.EntitySet.ElementType as EntityType;
            if (entityType == null)
                continue;

            var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap);
            var props = entry.GetModifiedProperties().ToArray();
            foreach (var prop in props)
            {
                NavigationProperty navProp;
                if (!navPropMap.TryGetValue(prop, out navProp))
                    continue;

                var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name);
                var enumerator = related.GetEnumerator();
                if (enumerator.MoveNext() && enumerator.Current != null)
                    continue;

                entry.Delete();
                break;
            }
        }
    }

    private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type )
    {
        var result = type.NavigationProperties
            .Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many)
            .Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType()))
            .Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() })
            .Where(v => v.DependentProperties.Length == 1)
            .ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty);

        return new ReadOnlyDictionary<string, NavigationProperty>(result);
    }
}

Cela a bien fonctionné pour moi. J'avais juste besoin d'ajouter context.DetectChanges();.
Andy Edinborough

1

J'ai essayé ces solutions et bien d'autres, mais aucune d'elles n'a vraiment fonctionné. Puisqu'il s'agit de la première réponse sur google, j'ajouterai ma solution ici.

La méthode qui a bien fonctionné pour moi était de supprimer les relations de l'image pendant les commits, donc il n'y avait rien à foutre pour EF. Je l'ai fait en retrouvant l'objet parent dans le DBContext et en le supprimant. Puisque les propriétés de navigation de l'objet retrouvé sont toutes nulles, les relations des enfants sont ignorées lors de la validation.

var toDelete = db.Parents.Find(parentObject.ID);
db.Parents.Remove(toDelete);
db.SaveChanges();

Notez que cela suppose que les clés étrangères sont configurées avec ON DELETE CASCADE, donc lorsque la ligne parente est supprimée, les enfants seront nettoyés par la base de données.


1

J'ai utilisé la solution de Mosh , mais il ne m'était pas évident de savoir comment implémenter correctement la clé de composition dans le code en premier.

Voici donc la solution:

public class Holiday
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int HolidayId { get; set; }
    [Key, Column(Order = 1), ForeignKey("Location")]
    public LocationEnum LocationId { get; set; }

    public virtual Location Location { get; set; }

    public DateTime Date { get; set; }
    public string Name { get; set; }
}

1

J'ai eu le même problème, mais je savais que cela avait bien fonctionné dans d'autres cas, alors j'ai réduit le problème à ceci:

parent.OtherRelatedItems.Clear();  //this worked OK on SaveChanges() - items were being deleted from DB
parent.ProblematicItems.Clear();   // this was causing the mentioned exception on SaveChanges()
  • OtherRelatedItems avait une clé primaire composite (parentId + une colonne locale) et fonctionnait bien
  • ProblematicItems avait leur propre clé primaire à une seule colonne et le parentId n'était qu'un FK. Cela provoquait l'exception après Clear ().

Tout ce que j'avais à faire était de faire de ParentId une partie de PK composite pour indiquer que les enfants ne peuvent pas exister sans un parent. J'ai utilisé le modèle DB-first, ajouté le PK et marqué la colonne parentId comme EntityKey (j'ai donc dû la mettre à jour à la fois dans DB et EF - je ne sais pas si EF seul suffirait).

J'ai intégré RequestId au PK Et puis mis à jour le modèle EF, ET définir l'autre propriété dans le cadre de la clé d'entité

Une fois que vous y réfléchissez, c'est une distinction très élégante qu'EF utilise pour décider si les enfants "ont un sens" sans parent (dans ce cas, Clear () ne les supprimera pas et ne lèvera pas d'exception à moins que vous ne définissiez le ParentId sur autre chose / spécial ), ou - comme dans la question d'origine - nous nous attendons à ce que les éléments soient supprimés une fois qu'ils sont supprimés du parent.


0

Ce problème se produit parce que nous essayons de supprimer la table parent encore les données de la table enfant sont présentes. Nous résolvons le problème avec l'aide de la suppression en cascade.

Dans le modèle Create, méthode dans la classe dbcontext.

 modelBuilder.Entity<Job>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                .WithRequired(C => C.Job)
                .HasForeignKey(C => C.JobId).WillCascadeOnDelete(true);
            modelBuilder.Entity<Sport>()
                .HasMany<JobSportsMapping>(C => C.JobSportsMappings)
                  .WithRequired(C => C.Sport)
                  .HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);

Après cela, dans notre appel API

var JobList = Context.Job                       
          .Include(x => x.JobSportsMappings)                                     .ToList();
Context.Job.RemoveRange(JobList);
Context.SaveChanges();

L' option de suppression en cascade supprime le parent ainsi que la table enfant liée au parent avec ce code simple. Faites-le essayer de cette manière simple.

Supprimer la plage utilisée pour supprimer la liste des enregistrements dans la base de données Merci


0

J'ai également résolu mon problème avec la réponse de Mosh et je pensais que la réponse de PeterB était un peu car elle utilisait une enum comme clé étrangère. N'oubliez pas que vous devrez ajouter une nouvelle migration après avoir ajouté ce code.

Je peux également recommander cet article de blog pour d'autres solutions:

http://www.kianryan.co.uk/2013/03/orphaned-child/

Code:

public class Child
{
    [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public string Heading { get; set; }
    //Add other properties here.

    [Key, Column(Order = 1)]
    public int ParentId { get; set; }

    public virtual Parent Parent { get; set; }
}

0

En utilisant la solution de Slauma, j'ai créé des fonctions génériques pour aider à mettre à jour les objets enfants et les collections d'objets enfants.

Tous mes objets persistants implémentent cette interface

/// <summary>
/// Base interface for all persisted entries
/// </summary>
public interface IBase
{
    /// <summary>
    /// The Id
    /// </summary>
    int Id { get; set; }
}

Avec cela, j'ai implémenté ces deux fonctions dans mon référentiel

    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public T AddOrUpdateEntry<T>(DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.Id == 0 || orgEntry == null)
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            Context.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public void AddOrUpdateCollection<T>(DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }

Pour l'utiliser, je fais ce qui suit:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

J'espère que cela t'aides


EXTRA: Vous pouvez également créer une classe DbContextExtentions séparée (ou votre propre inferface de contexte):

public static void DbContextExtentions {
    /// <summary>
    /// Check if orgEntry is set update it's values, otherwise add it
    /// </summary>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The collection</param>
    /// <param name="entry">The entry</param>
    /// <param name="orgEntry">The original entry found in the database (can be <code>null</code> is this is a new entry)</param>
    /// <returns>The added or updated entry</returns>
    public static T AddOrUpdateEntry<T>(this DbContext _dbContext, DbSet<T> set, T entry, T orgEntry) where T : class, IBase
    {
        if (entry.IsNew || orgEntry == null) // New or not found in context
        {
            entry.Id = 0;
            return set.Add(entry);
        }
        else
        {
            _dbContext.Entry(orgEntry).CurrentValues.SetValues(entry);
            return orgEntry;
        }
    }

    /// <summary>
    /// check if each entry of the new list was in the orginal list, if found, update it, if not found add it
    /// all entries found in the orignal list that are not in the new list are removed
    /// </summary>
    /// <typeparam name="T">The type of entry</typeparam>
    /// <param name="_dbContext">The context object</param>
    /// <param name="set">The database set</param>
    /// <param name="newList">The new list</param>
    /// <param name="orgList">The original list</param>
    public static void AddOrUpdateCollection<T>(this DbContext _dbContext, DbSet<T> set, ICollection<T> newList, ICollection<T> orgList) where T : class, IBase
    {
        // attach or update all entries in the new list
        foreach (T entry in newList)
        {
            // Find out if we had the entry already in the list
            var orgEntry = orgList.SingleOrDefault(e => e.Id != 0 && e.Id == entry.Id);

            AddOrUpdateEntry(_dbContext, set, entry, orgEntry);
        }

        // Remove all entries from the original list that are no longer in the new list
        foreach (T orgEntry in orgList.Where(e => e.Id != 0).ToList())
        {
            if (!newList.Any(e => e.Id == orgEntry.Id))
            {
                set.Remove(orgEntry);
            }
        }
    }
}

et utilisez-le comme:

var originalParent = _dbContext.ParentItems
    .Where(p => p.Id == parent.Id)
    .Include(p => p.ChildItems)
    .Include(p => p.ChildItems2)
    .SingleOrDefault();

// Add the parent (including collections) to the context or update it's values (except the collections)
originalParent = _dbContext.AddOrUpdateEntry(_dbContext.ParentItems, parent, originalParent);

// Update each collection
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems, parent.ChildItems, orgiginalParent.ChildItems);
_dbContext.AddOrUpdateCollection(_dbContext.ChildItems2, parent.ChildItems2, orgiginalParent.ChildItems2);

Vous pouvez également créer une classe d'extension pour votre contexte avec ces fonctions:
Bluemoon74

0

J'ai été confronté au même problème lorsque je vais supprimer mon enregistrement qu'un problème s'est produit, car la solution de ce problème est que lorsque vous allez supprimer votre enregistrement, il vous manque quelque chose avant de supprimer l'en-tête / l'enregistrement principal, vous devez écrire dans le code pour supprimer ses détails avant l'en-tête / maître J'espère que votre problème sera résolu.


0

Si vous utilisez Auto mapper et que le problème suivant est la bonne solution, cela fonctionne pour moi

https://www.codeproject.com/Articles/576393/Solutionplusto-aplus-Theplusoperationplusfailed

Étant donné que le problème est que nous mappons des propriétés de navigation nulles et que nous n'avons en fait pas besoin de les mettre à jour sur l'entité puisqu'elles n'ont pas changé sur le contrat, nous devons les ignorer dans la définition de mappage:

ForMember(dest => dest.RefundType, opt => opt.Ignore())

Donc mon code s'est terminé comme ceci:

Mapper.CreateMap<MyDataContract, MyEntity>
ForMember(dest => dest.NavigationProperty1, opt => opt.Ignore())
ForMember(dest => dest.NavigationProperty2, opt => opt.Ignore())
.IgnoreAllNonExisting();

-1

J'ai rencontré ce problème avant plusieurs heures et j'ai tout essayé, mais dans mon cas, la solution était différente de la liste ci-dessus.

Si vous utilisez une entité déjà extraite de la base de données et essayez de modifier ses enfants, l'erreur se produira, mais si vous obtenez une nouvelle copie de l'entité de la base de données, il ne devrait y avoir aucun problème. N'utilisez pas ceci:

 public void CheckUsersCount(CompanyProduct companyProduct) 
 {
     companyProduct.Name = "Test";
 }

Utilisez ceci:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.