Entity Framework 4.3 с MVC при редактировании не сохраняет сложный объект - PullRequest
2 голосов
/ 26 марта 2012

Я сделал небольшой проект с базой данных Northwind, чтобы проиллюстрировать проблемные.

Вот действие контроллера:

[HttpPost]
public ActionResult Edit(Product productFromForm)
{
    try
    {
        context.Products.Attach(productFromForm);
        var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
        productFromForm.Category = fromBD;
        context.Entry(productFromForm).State = EntityState.Modified;
        context.SaveChanges();
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}

контекст создается в конструкторе контроллера как new DatabaseContext().

public class DatabaseContext:DbContext
{
    public DatabaseContext()
        : base("ApplicationServices") {
        base.Configuration.ProxyCreationEnabled = false;
        base.Configuration.LazyLoadingEnabled = false;
    }

    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder){

        modelBuilder.Configurations.Add(new ProductConfiguration());
        modelBuilder.Configurations.Add(new CategoriesConfiguration());
    }

    private class ProductConfiguration : EntityTypeConfiguration<Product> {
        public ProductConfiguration() {
            ToTable("Products");
            HasKey(p => p.ProductID);
            HasOptional(p => p.Category).WithMany(x=>x.Products).Map(c => c.MapKey("CategoryID"));
            Property(p => p.UnitPrice).HasColumnType("Money");
        }
    }

    private class CategoriesConfiguration : EntityTypeConfiguration<Category> {
        public CategoriesConfiguration() {
            ToTable("Categories");
            HasKey(p => p.CategoryID);
        }
    }
}

public class Category {
    public int CategoryID { get; set; }
    public string CategoryName { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product {
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public string QuantityPerUnit { get; set; }
    public decimal UnitPrice { get; set; }
    public Int16 UnitsInStock { get; set; }
    public Int16 UnitsOnOrder { get; set; }
    public Int16 ReorderLevel { get; set; }
    public bool Discontinued { get; set; }
    public virtual Category Category { get; set; }
}

Проблема в том, что я могу сохранить что-либо из Продукта, но не изменение категории.

Объект productFromForm содержит новый CategoryID внутри productFromForm.Product.ProductID без проблем. Но когда я Find() категории для извлечения объекта из контекста, у меня появляется объект без Name и Description (оба остаются равными NULL), а SaveChanges() не изменяет ссылку, даже если идентификатор для свойства был изменен Category.

Есть идеи, почему?

Ответы [ 2 ]

7 голосов
/ 26 марта 2012

Ваши (очевидно) измененные отношения не сохраняются, потому что вы действительно не меняете отношения:

context.Products.Attach(productFromForm);

Эта строка присоединяет productFromForm AND productFromForm.Category к контексту.

var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);

Эта строка возвращает присоединенный объект productFromForm.Category, НЕ объект из базы данных.

productFromForm.Category = fromBD;

Эта строка присваивает тот же объект, поэтому ничего не делает.

context.Entry(productFromForm).State = EntityState.Modified;

Эта строка влияет только на скалярные свойства productFromForm, а не на любые свойства навигации.

Лучшим подходом будет:

// Get original product from DB including category
var fromBD = context.Products
    .Include(p => p.Category)  // necessary because you don't have a FK property
    .Single(p => p.ProductId == productFromForm.ProductId);

// Update scalar properties of product
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);

// Update the Category reference if the CategoryID has been changed in the from
if (productFromForm.Category.CategoryID != fromBD.Category.CategoryID)
{
    context.Categories.Attach(productFromForm.Category);
    fromBD.Category = productFromForm.Category;
}

context.SaveChanges();

Это станет намного проще, если вы выставите внешние ключикак свойства в модели - как уже говорилось в ответе @ Лентности и в ответе на ваш предыдущий вопрос.Со свойствами FK (и при условии, что вы привязываете Product.CategoryID непосредственно к представлению, а не Product.Category.CategoryID), приведенный выше код сокращается до:

var fromBD = context.Products
    .Single(p => p.ProductId == productFromForm.ProductId);
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
context.SaveChanges();

В качестве альтернативы вы можете установить состояние на Modified, которое будет работатьсо свойствами FK:

context.Entry(productFromForm).State = EntityState.Modified;
context.SaveChanges();
2 голосов
/ 26 марта 2012

Проблема в том, что EF отслеживает обновления ассоциаций иначе, чем типы значений. Когда вы делаете это, context.Products.Attach(productFromForm);, productFromForm - это просто документ, который не отслеживает никаких изменений. Когда вы помечаете его как измененный, EF обновит все типы значений, но не ассоциации.

Более распространенный способ сделать это:

[HttpPost]
public ActionResult Edit(Product productFromForm)
{
    // Might need this - category might get attached as modified or added
    context.Categories.Attach(productFromForm.Category);

    // This returns a change-tracking proxy if you have that turned on.
    // If not, then changing product.Category will not get tracked...
    var product = context.Products.Find(productFromForm.ProductId);

    // This will attempt to do the model binding and map all the submitted 
    // properties to the tracked entitiy, including the category id.
    if (TryUpdateModel(product))  // Note! Vulnerable to overposting attack.
    {
        context.SaveChanges();
        return RedirectToAction("Index");
    }

    return View();
}

Наименее решение, которое подвержено наименьшим ошибкам, особенно в связи с усложнением моделей, в два раза:

  • Используйте DTO для любого входа (класс ProductInput). Затем используйте что-то вроде AutoMapper для сопоставления данных с вашим объектом домена. Особенно полезно, когда вы начинаете отправлять все более сложные данные.
  • Явно объявляйте внешние ключи в объектах вашего домена. То есть, добавьте CategoryId для вашего продукта. Сопоставьте свои данные с этим свойством, а не с объектом ассоциации. Ответ Ладислава и в последующем посте объясните подробнее об этом. Как у независимых ассоциаций, так и у внешних ключей есть свои проблемы, но до сих пор я обнаружил, что у метода внешнего ключа меньше головной боли (т. Е. Связанные объекты помечаются как добавленные, порядок подключения, пересечение проблем базы данных перед отображением и т. Д.). )

    public class Product
    {
        // EF will automatically assume FooId is the foreign key for Foo.
        // When mapping input, change this one, not the associated object.
        [Required]
        public int CategoryId { get; set; }
    
        public virtual Category Category { get; set; }
    }
    
...