Bisakah saya memperbarui objek yang dilampirkan menggunakan objek yang terpisah tetapi sama?

10

Saya mengambil data film dari API eksternal. Pada fase pertama saya akan mengikis setiap film dan memasukkannya ke dalam database saya sendiri. Pada tahap kedua saya akan memperbarui basis data saya secara berkala dengan menggunakan API "Perubahan" API yang dapat saya minta untuk melihat film apa yang informasinya diubah.

Lapisan ORM saya adalah Entity-Framework. Kelas Film terlihat seperti ini:

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

Masalah muncul ketika saya memiliki film yang perlu diperbarui: database saya akan menganggap objek yang dilacak dan yang baru yang saya terima dari panggilan API pembaruan sebagai objek yang berbeda, mengabaikan .Equals().

Ini menyebabkan masalah karena ketika saya sekarang mencoba untuk memperbarui database dengan film yang diperbarui, itu akan memasukkannya daripada memperbarui Film yang ada.

Saya memiliki masalah ini sebelumnya dengan bahasa dan solusi saya adalah untuk mencari objek bahasa yang dilampirkan, melepaskan mereka dari konteks, memindahkan PK mereka ke objek yang diperbarui dan melampirkannya ke konteks. KapanSaveChanges() sekarang dieksekusi, pada dasarnya akan menggantinya.

Ini adalah pendekatan yang agak bau karena jika saya melanjutkan pendekatan ini ke saya Movie objek , itu berarti saya harus melepaskan film, bahasa, genre dan kata kunci, mencari masing-masing dalam database, mentransfer ID mereka dan memasukkan benda baru.

Apakah ada cara untuk melakukan ini dengan lebih elegan? Idealnya saya hanya ingin meneruskan film yang diperbarui ke konteks dan memilih film yang benar untuk diperbarui berdasarkan Equals()metode, memperbarui semua bidangnya dan untuk setiap objek yang kompleks: gunakan catatan yang ada lagi berdasarkan pada sendiriEquals() metode dan masukkan jika itu belum ada.

Saya dapat melewati detaching / attaching dengan memberikan .Update()metode pada setiap objek kompleks yang dapat saya gunakan dalam kombinasi mengambil semua objek yang dilampirkan tetapi ini masih akan mengharuskan saya untuk mengambil setiap objek yang ada untuk kemudian memperbaruinya.

Jeroen Vannevel
sumber
Mengapa Anda tidak bisa memperbarui entitas yang dilacak dari data API Anda dan menyimpan perubahan tanpa mengganti / memisahkan / mencocokkan / melampirkan entitas?
Si-N
@ Si-N: bisakah Anda mengembangkan bagaimana tepatnya itu?
Jeroen Vannevel
Ok sekarang Anda telah menambahkan bahwa paragraf terakhir lebih masuk akal, Anda mencoba menghindari mengambil entitas sebelum memperbaruinya? Tidak ada yang bisa menjadi PK Anda di kelas Film Anda? Bagaimana Anda mencocokkan film dari api eksternal ke entitas Anda? Anda dapat mengambil semua entitas yang perlu Anda perbarui dalam satu panggilan db, bukankah ini akan menjadi solusi yang lebih sederhana yang seharusnya tidak menyebabkan hit kinerja besar (kecuali jika Anda berbicara tentang sejumlah besar film untuk diperbarui)?
Si-N
Kelas Film saya memiliki PK iddan film dari API eksternal dicocokkan dengan yang lokal menggunakan bidang tmdbid. Saya tidak dapat mengambil semua entitas yang perlu diperbarui dalam satu panggilan karena ini tentang film, genre, bahasa, kata kunci, dll. Masing-masing memiliki PK dan mungkin sudah ada dalam database.
Jeroen Vannevel

Jawaban:

8

Saya tidak menemukan apa yang saya harapkan tetapi saya menemukan perbaikan atas urutan select-detach-update-attach yang ada.

Metode ekstensi AddOrUpdate(this DbSet)memungkinkan Anda untuk melakukan apa yang ingin saya lakukan: Masukkan jika tidak ada dan perbarui jika menemukan nilai yang ada. Saya tidak menyadari menggunakan ini lebih cepat karena saya benar-benar hanya melihatnya digunakan dalam seed()metode dalam kombinasi dengan Migrasi. Jika ada alasan saya tidak boleh menggunakan ini, beri tahu saya.

Sesuatu yang berguna untuk dicatat: Ada kelebihan yang tersedia yang memungkinkan Anda memilih secara khusus bagaimana kesetaraan harus ditentukan. Di sini saya bisa menggunakan saya TMDbIdtetapi saya memilih untuk mengabaikan ID saya sendiri dan menggunakan PK pada TMDbId yang dikombinasikan DatabaseGeneratedOption.None. Saya menggunakan pendekatan ini pada setiap subkoleksi juga, jika perlu.

Bagian yang menarik dari sumber :

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

begitulah data sebenarnya diperbarui di bawah tenda.

Semua yang tersisa memanggil AddOrUpdatesetiap objek yang saya ingin terpengaruh oleh ini:

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

Ini tidak sebersih yang saya harapkan karena saya harus secara manual menentukan setiap bagian dari objek saya yang perlu diperbarui tapi itu sedekat yang akan didapat.

Bacaan terkait: /programming/15336248/entity-framework-5-updating-a-record


Memperbarui:

Ternyata tes saya tidak cukup ketat. Setelah menggunakan teknik ini saya perhatikan bahwa sementara bahasa baru ditambahkan, itu tidak terhubung ke film. di tabel banyak ke banyak. Ini adalah masalah yang diketahui tetapi tampaknya prioritas rendah dan belum diperbaiki sejauh yang saya tahu.

Pada akhirnya saya memutuskan untuk pergi ke pendekatan di mana saya memiliki Update(T)metode pada setiap jenis dan mengikuti urutan peristiwa ini:

  • Ulangi koleksi di objek baru
  • Untuk setiap entri di setiap koleksi, cari di database
  • Jika ada, gunakan Update()metode untuk memperbaruinya dengan nilai-nilai baru
  • Jika tidak ada, tambahkan ke DbSet yang sesuai
  • Kembalikan objek terlampir dan ganti koleksi di objek root dengan koleksi objek terlampir
  • Temukan dan perbarui objek root

Ini banyak pekerjaan manual dan jelek jadi itu akan melalui beberapa refactoring lagi tapi sekarang tes saya menunjukkan itu harus bekerja untuk skenario yang lebih ketat.


Setelah membersihkannya lebih lanjut saya sekarang menggunakan metode ini:

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

Ini memungkinkan saya untuk menyebutnya seperti ini dan menyisipkan / memperbarui koleksi yang mendasarinya:

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

Perhatikan bagaimana saya menetapkan kembali nilai yang diambil ke objek root asli: sekarang terhubung ke setiap objek yang dilampirkan. Memperbarui objek root (film) dilakukan dengan cara yang sama:

var localMovie = _context.Movies.SingleOrDefault(x => x.TmdbId == movie.TmdbId);
if (localMovie == null)
{
    _context.Movies.Add(movie);
} 
else
{
    _context.Entry(localMovie).CurrentValues.SetValues(movie);
}
Jeroen Vannevel
sumber
Bagaimana Anda menangani Hapus dalam hubungan 1-M? misalnya, 1-Film dapat memiliki banyak bahasa; jika salah satu bahasa dihapus, apakah kode Anda menghapusnya? Tampaknya solusi Anda hanya menyisipkan dan atau pembaruan (tetapi tidak menghapus?)
joedotnot
0

Karena Anda berurusan dengan bidang yang berbeda iddan tmbid, saya sarankan memperbarui API untuk membuat indeks tunggal dan terpisah dari semua info seperti genre, bahasa, kata kunci dll ... Dan kemudian membuat panggilan untuk mengindeks dan memeriksa info daripada mengumpulkan seluruh info tentang objek spesifik di kelas Film Anda.

Snazzy Sanoj
sumber
1
Saya tidak mengikuti jalur pemikiran di sini. Bisakah kamu berkembang? Perhatikan bahwa API eksternal sepenuhnya di luar kendali saya.
Jeroen Vannevel