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


151

2つのエンティティは1対多の関係です(コードが最初に流れる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. で指定existingされた追跡対象の親エンティティを取得し、1つずつエンティティmodel.Idに値を割り当てますmodel。これは愚かに聞こえます。そして、model.Childrenどの子が新しいのか、どの子が変更されている(または削除されている)のかわかりません。

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

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


重複した質問にGraphDiffでも例を参照してくださいstackoverflow.com/questions/29351401/...
マイケルFreidgeimに

回答:


219

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つずつ割り当てる必要があります。


35
しかし、なぜefにはもっと「素晴らしい」方法がないのでしょうか。私はefが子が変更/削除/追加されたかどうかを検出できると思います。IMO上記のコードはEFフレームワークの一部となり、より一般的なソリューションになる可能性があります。
Cheng Chen

7
@DannyChen:切断されたエンティティの更新をEFがより快適な方法でサポートする必要があるというのは確かに長い要求です(entityframework.codeplex.com/workitem/864)が、それでもフレームワークの一部ではありません。現在、そのcodeplex作業項目で言及されているサードパーティのlib "GraphDiff"を試すか、上記の私の回答のように手動でコードを書くことができます。
Slauma 2014年

7
追加すること:更新と挿入の子のforeach内では実行できませんexistingParent.Children.Add(newChild)。それは、existingChild linq検索が最近追加されたエンティティを返し、そのエンティティが更新されるためです。一時的なリストに挿入して追加するだけです。
Erre Efe

3
@RandolfRincónFadulこの問題に遭遇しました。少し少ない労力である私の修正がでwhere句を変更することですexistingChildLINQクエリ:.Where(c => c.ID == childModel.ID && c.ID != default(int))
ギャビン・ワード

2
@RalphWillgossあなたが話していた2.2の修正は何ですか?
Jan Paolo Go

11

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

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を直接呼び出す代わりに、リポジトリを呼び出します。

これがどのように大規模に機能するかはわかりませんが、この問題で他に何ができるかわかりません。


1
素晴らしい解決策!しかし、複数の新しいアイテムを追加すると失敗し、更新された辞書はゼロのIDを2回持つことができません。いくつかの回避策が必要です。また、関係がN-> Nの場合も失敗します。実際、アイテムはデータベースに追加されますが、N-> Nテーブルは変更されません。
RenanStr '19年

1
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value));n-> nの問題を解決する必要があります。
RenanStr '19年

10

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

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コードは、必要に応じてエンティティを削除および追加します。それを気にする必要はありません。子のコレクションを含めるか、サイコロを含めないでください。幸運を!


モデルの子の数は一般的に非常に少ないため、必要なものだけです。したがって、Linqは最初にすべての元の子をテーブルから削除し、次に新しい子をすべて追加すると仮定すると、パフォーマンスへの影響は問題になりません。
ウィリアムT.マラード

@チャールズマッキントッシュ。最初のクエリに含めているのに、なぜ子を再度設定したのか理解できませんか?
パントニー

1
@pantonis編集のためにロードできるように、子コレクションを含めます。それを理解するために遅延読み込みに依存している場合、それは機能しません。子を設定します(一度)。手動でアイテムを削除してコレクションに追加する代わりに、リストを置き換えるだけで、entityframeworkがアイテムを追加および削除してくれるからです。重要なのは、エンティティの状態を変更済みに設定し、entityframeworkが重い作業を実行できるようにすることです。
Charles McIntosh

@CharlesMcIntosh私はまだあなたがそこで子供たちと達成しようとしていることを理解していません。最初のリクエストに含めました(Include(p => p.Children)。なぜもう一度リクエストするのですか?
pantonis

@pantonis、私は.include()を使用して古いリストをプルし、データベースからコレクションとしてロードおよびアタッチする必要がありました。遅延読み込みが呼び出される方法です。これがないと、リストへの変更は、entitystate.modifiedを使用したときに追跡されません。繰り返しになりますが、私がしていることは、現在の子コレクションを別の子コレクションに設定することです。マネージャーがたくさんの新入社員を獲得したり、数人を失ったようなものです。クエリを使用してそれらの新しい従業員を含めたり除外したりして、古いリストを新しいリストに置き換えるだけで、データベース側から必要に応じてEFが追加または削除できるようにします。
Charles McIntosh

9

EntityFrameworkCoreを使用している場合、コントローラーのpostアクションで次の操作を実行できます(Attachメソッドは、コレクションを含むナビゲーションプロパティを再帰的にアタッチします)。

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

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

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


5
public async Task<IHttpActionResult> PutParent(int id, Parent parent)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != parent.Id)
            {
                return BadRequest();
            }

            db.Entry(parent).State = EntityState.Modified;

            foreach (Child child in parent.Children)
            {
                db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
            }

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!ParentExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Ok(db.Parents.Find(id));
        }

これが私がこの問題を解決した方法です。このようにして、EFは更新するものを追加する必要があります。


魅力のように働いた!ありがとう。
Inktkiller

2

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

あなたが見たいと思う2つはここにあります:

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


1

概念実証 だけでは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

@Charles McIntoshは、渡されたモデルが切り離されているという私の状況に対する答えを本当にくれました。私にとって最終的に機能したのは、渡されたモデルを最初に保存することでした...次に、以前と同じように子を追加し続けました:

public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
{
    db.Entry(order).State = EntityState.Modified;
    db.SaveChanges();
  ...
}

0

VB.NET開発者向け使いやすいこの汎用サブを使用して、子の状態をマーク

ノート:

  • PromatCon:エンティティオブジェクト
  • amList:追加または変更する子リストです
  • rList:は、削除する子リストです
updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
        If amList IsNot Nothing Then
            For Each obj In amList
                Dim x = PromatCon.Entry(obj).GetDatabaseValues()
                If x Is Nothing Then
                    PromatCon.Entry(obj).State = EntityState.Added
                Else
                    PromatCon.Entry(obj).State = EntityState.Modified
                End If
            Next
        End If

        If rList IsNot Nothing Then
            For Each obj In rList.ToList
                PromatCon.Entry(obj).State = EntityState.Deleted
            Next
        End If
End Sub
PromatCon.SaveChanges()

0
var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
if (parent != null)
{
  parent.Childs = modelParent.Childs;
}

ソース


0

これがうまく機能する私のコードです。

public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
        decimal motohours, int driverID, List<int> commission,
        string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
        bool isTransportation, string violationConditions, DateTime shutdownStartTime,
        DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
        {
            try
            {
                using (var db = new GJobEntities())
                {
                    var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);

                    if (isExisting != null)
                    {
                        isExisting.AreaID = areaID;
                        isExisting.DriverID = driverID;
                        isExisting.IsTransportation = isTransportation;
                        isExisting.Mileage = mileage;
                        isExisting.Motohours = motohours;
                        isExisting.Notes = notes;                    
                        isExisting.DeviceShutdownDesc = deviceShutdownDesc;
                        isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
                        isExisting.ShutdownAtTime = shutdownAtTime;
                        isExisting.ShutdownEndTime = shutdownEndTime;
                        isExisting.ShutdownStartTime = shutdownStartTime;
                        isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
                        isExisting.ViolationConditions = violationConditions;

                        // Delete children
                        foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
                        {
                            db.DeviceShutdownFaults.Remove(existingChild);
                        }

                        if (faultIDs != null && faultIDs.Any())
                        {
                            foreach (var faultItem in faultIDs)
                            {
                                var newChild = new DeviceShutdownFault
                                {
                                    ID = Guid.NewGuid(),
                                    DDFaultID = faultItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownFaults.Add(newChild);
                            }
                        }

                        // Delete all children
                        foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
                        {
                            db.DeviceShutdownComissions.Remove(existingChild);
                        }

                        // Add all new children
                        if (commission != null && commission.Any())
                        {
                            foreach (var cItem in commission)
                            {
                                var newChild = new DeviceShutdownComission
                                {
                                    ID = Guid.NewGuid(),
                                    PersonalID = cItem,
                                    DeviceShutdownID = isExisting.ID,
                                };

                                isExisting.DeviceShutdownComissions.Add(newChild);
                            }
                        }

                        await db.SaveChangesAsync();

                        return true;
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error(ex);
            }

            return false;
        }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.