2

問題を説明するためにNorthwindデータベースで小さなプロジェクトを作成しました。ここMVCを編集したEntity Framework 4.3では複雑なオブジェクトは保存されません

は、コントローラのアクションである:

[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オブジェクトには、productFromForm.Product.ProductID内の新しいCategoryIDが問題なく含まれています。しかし、私はFind()コンテキストからオブジェクトを取得するカテゴリを取得するときに、名前と説明を持たないオブジェクト(NULLにとどまりません)と、CategoryのIDが変更されたとしてもSaveChanges()は参照を変更しません。

理由は何ですか?

+0

Categoryナビゲーション参照の代わりにproduct.CategoryIDを設定するとどうなりますか? – Dismissile

+0

productFromFormには既にフォームから.CategoryIDが正しく入力されています。 –

+0

ああ、私は自分のエンティティをコントローラで直接使用しないので、わかりませんでした。 – Dismissile

答えて

7

あなたは(明らかに)あなたが本当に関係が変化しないので関係が保存されません変更:

context.Products.Attach(productFromForm); 

この行は、コンテキストにproductFromFormproductFromForm.Categoryを添付する。

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

この行は、データベースから添付オブジェクトproductFromForm.Category、NOTオブジェクトを返します。

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.Category.CategoryIDに直接Product.CategoryIDに結合すると仮定して)で、上記のコードは、に帰着する:

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

別の方法としては、FK特性で動作あろうModifiedに状態を設定することができる:

context.Entry(productFromForm).State = EntityState.Modified; 
context.SaveChanges(); 
+0

ありがとうございます。現時点では、外部キーのようなデータベースでモデルを変更しないことに決めました。私は書くのがより少ないことを理解していますが、これは現時点でのグループの決定でした。解決策はうまくいき、説明は本当に堅実でした。よくやった。 0..1の関係でカテゴリをNULLに設定すると、この種のコードで動作すると思いますか? –

+0

@PatrickDesjardins:はい、うまくいくと思います。参照をnullに設定する前に、オリジナルを 'Include'でロードするだけです。 – Slauma

+0

私はテストを行い、それも動作します。この回答とその品質については、ありがとうございます。 –

2

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

私はモデルがより複雑になり、特にとして、見つけた以上、エラーが発生しやすいソリューションは、2倍である:

  • 使用DTOさん任意の入力(クラスProductInput)に対して次に、AutoMapperのようなものを使用して、データをドメインオブジェクトにマップします。ますます複雑化するデータを提出し始めると特に便利です。
  • ドメインオブジェクトに外部キーを明示的に宣言します。つまり、商品をCategoryIdに追加します。関連オブジェクトではなく、このプロパティに入力をマッピングします。 Ladislav's answerおよびsubsequent postについて詳しく説明します。独立したアソシエーションと外部キーの両方には独自の問題がありますが、これまで外来キーメソッドには頭痛の少ない方法がありました(関連エンティティの追加、添付順、マッピング前のデータベースの問題など)。 )

    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; } 
    } 
    
+0

あなたのコードで二重返信と思うエラーがあります。 –

+0

ああ、ありがとう - 今すぐ修正。 – Leniency

関連する問題