デタッチされたが等しいオブジェクトを使用して、アタッチされたオブジェクトを更新できますか?


10

外部APIから映画データを取得します。最初のフェーズでは、各映画をかき取り、自分のデータベースに挿入します。2番目のフェーズでは、APIの「変更」APIを使用してデータベースを定期的に更新します。このAPIをクエリして、情報が変更された映画を確認できます。

私のORMレイヤーはEntity-Frameworkです。Movieクラスは次のようになります。

class Movie
{
    public virtual ICollection<Language> SpokenLanguages { get; set; }
    public virtual ICollection<Genre> Genres { get; set; }
    public virtual ICollection<Keyword> Keywords { get; set; }
}

この問題は、更新が必要な映画がある場合に発生します。私のデータベースは、追跡されているオブジェクトと、更新API呼び出しから受け取った新しいオブジェクトを、を無視して別のオブジェクトと見なし.Equals()ます。

これにより、更新されたムービーでデータベースを更新しようとすると、既存のムービーを更新する代わりにデータベースが挿入されるため、問題が発生します。

以前この言語で問題がありましたが、私の解決策は、アタッチされた言語オブジェクトを検索し、それらをコンテキストから切り離し、それらのPKを更新されたオブジェクトに移動して、それをコンテキストにアタッチすることでした。ときにSaveChanges()今実行され、それは基本的にそれを交換します。

Movieオブジェクトへのこのアプローチを続けると、映画、言語、ジャンル、キーワードを切り離し、データベースでそれぞれを検索し、IDを転送して、新しいオブジェクト。

これをよりエレガントに行う方法はありますか?理想的には、更新されたムービーをコンテキストに渡し、Equals()メソッドに基づいて更新する正しいムービーを選択し、そのすべてのフィールドを更新し、各複雑なオブジェクトを更新したい場合:独自のEquals()メソッドに基づいて既存のレコードを再度使用し、それはまだ存在していません。

アタッチされている.Update()すべてのオブジェクトの取得と組み合わせて使用​​できる各複雑なオブジェクトにメソッドを提供することで、デタッチ/アタッチをスキップできますが、それでも、すべての既存のオブジェクトを取得して更新する必要があります。


APIデータから追跡されたエンティティを更新し、エンティティを置換/デタッチ/マッチング/アタッチせずに変更を保存できないのはなぜですか?
Si-N

@ Si-N:では、それがどの程度正確に拡大できるか。
Jeroen Vannevel

さて、最後の段落を追加したのは理にかなっています。エンティティを更新する前に取得しないようにしていますか?MovieクラスでPKになる可能性のあるものはありませんか?外部APIの動画をエンティティにどのように一致させますか?とにかく、1つのdb呼び出しで更新する必要があるすべてのエンティティを取得できます。これは、大きなパフォーマンスヒットを引き起こさない簡単な解決策ではないでしょうか(更新する膨大な数の映画について話しているのでない限り)。
Si-N

私のMovieクラスにはPKがidあり、外部APIからの映画は、フィールドを使用してローカルのものと照合されますtmdbid。映画、ジャンル、言語、キーワードなどに関するものであるため、1回の呼び出しで更新する必要のあるエンティティをすべて取得することはできません。それぞれにPKがあり、データベースにすでに存在している可能性があります。
Jeroen Vannevel 2015年

回答:


8

私が望んでいたものは見つかりませんでしたが、既存のselect-detach-update-attachシーケンスに対する改善点を見つけました。

拡張メソッドをAddOrUpdate(this DbSet)使用すると、私がやりたいことを正確に実行できます。存在しない場合は挿入し、既存の値が見つかった場合は更新します。seed()Migrationsと組み合わせてメソッドで使用されるのを実際に見ただけなので、これをすぐに使用することに気付きませんでした。これを使用しない理由がある場合は、お知らせください。

特記事項:同等性をどのように決定するかを具体的に選択できるオーバーロードが利用可能です。ここでは自分を使用することもできましたがTMDbId、代わりに自分のIDを完全に無視して、TMDbIdのPKをと組み合わせて使用​​することを選択しましたDatabaseGeneratedOption.None。この方法は、必要に応じて、各サブコレクションにも使用します。

ソースの興味深い部分:

internalSet.InternalContext.Owner.Entry(existing).CurrentValues.SetValues(entity);

これは、データが内部で実際に更新される方法です。

残っているのはAddOrUpdate、これによって影響を受けたい各オブジェクトを呼び出すことだけです。

public void InsertOrUpdate(Movie movie)
{
    _context.Movies.AddOrUpdate(movie);
    _context.Languages.AddOrUpdate(movie.SpokenLanguages.ToArray());
    // Other objects/collections
    _context.SaveChanges();
}

更新する必要があるオブジェクトの各部分を手動で指定する必要があるため、思ったほどきれいではありませんが、それは可能な限り近いです。

関連資料:https : //stackoverflow.com/questions/15336248/entity-framework-5-updating-a-record


更新:

私のテストは十分に厳格ではなかったことがわかりました。このテクニックを使用した後、新しい言語が追加されている間、それは映画に関連付けられていませんでした。多対多のテーブルで。これは既知の問題ですが優先度は低いようですが私の知る限りでは修正されていません。

最後に、私はUpdate(T)各タイプにメソッドがあり、次の一連のイベントに従うというアプローチをとることにしました。

  • 新しいオブジェクトのコレクションをループする
  • 各コレクションの各エントリについて、データベースで検索します
  • 存在する場合は、Update()メソッドを使用して新しい値で更新します
  • 存在しない場合は、適切なDbSetに追加します。
  • アタッチされたオブジェクトを返し、ルートオブジェクトのコレクションをアタッチされたオブジェクトのコレクションで置き換えます
  • ルートオブジェクトを見つけて更新する

これは多くの手動作業であり、見苦しいので、さらにいくつかのリファクタリングを行いますが、私のテストでは、より厳密なシナリオで動作するはずであることが示されています。


さらにクリーンアップした後、この方法を使用します。

private IEnumerable<T> InsertOrUpdate<T, TKey>(IEnumerable<T> entities, Func<T, TKey> idExpression) where T : class
{
    foreach (var entity in entities)
    {
        var existingEntity = _context.Set<T>().Find(idExpression(entity));
        if (existingEntity != null)
        {
            _context.Entry(existingEntity).CurrentValues.SetValues(entity);
            yield return existingEntity;
        }
        else
        {
            _context.Set<T>().Add(entity);
            yield return entity;
        }
    }
    _context.SaveChanges();
}

これにより、次のように呼び出して、基になるコレクションを挿入/更新できます。

movie.Genres = new List<Genre>(InsertOrUpdate(movie.Genres, x => x.TmdbId));

取得した値を元のルートオブジェクトに再割り当てする方法に注意してください。これで、アタッチされた各オブジェクトに接続されます。ルートオブジェクト(ムービー)の更新も同じ方法で行われます。

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}

1-Mの関係で削除をどのように処理していますか?たとえば、1-Movieは複数の言語を持つことができます。言語の1つが削除された場合、コードはそれを削除していますか?ソリューションは挿入または更新のみを行うようです(ただし、削除はしませんか?)
joedotnot

0

異なるフィールドidとを扱っているのでtmbid、ジャンル、言語、キーワードなどのすべての情報の単一の個別のインデックスを作成するようにAPIを更新することをお勧めします。次に、インデックスを呼び出して、収集するのではなく、情報を確認しますMovieクラスの特定のオブジェクトに関する情報全体。


1
私はここでの考えの流れには従いません。拡張できますか?外部APIは完全に私の制御外であることに注意してください。
Jeroen Vannevel 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.