web-dev-qa-db-ja.com

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

外部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; }
}
_

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

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

以前にこの問題がありました 言語を使用して、添付された言語オブジェクトを検索し、それらをコンテキストから切り離し、それらのPKを更新されたオブジェクトに移動して、それをコンテキストに添付しました。 SaveChanges()が実行されると、基本的に置き換えられます。

Movieオブジェクトに対してこのアプローチを続けると、映画、言語、ジャンル、キーワードを切り離して、データベースでそれぞれを検索する必要があるため、これはかなり臭いアプローチです。 IDを転送し、新しいオブジェクトを挿入します。

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

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

10
Jeroen Vannevel

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

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

注目に値するもの:同等性を判断する方法を具体的に選択できるようにするオーバーロードが利用可能です。ここでは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);
}
_
7
Jeroen Vannevel

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

0
Snazzy Sanoj