web-dev-qa-db-fra.com

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

J'obtiens l'erreur suivante lors de la mise à jour avec EF:

L'opération a échoué: la relation n'a pas pu être modifiée car une ou plusieurs des propriétés de clé étrangère ne peuvent pas être annulées. 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 être affectée à une autre valeur non nulle ou l'objet non lié doit être supprimé.

Existe-t-il un moyen général de trouver les propriétés de clé étrangère qui provoquent l'erreur ci-dessus?

[Mise à jour]

Pour un cas, le code suivant provoque l'erreur ci-dessus (j'ai travaillé dans un environnement déconnecté, j'ai donc utilisé graphdiff pour mettre à jour mon graphique d'objets), quand il veut exécuter _uow.Commit();:

public void CopyTechnicalInfos(int sourceOrderItemId, List<int> targetOrderItemIds)
{
  _uow = new MyDbContext();
   var sourceOrderItem = _uow.OrderItems
          .Include(x => x.NominalBoms)
          .Include("NominalRoutings.NominalSizeTests")
          .AsNoTracking()
          .FirstOrDefault(x => x.Id == sourceOrderItemId);


   var criteria = PredicateBuilder.False<OrderItem>();
   foreach (var targetOrderItemId in orderItemIds)
   {
      int id = targetOrderItemId;
      criteria = criteria.OR(x => x.Id == id);
   }
   var targetOrderItems = _uow.OrderItems
                              .AsNoTracking()
                              .AsExpandable()   
                              .Where(criteria)
                              .ToList();

  foreach (var targetOrderItem in targetOrderItems)
  {
        //delete old datas and insert new datas 
        targetOrderItem.NominalBoms = sourceOrderItem.NominalBoms;
        targetOrderItem.NominalBoms.ForEach(x => x.Id = 0);

        targetOrderItem.NominalRoutings = sourceOrderItem.NominalRoutings;
        targetOrderItem.NominalRoutings.ForEach(x => x.Id = 0);
        targetOrderItem.NominalRoutings
                       .ForEach(x => x.NominalTests.ForEach(y => y.Id = 0));
        targetOrderItem.NominalRoutings
                       .ForEach(x => x.NominalSizeTests.ForEach(y => y.Id = 0));
       _uow.OrderItems.UpdateGraph(targetOrderItem, 
                                   x => x.OwnedCollection(y => y.NominalBoms)
                                         .OwnedCollection(y => y.NominalRoutings, 
                                          with => with
                                         .OwnedCollection(t => t.NominalTests)));
   }
   _uow.Commit();
}
28
Masoud

Dans Entity Framework, vous pouvez travailler avec les associations de clés étrangères . En d'autres termes, une clé étrangère vers un autre objet est exprimée comme une paire de deux propriétés: une propriété de clé étrangère primitive (par exemple NominalRouting.OrderItemId) et une référence d'objet (NominalRouting.OrderItem).

Cela signifie que vous pouvez définir une valeur primitive ou une référence d'objet pour établir une association de clé étrangère. Si vous en définissez un, EF essaie de garder l'autre synchronisé, si possible. Malheureusement, cela peut également donner lieu à des conflits entre les valeurs de clé étrangère primitive et les références qui les accompagnent.

Il est difficile de dire ce qui se passe exactement dans votre cas. Cependant, je sais que votre approche de "copie" d'objets d'un parent à un autre n'est ... pas idéale. Tout d'abord, ce n'est jamais une bonne idée de modifier les valeurs de clé primaire. En les définissant sur 0 vous donnez à l'objet un aspect neuf, mais ce n'est pas le cas. Deuxièmement, vous affectez plusieurs fois les mêmes objets enfants à d'autres objets parents. Je pense qu'en conséquence, vous vous retrouvez avec un grand nombre d'objets ayant une clé étrangère value mais pas une référence .

J'ai dit "copier", car c'est ce que vous essayez apparemment de réaliser. Si tel est le cas, vous devez cloner correctement les objets et les Add dans chaque targetOrderItem. En même temps, je me demande pourquoi vous clonez (apparemment) tous ces objets. Il semble que les associations plusieurs-à-plusieurs soient plus appropriées ici. Mais c'est un sujet différent.

Maintenant votre vraie question: comment trouver les associations conflictuelles?

C'est très, très dur. Il faudrait du code pour rechercher dans le modèle conceptuel et trouver les propriétés impliquées dans les associations de clés étrangères. Ensuite, vous devez trouver leurs valeurs et trouver des décalages. Assez difficile, mais trivial par rapport à déterminer quand un conflit possible est un réel conflit. Permettez-moi de clarifier cela par deux exemples. Ici, une classe OrderItem a une association de clé étrangère requise composée des propriétés Order et OrderId.

var item = new OrderItem { OrderId = 1, ... };
db.OrderItems.Add(item);
db.SaveChanges();

Il y a donc un élément avec OrderId assigné et Order = null, et EF est content.

var item = db.OrderItems.Include(x => x.Order).Find(10);
// returns an OrderItem with OrderId = 1
item.Order = null;
db.SaveChanges();

Encore une fois, un élément avec OrderId affecté et Order = null, mais EF lève l'exception "La relation n'a pas pu être modifiée ...".

(et il y a plus de situations de conflit possibles)

Il ne suffit donc pas de rechercher des valeurs inégalées dans OrderId/Order paires, vous devez également inspecter les états d'entité et savoir exactement dans quelle combinaison d'états une incompatibilité n'est pas autorisée. Mon conseil: oubliez ça, corrigez votre code.

Il y a cependant un sale tour. Lorsque EF essaie de faire correspondre les valeurs de clés étrangères et les références, quelque part au fond d'une arborescence de ifs imbriqués, il recueille les conflits dont nous parlons dans une variable membre de ObjectStateManager, nommée _entriesWithConceptualNulls. Il est possible d'obtenir sa valeur en faisant quelques réflexions:

#if DEBUG

db.ChangeTracker.DetectChanges(); // Force EF to match associations.
var objectContext = ((IObjectContextAdapter)db).ObjectContext;
var objectStateManager = objectContext.ObjectStateManager;
var fieldInfo = objectStateManager.GetType().GetField("_entriesWithConceptualNulls", BindingFlags.Instance | BindingFlags.NonPublic);
var conceptualNulls = fieldInfo.GetValue(objectStateManager);

#endif

conceptualNulls est un HashSet<EntityEntry>, EntityEntry est une classe interne, vous pouvez donc uniquement inspecter la collection dans le débogueur pour avoir une idée des entités en conflit. À des fins de diagnostic uniquement !!!

61
Gert Arnold