web-dev-qa-db-ja.com

EntityFrameworkを使用してコレクションにアイテムを追加する

Entity Framework 4でDDDリポジトリパターンに従おうとしていますが、集計ルートのコレクションプロパティへの変更を保存する際に問題が発生します。以下の私のクラスを考えてみましょう。 Itemは、SubItemエンティティのコレクションを含む集約ルートです。

public class Item
{
    public int ItemId { get; set; }
    public string Name { get; set; }
    public ICollection<SubItem> SubItems { get; private set; }

    public Item()
    {
        this.SubItems = new HashSet<SubItem>();
    }
}

public class SubItem
{
    public int ItemId { get; set; }
    public int SubItemId { get; set; }
    public string Name { get; set; }
}

次に、集約ルートクラスのリポジトリインターフェイスを定義しました

public interface IItemRespository
{
    Item Get(int id);
    void Add(Item i);
    void Save(Item i);
}

これが、EFマッピングを設定するDbContextクラスです。

public class ItemContext : System.Data.Entity.DbContext
{
    protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Item>().HasKey(i => i.ItemId);
        modelBuilder.Entity<Item>().Property(i => i.Name);

        modelBuilder.Entity<Item>().HasMany(i => i.SubItems)
            .WithRequired()
            .HasForeignKey(si => si.ItemId);

        modelBuilder.Entity<SubItem>().HasKey(i => i.SubItemId);
        modelBuilder.Entity<SubItem>().Property(i => i.Name);
    }
}

最後に、DBContextを使用したIRepositoryの実装を示します。

public class Repository : IItemRespository
{
    public void Save(Item i)
    {
        using (var context = new ItemContext())
        {
            context.Set<Item>().Attach(i);
            context.SaveChanges();
        }
    }

    public Item Get(int id)
    {
        using (var context = new ItemContext())
        {
            var result = (from x in context.Set<Item>() where x.ItemId == id select x).FirstOrDefault();
            return result;
        }
    }

    public void Add(Item i)
    {
        using (var context = new ItemContext())
        {
            context.Set<Item>().Add(i);
            context.SaveChanges();
        }
    }
}

次のコードは、新しいアイテムを作成し、それをリポジトリに追加し、いくつかの新しいサブアイテムを追加してから、変更を保存します。

IItemRespository repo = new Repository();

//Create a new Item
Item parent = new Item() { Name = "Parent" };
repo.Add(parent);

//A long period of time may pass .. . .

//later add sub items
parent.SubItems.Add(new SubItem() { Name = "Child 1" });
parent.SubItems.Add(new SubItem() { Name = "Child 2" });   
parent.SubItems.Add(new SubItem() { Name = "Child 3" });

//save the added sub items
repo.Save(parent);

Save()メソッドがアイテムをコンテキストにアタッチしようとすると、次の例外が発生します。

参照整合性制約違反が発生しました:参照制約を定義するプロパティ値が、関係内の主オブジェクトと依存オブジェクトの間で一貫していません。

リポジトリ内のメソッドごとに新しいコンテキストを作成していることに気付きました。これは意図的なものです。アイテムが追加されてから後で編集されるまでに長い時間がかかる場合があります。その間、コンテキストまたはデータベース接続を開いたままにしたくありません。

以下のコードのように、サブアイテムを追加する前にnewItemを2番目のコンテキストにアタッチすると、機能します。

//Create a new item
Item newItem = new Item() { Name = "Parent" };

using (ItemContext context1 = new ItemContext())
{
    //Create a new aggrgate
    context1.Set<Item>().Add(newItem);
    context1.SaveChanges();
}

//Long period of time may pass

using (ItemContext context2 = new ItemContext())
{
    context2.Set<Item>().Attach(newItem);

    newItem.Name = "Edited Name";
    newItem.SubItems.Add(new SubItem() { Name = "Child 1" });
    newItem.SubItems.Add(new SubItem() { Name = "Child 2" });
    newItem.SubItems.Add(new SubItem() { Name = "Child 3" });

    context2.SaveChanges();
}

ただし、リポジトリパターンに忠実でありたい場合、Itemを編集するコードは、リポジトリの動作やItemContextクラスについて何も知らないはずです。単に集約ルートエンティティに変更を加えてから、リポジトリのSave()メソッドを介してそれらの変更を保存できる必要があります。

では、Item.SubItemsへの変更が正しく保存されるようにSave()メソッドを変更するにはどうすればよいですか?

13
Eric Anastas

これを機能させるには、いくつかのプロパティを設定してEFを支援する必要があります。

新しいサブアイテムを作成するときは、FKを自分で設定する必要があります。

   parent.SubItems.Add(new SubItem() { Name = "Child 1", ItemId = parent.ItemId});
   parent.SubItems.Add(new SubItem() { Name = "Child 2", ItemId = parent.ItemId });
   parent.SubItems.Add(new SubItem() { Name = "Child 3", ItemId = parent.ItemId });

次に、保存機能で、アイテムをコンテキストに追加または添付します。

    public void Save(Item i)
    {
        using (var context = new ItemContext())
        {
            foreach (var subitem in i.SubItems)
            {
                if (subitem.SubItemId == 0)
                    context.Set<SubItem>().Add(subitem);
                else
                    context.Set<SubItem>().Attach(subitem);
            }
            context.Set<Item>().Attach(i);
            context.SaveChanges();
        }
    }

その理由は、アタッチを実行しているときにエンティティがコンテキストにアタッチされていないため、EFはエンティティがどこから来たのかを実際には知らないためです。FKが設定されていない(おそらく0)のは有効な状態であると考えられていました。 -これがエラーの原因です。最初に子オブジェクトをアタッチする必要がある理由は、アタッチするのではなく実際に追加できるようにするためです。繰り返しになりますが、サブアイテムがアタッチされたときにコンテキストが有効でなかったため、EFはエンティティがどこから来たのかわからず、0 PKが正しいと見なして、エラーを作成します。

14
Mark Oreta