Прежде всего, почему я не могу обновить сложные свойства?

Я работаю над небольшим образцовым проектом, используя Entity Framework 4.1 (сначала код). Мои классы выглядят так:

public class Context : DbContext
{
 public IDbSet<person> People { get; set; }
 public IDbSet<employeetype> EmployeeTypes { get; set; }
}
public class Person
{
 [Key]
 public int Key { get; set; }
 public string FirstName { get; set; }
 public string LastName { get; set; }
 virtual public EmployeeType EmployeeType { get; set; }
}
public class EmployeeType
{
 [Key]
 public int Key { get; set; }
 public string Text { get; set; }
 virtual public ICollection<person> People { get; set; }
}
</person></employeetype></person>

Я сохранил пару EmployeeTypes ( "первый", "второй" ) в базе данных, и я сохранил Person, у которого есть первый тип. Теперь я хочу изменить Личность. Я знаю, что могу сделать это, загрузив Person, изменив свойства, а затем сохраните. Но то, что я хочу сделать вместо этого, что мне кажется, что это должно сработать, заключается в следующем:

var c = new Context();
var e = c.EmployeeTypes.Single(x => x.Text.Equals("second"));
var p = new Person { 
 Key = originalKey, // same key
 FirstName = "NewFirst", // new first name
 LastName = "NewLast", // new last name
 EmployeeType = e }; // new employee type
c.Entry(p).State = EntityState.Modified;
c.SaveChanges();

Как ни странно, это изменяет имя FirstName и LastName, но не EmployeeType. Если я получаю новый Контекст и запрашиваю этого Лица, EmployeeType остается установленным как "первым", как это было до того, как этот код запустился.

Что мне нужно сделать, чтобы обновить свойства навигации, а не только скалярные свойства? (Это особенно проблематично, потому что для EmployeeType единственное, что действительно нужно изменить, это внешний ключ в таблице Person, и этот ключ является скалярным свойством.)

(Кстати, я знаю, что могу сделать это, сначала извлекая Person, а затем меняя свойства один за другим, но поскольку я использую привязку к модели в ASP.NET MVC, похоже, этот способ был бы проще, потому что у меня будет обновленный объект человека уже в методе POST.)

3 ответа

Вы можете попробовать по-другому:

var c = new Context();
var e = c.EmployeeTypes.Single(x => x.Text.Equals("second"));
var p = new Person { 
 Key = originalKey, // same key
 FirstName = "NewFirst", // new first name
 LastName = "NewLast"}; // new last name
c.People.Attach(p); // Attach person first so that changes are tracked 
c.Entry(p).Reference(e => e.EmployeeType).Load(); 
p.EmployeeType = e; // Now context should know about the change
c.Entry(p).State = EntityState.Modified;
c.SaveChanges();

Другой подход заключается в том, чтобы выставлять внешний ключ в вашей организации Person, например:

public class Person
{
 [Key]
 public int Key { get; set; }
 public string FirstName { get; set; }
 public string LastName { get; set; }
 [ForeignKey("EmployeeType")]
 public int EmployeeTypeKey { get; set; }
 public virtual EmployeeType EmployeeType { get; set; }
}

Это изменит тип отношения между Person и EmployeeType от независимой ассоциации к ассоциации внешних ключей. Вместо назначения свойства навигации назначьте свойство внешнего ключа. Это позволит вам изменить отношение по вашему текущему коду.

Проблема в том, что независимые ассоциации (те, которые не используют свойство внешнего ключа) обрабатываются как отдельный объект в диспетчере состояний/изменении трекера. Таким образом, ваша модификация человека не повлияла на состояние существующего отношения и не установила новое отношение. Я спросил у MSDN, как это сделать с DbContext API, но это возможно, только если вы отбрасываете DbContext в ObjectContext и используете ObjectStateManager и ChangeRelationshipState.


Попробовав дюжину различных способов сделать это EF-способом, я пришел к выводу, что нет разумного кода EF-кода. Первый способ сделать то, что я пытаюсь сделать. Поэтому я использовал отражение. Я создал этот метод для моего класса, который наследует от DbContext:

public void UpdateFrom<t>(T updatedItem) where T : KeyedItem
{
 var originalItem = Set<t>().Find(updatedItem.Key);
 var props = updatedItem.GetType().GetProperties(
 BindingFlags.Public | BindingFlags.Instance);
 foreach (var prop in props)
 {
 var value = prop.GetValue(updatedItem, null);
 prop.SetValue(originalItem, value, null);
 }
}
</t></t>

Все мои объекты наследуются от абстрактного класса и имеют общее свойство первичного ключа, поэтому он находит существующий объект с тем же ключом, что и тот, который был передан, а затем обновляет существующий объект из нового. SaveChanges необходимо вызвать впоследствии.


Это работает для коллекций, хотя я чувствую, что должен быть лучший способ.

var properties = typeof(TEntity).GetProperties();
foreach (var property in properties)
{
 if (property.GetCustomAttributes(typeof(OneToManyAttribute), false).Length > 0)
 {
 dynamic collection = db.Entry(e).Collection(property.Name).CurrentValue;
 foreach (var item in collection)
 {
 if(item.GetType().IsSubclassOf(typeof(Entity))) 
 {
 if (item.Id == 0)
 {
 db.Entry(item).State = EntityState.Added;
 }
 else
 {
 db.Entry(item).State = EntityState.Modified;
 }
 }
 }
 }
}
db.Entry(e).State = EntityState.Modified; 
db.SaveChanges();

licensed under cc by-sa 3.0 with attribution.