web-dev-qa-db-ja.com

EFで親エンティティを更新するときに子エンティティを追加/更新する方法

2つのエンティティは1対多の関係です(コードが最初に流fluentなAPIによって構築されます)。

public class Parent
{
    public Parent()
    {
        this.Children = new List<Child>();
    }

    public int Id { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int Id { get; set; }

    public int ParentId { get; set; }

    public string Data { get; set; }
}

WebApiコントローラーには、親エンティティを作成する(正常に機能している)アクションと、親エンティティを更新する(何らかの問題がある)アクションがあります。更新アクションは次のようになります。

public void Update(UpdateParentModel model)
{
    //what should be done here?
}

現在、2つのアイデアがあります。

  1. model.Idによってexistingという名前の追跡された親エンティティを取得し、modelの値を1つずつエンティティに割り当てます。これはばかげているように聞こえます。 model.Childrenでは、どの子が新しいか、どの子が変更されているか(または削除されているか)わかりません。

  2. modelを介して新しい親エンティティを作成し、DbContextに添付して保存します。しかし、DbContextはどのようにして子の状態(新しい追加/削除/変更)を知ることができますか?

この機能を実装する正しい方法は何ですか?

125
Cheng Chen

WebApiコントローラーにポストされるモデルはエンティティフレームワーク(EF)コンテキストから切り離されるため、唯一のオプションはオブジェクトグラフ(子を含む親)をデータベースから読み込み、どの子が追加、削除、または更新しました。 (私の意見では、以下のように複雑な分離状態(ブラウザまたはどこでも)で独自の追跡メカニズムで変更を追跡する場合を除きます。)次のようになります。

public void Update(UpdateParentModel model)
{
    var existingParent = _dbContext.Parents
        .Where(p => p.Id == model.Id)
        .Include(p => p.Children)
        .SingleOrDefault();

    if (existingParent != null)
    {
        // Update parent
        _dbContext.Entry(existingParent).CurrentValues.SetValues(model);

        // Delete children
        foreach (var existingChild in existingParent.Children.ToList())
        {
            if (!model.Children.Any(c => c.Id == existingChild.Id))
                _dbContext.Children.Remove(existingChild);
        }

        // Update and Insert children
        foreach (var childModel in model.Children)
        {
            var existingChild = existingParent.Children
                .Where(c => c.Id == childModel.Id)
                .SingleOrDefault();

            if (existingChild != null)
                // Update child
                _dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
            else
            {
                // Insert child
                var newChild = new Child
                {
                    Data = childModel.Data,
                    //...
                };
                existingParent.Children.Add(newChild);
            }
        }

        _dbContext.SaveChanges();
    }
}

...CurrentValues.SetValuesは、任意のオブジェクトを取得し、プロパティ名に基づいてプロパティ値を添付されたエンティティにマッピングできます。モデルのプロパティ名がエンティティの名前と異なる場合、このメソッドは使用できず、値を1つずつ割り当てる必要があります。

175
Slauma

私はこのようなものをいじっていました...

protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
    {
        var dbItems = selector(dbItem).ToList();
        var newItems = selector(newItem).ToList();

        if (dbItems == null && newItems == null)
            return;

        var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
        var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();

        var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
        var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();

        var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
        toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));

        var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
        toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
    }

次のようなもので呼び出すことができます:

UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)

残念なことに、子型にコレクションプロパティがあり、更新する必要がある場合、この種の機能は無効になります。 UpdateChildCollectionを単独で呼び出す責任があるIRepository(基本的なCRUDメソッドを含む)を渡すことでこれを解決しようとすることを検討してください。 DbContext.Entryを直接呼び出すのではなく、リポジトリを呼び出します。

これがすべて大規模にどのように機能するかはわかりませんが、この問題をどう処理するかはわかりません。

10
brettman

EntityFrameworkCoreを使用している場合は、コントローラーのポストアクションで次のことができます( Attach method はコレクションを含むナビゲーションプロパティを再帰的にアタッチします)。

_context.Attach(modelPostedToController);

IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);

foreach(EntityEntry ee in unchangedEntities){
     ee.State = EntityState.Modified;
}

await _context.SaveChangesAsync();

更新された各エンティティには、クライアントからの投稿データで設定および提供されたすべてのプロパティがあると想定されます(たとえば、エンティティの部分的な更新では機能しません)。

また、この操作に新しい/専用のエンティティフレームワークデータベースコンテキストを使用していることを確認する必要があります。

7
hallz

わかりました。この答えは一度ありましたが、途中で失いました。より良い方法があるが、それを思い出せない、または見つけられないときは絶対的な拷問を!とても簡単です。複数の方法でテストしました。

var parent = _dbContext.Parents
  .Where(p => p.Id == model.Id)
  .Include(p => p.Children)
  .FirstOrDefault();

parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
_dbContext.Entry(parent).State = EntityState.Modified;

_dbContext.SaveChanges();

リスト全体を新しいリストに置き換えることができます! SQLコードは、必要に応じてエンティティを削除および追加します。心配する必要はありません。必ず子コレクションを含めるか、サイコロを含めないでください。がんばろう!

4

オブジェクトグラフ全体の保存に関する限り、クライアントとサーバー間のやり取りを容易にするプロジェクトがいくつかあります。

ご覧になりたい2つを次に示します。

上記の両方のプロジェクトは、サーバーに返されるときに切断されたエンティティを認識し、変更を検出して保存し、クライアントの影響を受けるデータに戻ります。

1
Shimmy

ちょうど概念実証Controler.UpdateModelは正しく動作しません。

完全なクラス ここ

const string PK = "Id";
protected Models.Entities con;
protected System.Data.Entity.DbSet<T> model;

private void TestUpdate(object item)
{
    var props = item.GetType().GetProperties();
    foreach (var prop in props)
    {
        object value = prop.GetValue(item);
        if (prop.PropertyType.IsInterface && value != null)
        {
            foreach (var iItem in (System.Collections.IEnumerable)value)
            {
                TestUpdate(iItem);
            }
        }
    }

    int id = (int)item.GetType().GetProperty(PK).GetValue(item);
    if (id == 0)
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Added;
    }
    else
    {
        con.Entry(item).State = System.Data.Entity.EntityState.Modified;
    }

}
0
Mertuarez