La relation n'a pas pu être modifiée parce qu'une ou plusieurs des propriétés de la clé étrangère ne sont pas nulles.

j'obtiens cette erreur lorsque je me retrouve avec GetById() sur une entity et que je mets la collection d'entities enfants sur ma nouvelle liste qui vient de la vue MVC.

l'opération a échoué: le la relation ne pouvait pas être changée parce que l'un ou plusieurs de la clé étrangère les propriétés n'est pas les valeurs null. Lorsqu'un modification est apportée à une relation, le liées clé étrangère propriété est définie à une valeur null. Si la clé étrangère ne ne supporte pas les valeurs nulles, une nouvelle la relation doit être définie, le les biens à clé étrangère doivent être attribués une autre valeur non nulle, ou la sans rapport avec l'objet doit être supprimé.

Je ne comprends pas tout à fait cette ligne:

la relation ne pouvait pas être changée parce que l'un ou plusieurs de la clé étrangère les propriétés n'est pas les valeurs 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'application.

le code sur lequel l'exception s'applique est simplement l'attribution de classes d'enfants modifiées dans une collection à la classe de parents existante. Cela permettrait, espérons-le, de supprimer les classes pour enfants, d'en ajouter de nouvelles et d'y apporter des modifications. Je pensais que Entity Framework s'en occupait.

les lignes de code peuvent être distillées à:

var thisParent = _repo.GetById(1);
thisParent.ChildItems = modifiedParent.ChildItems();
_repo.Save();
154
demandé sur Ladislav Mrnka 2011-04-04 17:13:27

16 réponses

vous devez supprimer les articles pour enfants âgés thisParent.ChildItems un par un manuellement. Entity Framework ne fait pas ça pour vous. Il ne peut finalement pas décider ce que vous voulez faire avec les vieux articles d'enfant - si vous voulez les jeter ou si vous voulez les garder et les assigner à d'autres entités parentales. Vous devez informer Entity Framework de votre décision. Mais une de ces deux décisions que vous devez prendre puisque les entités enfant ne peut pas vivre seul sans une référence à l'un des parents dans la base de données (en raison de la contrainte de clé étrangère). C'est ce que l'exception dit.

Modifier

Ce que je ferais si l'enfant les éléments pourraient être ajoutées, mises à jour et supprimées:

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

Note: Ceci n'est pas testé. Il suppose que la collection d'articles pour enfants est du type ICollection . (J'ai habituellement IList et puis le code semble un peu différent. J'ai aussi enlevé toutes les abstractions du dépôt pour le conserver. simple.

Je ne sais pas si c'est une bonne solution, mais je crois qu'il faut travailler dur dans ce sens pour s'occuper de toutes sortes de changements dans la collection de navigation. Je serais également heureux de voir un moyen plus facile de le faire.

138
répondu Slauma 2018-04-26 01:41:08

la raison pour laquelle vous faites face à cette est due à la différence entre composition et agrégation .

dans sa composition, l'objet enfant est créé lorsque le parent est créé et est détruit lorsque son parent est détruit . Donc sa durée de vie est contrôlée par sa mère. par exemple un billet de blog et ses commentaires. Si un message est supprimé, ses commentaires doivent être supprimés. Il n'est pas logique d'avoir commentaires à un poste qui n'existe pas. Idem pour les commandes et les articles de commande.

l'objet enfant peut exister indépendamment de son parent . Si le parent est détruit, l'objet enfant peut encore exister, comme il peut être ajouté à un autre parent plus tard. par exemple: la relation entre une playlist et les chansons de cette playlist. Si la playlist 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 l'Entité Cadre différencie d'agrégation et de composition des relations est comme suit:

  • pour la composition: il s'attend à ce que l'objet enfant ait une clé primaire composite (ParentID, ChildID). Il s'agit là d'un choix délibéré, car l'identification des enfants devrait relever de la compétence de leurs parents.

  • Pour l'agrégation: il attend la propriété de clé étrangère dans l'objet enfant à accepter les valeurs null.

donc, la raison pour laquelle vous avez ce problème est à cause de la façon dont vous avez placé votre clé primaire dans votre table enfant. Il devrait être composite, mais il ne l'est pas. Ainsi, le cadre D'entité voit cette association comme une agrégation, ce qui signifie que, lorsque vous supprimez ou effacez les objets enfant, il ne va pas supprimer les dossiers enfant. Il va simplement supprimer l'association et définit la colonne de clé étrangère correspondante à NULL (de sorte que ces dossiers d'enfant peuvent plus tard être associés avec un autre parent). Puisque 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 une clé composite, vous devez supprimer les objets enfants explicitement. Et cela peut être fait plus simplement que les solutions suggérées plus haut:

context.Children.RemoveRange(parent.Children);

2-sinon, en plaçant la clé primaire appropriée sur votre table d'enfant, votre le code aura l'air plus significatif:

parent.Children.Clear();
78
répondu Mosh 2015-10-07 03:21:56

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

  • Vous chargez Parent à partir de la base de données et d'obtenir un joint entité
  • vous remplacez sa collection d'enfants par une nouvelle collection d'enfants détachés
  • vous économisez des modifications mais pendant cette opération tous les enfants sont considérés comme ajouté parce que EF ne savait pas à leur sujet jusqu'à ce moment. L'EF tente donc de définir null à clé étrangère des anciens enfants et insérez tous les nouveaux enfants => doublez les lignes.

Maintenant, la solution dépend vraiment de ce que vous voulez faire et comment voulez-vous faire?

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

si vous voulez simplement mettre à jour les enfants existants manuellement, 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();

la connexion n'est pas réellement nécessaire (réglage de l'état de Modified sera également attacher de l'entité), mais je l'aime parce qu'il rend le processus plus évident.

si vous voulez modifier existant, supprimer 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();
65
répondu Ladislav Mrnka 2017-08-06 07:22:12

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

je crois que vous aviez raison de supposer que le cadre de L'ORM devrait s'en charger - après tout, c'est ce qu'il promet de fournir. Sinon, votre modèle de domaine est corrompu par la persistance. NHibernate gère cela avec plaisir si vous configurez correctement les paramètres de cascade. Dans le cadre D'Entity il est également possible, ils s'attendent juste à ce que vous suivez de meilleures normes lors de la mise en place de votre modèle de base de données, surtout quand ils doivent inférer ce que Cascade devrait être fait:

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

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

à l'exclusion de ce qui précède, vous pourrait besoin d' (à partir de NHibernate expérience)

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

au lieu de remplacer entièrement la liste.

mise à 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 utiliser un classeur de modèle 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.

19
répondu Andre Luus 2017-05-23 12:18:24

si vous utilisez AutoMapper avec Entity Framework sur la même classe, vous pourriez avoir 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 va essayer de copier les deux propriétés. Dans ce cas, ClassBId est non nul. Puisque AutoMapper copiera destination.ClassB = input.ClassB; cela causera un problème.

définit votre AutoMapper pour ignorer la propriété ClassB .

 cfg.CreateMap<A, A>()
     .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId
9
répondu jsgoupil 2016-02-17 00:20:06

cela se produit parce que l'entité enfant est marquée comme modifiée au lieu d'être supprimée.

et la modification que EF fait à L'entité enfant lorsque parent.Remove(child) est exécuté, est simplement de mettre la référence à son parent à null .

vous pouvez vérifier L'EntityState de L'enfant en tapant le code suivant dans la fenêtre immédiate de Visual Studio lorsque l'exception se produit, après avoir exécuté 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 au ObjectContext pour exécuter _context.ChildEntity.Remove(child) , vous pouvez résoudre ce problème en faisant de la clé étrangère une partie de la clé primaire sur 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.

4
répondu Mauricio Ramalho 2015-08-25 16:45:51

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

cependant cela n'a pas fonctionné dans EF, l'erreur décrite dans ce thread s'est manifestée. La raison pour cela était, que dans mon modèle de données d'entité (fichier edmx) le les propriétés de l'association entre la table parent et l'enfant n'étaient pas correctes. L'option End1 OnDelete 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'option End1 OnDelete en Cascade et que cela a fonctionné. Je ne sais pas pourquoi EF n'est pas capable de le récupérer, quand 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, c'est Comment mon code à supprimer ressemble à:

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

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

si je n'avais pas de suppression en cascade définie, je devrais supprimer les lignes enfant manuellement avant de supprimer la ligne parent.

3
répondu Martin 2015-08-05 07:28:55

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 enfant avant d'obtenir le Parent de la base de données.

Auparavant, je le faisais comme dans le code ci-dessous. Je vais alors obtenir la même erreur énuméré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 les articles pour enfants d'abord, en utilisant le parentId (clé étrangère) et puis supprimer ces articles. Alors je peux avoir le Parent à partir de la base de données et, à ce stade, il ne devrait pas avoir d'enfants, et je peux ajouter de nouveaux 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
répondu Dino Bansigan 2015-06-23 20:55:49

vous devez vider manuellement la collection de ChildItems et y ajouter de nouveaux articles:

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

après cela, vous pouvez appeler la méthode D'extension DeleteOrphans qui traitera avec 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);
    }
}
2
répondu Sat 2015-09-22 19:06:18

j'ai essayé ces solutions et bien d'autres, mais aucune n'a vraiment fonctionné. Puisque c'est la première réponse sur google, je vais ajouter ma solution ici.

la méthode qui a bien fonctionné pour moi était de retirer les relations de l'image pendant les commits, donc il n'y avait pas de raison pour que EF foire. J'ai fait cela en retrouvant l'objet parent dans le DBContext, et en supprimant cela. Comme les propriétés de navigation de l'objet retrouvé sont toutes nulles, les relations sont ignorés 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 quand la ligne parent est retirée, les enfants seront nettoyés par la base de données.

1
répondu Steve 2013-12-03 15:43:37

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 insérer à nouveau. Mais pour mon cas (moins de 10) c'est ok.

j'espère que ça aidera.

1
répondu Wagner Bertolini Junior 2014-02-25 17:46:17

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

si vous utilisez déjà entité récupérée à partir de la base de données et essayer de modifier ses enfants l'erreur se produira, mais si vous obtenez une nouvelle copie de l'entité à partir de la base de données, il ne devrait pas y avoir de problèmes. N'utilisez pas ce:

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

utilisez ceci:

 public void CheckUsersCount(Guid companyProductId)
 {
      CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId);
      companyProduct.Name = "Test";
 }
0
répondu Tanyo Ivanov 2016-08-15 03:15:28

cette question se pose parce que nous essayons de supprimer la table parent les données de la table enfant immobile sont présentes. Nous résolvons le problème avec l'aide de cascade supprimer.

dans le modèle Créer la 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();

Cascade supprimer option Supprimer la table enfant parent ainsi parent avec ce code simple. Rendre essayez-le dans cette manière simple.

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

0
répondu Sowmiya V 2017-01-30 10:33:55

j'ai utilisé Mosh's solution , mais il n'était pas évident pour moi comment mettre en œuvre correctement la clé de composition dans le code d'abord.

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; }
}
0
répondu PeterB 2017-05-23 10:31:37

j'ai également résolu mon problème avec réponse de Mosh et je pensais réponse de PeterB était un peu de car il a utilisé un enum comme clé étrangère. Rappelez-vous 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
répondu Ogglas 2017-07-07 13:54:06