Saat mempelajari LINQ, saya hampir mengalami kesulitan, yaitu operasi pembaruan database seperti yang Anda lihat di judul. Sekarang saya akan membawa Anda selangkah demi selangkah ke dalam rawa ini. Mohon siapkan batu bata dan air liur Anda, ikuti saya.
Mari kita mulai dengan kasus yang paling sederhana. Mari kita ambil database Northwind sebagai contoh. Saat Anda perlu mengubah Nama Produk suatu produk, Anda dapat langsung menulis kode berikut pada klien:
// Daftar 0NorthwindDataContext db = new NorthwindDataContext();
Produk produk = db.Produk.Tunggal(p => p.ProductID == 1);
product.ProductName = "Chai Berubah";
db.KirimPerubahan();
Uji dan pembaruan berhasil. Namun, saya yakin kode tersebut tidak akan muncul di proyek Anda karena tidak mungkin untuk digunakan kembali. Oke, mari kita refactor dan mengekstraknya menjadi sebuah metode. Apa yang harus menjadi parameternya? adalah nama produk baru dan ID produk yang akan diperbarui. Sepertinya memang begitu.
public void UpdateProduct(int id, string nama produk)
{
NorthwindDataContext db = baru NorthwindDataContext();
Produk produk = db.Produk.Tunggal(p => p.ProductID == id);
produk.NamaProduk = NamaProduk;
db.KirimPerubahan();
}Dalam proyek sebenarnya, kami tidak bisa begitu saja mengubah nama produk. Bidang Produk lainnya juga dapat diubah. Maka tanda tangan dari metode UpdateProduct akan menjadi seperti berikut:
kekosongan publik UpdateProduct(int id,
string Nama Produk,
ke dalam suplierId,
int kategoriId,
kuantitas stringPerUnit,
satuan desimalHarga,
unit pendekInStock,
unit pendekOnOrder,
short reorderLevel) Tentu saja, ini hanyalah database sederhana. Dalam proyek sebenarnya, tidak jarang terdapat dua puluh, tiga puluh, atau bahkan ratusan bidang. Siapa yang bisa mentoleransi metode seperti itu? Jika Anda menulis seperti ini, apa yang dilakukan objek Produk?
Itu benar, gunakan Produk sebagai parameter metode dan lemparkan operasi penugasan yang mengganggu ke kode klien. Pada saat yang sama, kami mengekstrak kode untuk mendapatkan instance Produk untuk membentuk metode GetProduct, dan memasukkan metode yang terkait dengan operasi basis data ke dalam kelas ProductRepository yang secara khusus bertanggung jawab untuk menangani basis data. Oh ya, SRP!
// Daftar 1
// Repositori Produk
Produk publik DapatkanProduk(int id)
{
NorthwindDataContext db = baru NorthwindDataContext();
return db.Produk.SingleOrDefault(p => p.id == id);
}
public void PerbaruiProduk(Produk produk)
{
NorthwindDataContext db = baru NorthwindDataContext();
db.Produk.Lampirkan(produk);
db.KirimPerubahan();
}
//Kode klien
Repositori ProductRepository = ProductRepository baru();
Produk produk = repositori.GetProduct(1);
product.ProductName = "Chai Berubah";
repositori.UpdateProduct(produk);
Di sini saya menggunakan metode Lampirkan untuk melampirkan instance Produk ke DataContext lainnya. Untuk database Northwind default, hasilnya adalah pengecualian berikut:
// Pengecualian 1 NotSupportException:
Mencoba Melampirkan atau Menambahkan entitas, entitas tersebut bukan entitas baru dan mungkin telah dimuat dari DataContext lain. Operasi ini tidak didukung.
Upaya telah dilakukan untuk Melampirkan atau Menambahkan entitas yang bukan baru,
Mungkin telah dimuat dari DataContext lain. Ini tidak didukung. Melihat MSDN kita tahu bahwa ketika membuat serial entitas ke klien, entitas ini terlepas dari DataContext aslinya. DataContext tidak lagi melacak perubahan pada entitas ini atau hubungannya dengan objek lain. Jika Anda ingin memperbarui atau menghapus data saat ini, Anda harus menggunakan metode Lampirkan untuk melampirkan entitas ke DataContext baru sebelum memanggil SubmitChanges, jika tidak, pengecualian di atas akan dilempar.
Dalam database Northwind, kelas Produk berisi tiga kelas terkait (yaitu, asosiasi kunci asing): Detail_Pesanan, Kategori, dan Pemasok. Dalam contoh di atas, meskipun kita Lampirkan Produk, tidak ada kelas Lampirkan yang terkait dengannya, sehingga NotSupportException dilempar.
Jadi bagaimana cara mengaitkan kelas yang terkait dengan Produk? Ini mungkin tampak rumit, bahkan untuk database sederhana seperti Northwind. Tampaknya pertama-tama kita harus mendapatkan kelas asli Order_Detail, Kategori, dan Pemasok yang terkait dengan Produk asli, lalu Melampirkannya masing-masing ke DataContext saat ini, namun kenyataannya, meskipun kita melakukan ini, NotSupportException akan dilempar.
Jadi bagaimana cara mengimplementasikan operasi pembaruan? Untuk mempermudah, kami menghapus kelas entitas lain di Northwind.dbml dan hanya menyimpan Produk. Dengan cara ini, kita bisa memulai analisis dari kasus yang paling sederhana.
Setelah menghapus kelas lain karena masalah, kami mengeksekusi kembali kode di Daftar 1, tetapi database tidak mengubah nama produk. Dengan melihat versi metode Attach yang kelebihan beban, kita dapat dengan mudah menemukan masalahnya.
Metode Attach(entity) memanggil kelebihan Attach(entity, false) secara default, yang akan melampirkan entitas terkait dalam keadaan tidak dimodifikasi. Jika objek Produk belum diubah, maka kita harus memanggil versi kelebihan beban ini untuk melampirkan objek Produk ke DataContext dalam keadaan tidak dimodifikasi untuk operasi selanjutnya. Saat ini, status objek Produk adalah "dimodifikasi", dan kita hanya dapat memanggil metode Attach(entity, true) .
Jadi kami mengubah kode yang relevan di Daftar 1 menjadi Lampirkan(produk, benar) dan lihat apa yang terjadi?
// Pengecualian 2 InvalidOperationException:
Jika suatu entitas mendeklarasikan anggota versi atau tidak memiliki kebijakan pemeriksaan pembaruan, entitas tersebut hanya dapat dilampirkan sebagai entitas yang dimodifikasi tanpa status aslinya.
Suatu entitas hanya dapat dilampirkan sebagai dimodifikasi tanpa keadaan aslinya
jika ia mendeklarasikan anggota versi atau tidak memiliki kebijakan pemeriksaan pembaruan.
LINQ ke SQL menggunakan kolom RowVersion untuk mengimplementasikan pemeriksaan konkurensi optimis default, jika tidak, kesalahan di atas akan terjadi saat melampirkan entitas ke DataContext dalam keadaan diubah. Ada dua cara untuk mengimplementasikan kolom RowVersion. Salah satunya adalah dengan menentukan kolom jenis stempel waktu untuk tabel database, dan cara lainnya adalah dengan menentukan atribut IsVersion=true pada atribut entitas yang sesuai dengan kunci utama tabel. Perhatikan bahwa Anda tidak dapat memiliki kolom TimeStamp dan atribut IsVersion=true secara bersamaan, jika tidak, InvalidOprationException akan ditampilkan: Anggota "System.Data.Linq.Binary TimeStamp" dan "Int32 ProductID" keduanya ditandai sebagai versi baris. Pada artikel ini, kami menggunakan kolom stempel waktu sebagai contoh.
Setelah membuat kolom bernama TimeStamp dan ketik stempel waktu untuk tabel Produk, seret kembali ke desainer, lalu jalankan kode di Daftar 1. Alhamdulillah, akhirnya berhasil.
Sekarang, kita tarik tabel Kategori ke dalam desainer. Saya mempelajari pelajaran kali ini dan pertama-tama menambahkan kolom stempel waktu ke tabel Kategori. Setelah diuji ternyata error di Exception 1 lagi! Setelah menghapus kolom stempel waktu Kategori, masalahnya tetap ada. Ya Tuhan, apa sebenarnya yang dilakukan dalam metode Attach yang mengerikan itu?
Oh iya, ada versi metode Lampirkan yang kelebihan beban, ayo kita coba.
public void PerbaruiProduk(Produk produk)
{
NorthwindDataContext db = baru NorthwindDataContext();
Produk oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
db.Produk.Lampirkan(produk, Produk lama);
db.KirimPerubahan();
} Atau kesalahan Pengecualian 1!
aku akan jatuh! Lampirkan, Lampirkan, apa yang terjadi padamu?
Untuk menjelajahi kode sumber LINQ ke SQL, kami menggunakan plug-in FileDisassembler Reflector untuk mendekompilasi System.Data.Linq.dll menjadi kode cs dan menghasilkan file proyek, yang membantu kami menemukan dan menemukannya di Visual Studio.
Kapan Pengecualian 1 dilempar?
Pertama-tama kita temukan informasi yang dijelaskan dalam Pengecualian 1 dari System.Data.Linq.resx dan dapatkan kunci "CannotAttachAddNonNewEntities", lalu temukan metode System.Data.Linq.Error.CannotAttachAddNonNewEntities(), temukan semua referensi ke metode ini, dan temukan bahwa ada dua metode yang digunakan di tiga tempat yaitu metode StandardChangeTracker.Track dan metode InitializeDeferredLoader.
Kita membuka kode Table.Attach(entity, bool) lagi, dan seperti yang diharapkan kita menemukan bahwa kode tersebut memanggil metode StandardChangeTracker.Track (hal yang sama juga berlaku untuk metode Lampirkan(entitas, entitas)):
trackedObject = this.context.Services.ChangeTracker.Track(entity, true); Dalam metode Track, kode berikut menampilkan Pengecualian 1:
jika (trackedObject.HasDeferredLoaders)
{
melempar System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}Jadi kita mengalihkan perhatian kita ke properti StandardTrackedObject.HasDeferredLoaders:
penggantian internal bool HasDeferredLoaders
{
mendapatkan
{
foreach (Asosiasi MetaAssociation di this.Type.Associations)
{
if (ini.HasDeferredLoader(asosiasi.ThisMember))
{
kembali benar;
}
}
foreach (anggota MetaDataMember dari p di this.Type.PersistentDataMembers
di mana p.IsDeferred && !p.IsAssociation
pilih p)
{
jika (ini.HasDeferredLoader(anggota))
{
kembali benar;
}
}
kembali salah;
}
} Dari sini kita secara kasar dapat menyimpulkan bahwa selama ada item yang lambat dimuat dalam entitas, operasi Lampirkan akan memunculkan Pengecualian 1. Hal ini persis sejalan dengan skenario di mana Pengecualian 1 terjadi - kelas Produk berisi item yang dimuat dengan lambat.
Kemudian muncul cara untuk menghindari pengecualian ini - hapus item yang perlu ditunda pemuatannya ke dalam Produk. Bagaimana cara menghapusnya? Anda dapat menggunakan DataLoadOptions untuk segera memuat, atau menyetel item yang memerlukan pemuatan lambat ke nol. Namun cara pertama tidak berhasil, jadi saya harus menggunakan cara kedua.
// Daftar 2
kelas ProdukRepositori
{
Produk publik DapatkanProduk(int id)
{
NorthwindDataContext db = baru NorthwindDataContext();
return db.Products.SingleOrDefault(p => p.ProductID == id);
}
Produk publik DapatkanProdukNoDeffered(int id)
{
NorthwindDataContext db = baru NorthwindDataContext();
//OpsiDataLoadOptions = DataLoadOptions baru();
//options.LoadWith<Produk>(p => p.Kategori);
//db.LoadOptions = opsi;
var produk = db.Produk.SingleOrDefault(p => p.ProductID == id);
produk.Kategori = null;
pengembalian produk;
}
public void PerbaruiProduk(Produk produk)
{
NorthwindDataContext db = baru NorthwindDataContext();
db.Produk.Lampirkan(produk, benar);
db.KirimPerubahan();
}
}
//Kode klien
Repositori ProductRepository = ProductRepository baru();
Produk produk = repositori.GetProductNoDeffered(1);
product.ProductName = "Chai Berubah";
repositori.UpdateProduct(produk);
Kapan Pengecualian 2 dilempar?
Mengikuti metode di bagian sebelumnya, kami dengan cepat menemukan kode yang memunculkan Pengecualian 2. Untungnya, hanya ada satu ini di keseluruhan proyek:
if (asModified && ((inheritanceType.VersionMember == null) && legacyType.HasUpdateCheck))
{
melempar System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
Seperti yang Anda lihat, jika parameter kedua asModified dari Attach benar, tidak berisi kolom RowVersion (VersionMember=null), dan berisi kolom pemeriksaan pembaruan (HasUpdateCheck), Pengecualian 2 akan dilempar. Kode HasUpdateCheck adalah sebagai berikut:
penggantian publik bool HasUpdateCheck
{
mendapatkan
{
foreach (anggota MetaDataMember di ini.PersistentDataMembers)
{
if (anggota.UpdateCheck!= UpdateCheck.Never)
{
kembali benar;
}
}
kembali salah;
}
}Ini juga konsisten dengan skenario kami - tabel Produk tidak memiliki kolom RowVersion, dan dalam kode yang dibuat secara otomatis oleh perancang, properti UpdateCheck dari semua bidang adalah selalu default, yaitu properti HasUpdateCheck benar.
Cara menghindari Pengecualian 2 bahkan lebih sederhana, tambahkan kolom TimeStamp ke semua tabel atau setel bidang IsVersion=true pada bidang kunci utama semua tabel. Karena metode terakhir memodifikasi kelas yang dibuat secara otomatis dan dapat ditimpa dengan desain baru kapan saja, saya sarankan menggunakan metode sebelumnya.
Bagaimana cara menggunakan metode Lampirkan?
Setelah analisis di atas, kita dapat mengetahui dua kondisi yang terkait dengan metode Lampirkan: apakah terdapat kolom RowVersion dan apakah terdapat asosiasi kunci asing (yaitu, item yang perlu dimuat lambat). Saya merangkum kedua kondisi ini dan penggunaan beberapa Attachment yang berlebihan ke dalam sebuah tabel. Jika melihat tabel di bawah ini, Anda harus benar-benar siap secara mental.
nomor seri
Lampirkan metode
Apakah kolom RowVersion memiliki deskripsi terkait
1 Lampirkan (entitas) Tidak Tidak Tidak ada modifikasi
2 Lampirkan (entitas) Tidak Ya NotSupportException: Entitas Lampirkan atau Tambah telah dicoba. Entitas tersebut bukan entitas baru dan dapat diambil dari DataContexts lainnya. Operasi ini tidak didukung.
3 Lampirkan (entitas) Apakah tidak ada modifikasi
4 Lampirkan (entitas) tidak diubah. Sama seperti 2 jika subset tidak memiliki kolom RowVersion.
5 Lampirkan (entitas, benar) Tidak Tidak InvalidOperationException: Jika entitas mendeklarasikan anggota versi atau tidak memiliki kebijakan pemeriksaan pembaruan, entitas hanya dapat dilampirkan sebagai entitas yang dimodifikasi tanpa status asli.
6 Lampirkan (entitas, benar) Tidak Ya NotSupportException: Entitas Lampirkan atau Tambah telah dicoba. Entitas tersebut bukan entitas baru dan dapat diambil dari DataContexts lainnya. Operasi ini tidak didukung.
7 Lampirkan (entitas, benar) Apakah modifikasinya normal (memodifikasi kolom RowVersion secara paksa akan melaporkan kesalahan)
8 Lampirkan(entitas, benar) Ya NotSupportException: Entitas Lampirkan atau Tambah telah dicoba. Entitas tersebut bukan entitas baru dan dapat diambil dari DataContexts lainnya. Operasi ini tidak didukung.
9 Lampirkan(entitas, entitas) Tidak Tidak Ada DuplikatKeyException: Tidak dapat menambahkan entitas yang kuncinya sudah digunakan.
10 Lampirkan (entitas, entitas) Tidak Ya NotSupportException: Entitas Lampirkan atau Tambah telah dicoba. Entitas tersebut bukan entitas baru dan dapat dimuat dari DataContexts lainnya. Operasi ini tidak didukung.
11 Lampirkan(entitas, entitas) DuplikatKeyException: Tidak dapat menambahkan entitas yang kuncinya sudah digunakan.
12 Lampirkan (entitas, entitas) Ya NotSupportException: Entitas Lampirkan atau Tambah telah dicoba. Entitas tersebut bukan entitas baru dan dapat dimuat dari DataContexts lainnya. Operasi ini tidak didukung.
Lampiran hanya dapat diperbarui secara normal dalam situasi ke-7 (termasuk kolom RowVersion dan tidak ada asosiasi kunci asing)! Situasi ini hampir mustahil untuk sistem berbasis database! API macam apa ini?
Ringkasan Mari kita tenang dan mulai meringkas.
Jika Anda menulis LINQ ke kode SQL langsung di UI seperti Daftar 0, tidak ada hal buruk yang akan terjadi. Namun jika Anda mencoba memisahkan lapisan akses data yang terpisah, bencana akan terjadi. Apakah ini berarti LINQ to SQL tidak cocok untuk pengembangan arsitektur multi-layer? Banyak orang mengatakan bahwa LINQ to SQL cocok untuk pengembangan sistem kecil, namun ukurannya yang kecil bukan berarti tidak berlapis. Apakah ada cara untuk menghindari begitu banyak pengecualian?
Artikel ini sebenarnya telah memberikan beberapa petunjuk. Dalam esai berikutnya dalam seri ini, saya akan mencoba memberikan beberapa solusi untuk dipilih semua orang.