Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
360 views
in Technique[技术] by (71.8m points)

c# - The relationship could not be changed because one or more of the foreign-key properties is non nullable

I get following error during update with EF:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

Is there any general way to find which foreign-key properties cause above error?

[Update]

For one case following code cause above error(I worked in a disconnected environment, so I used graphdiff to update my objects graph), when it wants to run _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();
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

In Entity Framework you can work with foreign key associations. That is, a foreign key to another object is expressed as a pair of two properties: a primitive foreign key property (e.g. NominalRouting.OrderItemId) and an object reference (NominalRouting.OrderItem).

This means that you can either set a primitive value or an object reference to establish a foreign key association. If you set one of them, EF tries to keep the other one in sync, if possible. Unfortunately, this may also give rise to conflicts between primitive foreign key values and their accompanying references.

It's hard to tell what exactly happens in your case. However, I do know that your approach of "copying" objects from one parent to another is... not ideal. First, it's never a good idea to change primary key values. By setting them to 0 you make the object look like new, but they aren't. Secondly, you assign the same child objects to other parent objects many times. I think that, as a consequence, you end up with a large number of objects having a foreign key value but not a reference.

I said "copying", because that's what you seemingly try to achieve. If so, you should properly clone objects and Add them to each targetOrderItem. At the same time, I wonder why you (apparently) clone all these objects. It looks like many-to-many associations are more appropriate here. But that's a different subject.

Now your actual question: how to find the conflicting associations?

That's very, very hard. It would require code to search through the conceptual model and find properties involved in foreign key associations. Then you'd have to find their values and find mismatches. Hard enough, but trivial when compared to determining when a possible conflict is an actual conflict. Let me clarify this by two examples. Here, a class OrderItem has a required foreign key association consisting of properties Order and OrderId.

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

So there's an item with OrderId assigned and Order = null, and EF is happy.

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

Again, an item with OrderId assigned and Order = null, but EF throws the exception "The relationship could not be changed...".

(and there are more possible conflict situations)

So it's no enough to look for unmatched values in OrderId/Order pairs, you'd also have to inspect entity states and know exactly in which combination of states a mismatch is not allowed. My advice: forget it, fix your code.

There's one dirty trick though. When EF tries to match foreign key values and references, somewhere deep down in a tree of nested ifs it collects the conflicts we're talking about into a member variable of the ObjectStateManager, named _entriesWithConceptualNulls. It's possible to get its value by doing some reflection:

#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 is a HashSet<EntityEntry>, EntityEntry is an internal class, so you can only inspect the collection in the debugger to get an idea of conflicting entities. For diagnostic purposes only!!!


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...