Pertanyaan "ORM mana yang harus saya gunakan" benar-benar menargetkan ujung gunung es besar ketika datang ke strategi akses data keseluruhan dan optimasi kinerja dalam aplikasi skala besar.
Desain dan Pemeliharaan Basis Data
Ini, dengan selisih yang lebar, satu-satunya penentu terpenting dari throughput aplikasi berbasis data atau situs web, dan sering kali sama sekali diabaikan oleh para programmer.
Jika Anda tidak menggunakan teknik normalisasi yang tepat, situs Anda akan hancur. Jika Anda tidak memiliki kunci utama, hampir setiap permintaan akan berjalan lambat. Jika Anda menggunakan anti-pola terkenal seperti menggunakan tabel untuk Pasangan Nilai Kunci (AKA Entity-Attribute-Value) tanpa alasan yang baik, Anda akan meledak jumlah bacaan dan tulisan fisik.
Jika Anda tidak memanfaatkan fitur yang diberikan oleh database, seperti kompresi halaman, FILESTREAM
penyimpanan (untuk data biner), SPARSE
kolom, hierarchyid
untuk hierarki, dan sebagainya (semua contoh SQL Server), maka Anda tidak akan melihat di dekat kinerja yang bisa Anda lihat.
Anda harus mulai mengkhawatirkan strategi akses data Anda setelah merancang basis data dan meyakinkan diri sendiri bahwa itu sebaik mungkin, setidaknya untuk saat ini.
Pemuatan bersemangat vs malas
Sebagian besar ORM menggunakan teknik yang disebut lazy loading untuk hubungan, yang berarti bahwa secara default akan memuat satu entitas (baris tabel) sekaligus, dan melakukan perjalanan bolak-balik ke database setiap kali perlu memuat satu atau banyak yang terkait (asing). kunci) baris.
Ini bukan hal yang baik atau buruk, itu lebih tergantung pada apa yang sebenarnya akan dilakukan dengan data, dan seberapa banyak Anda tahu di muka. Terkadang lazy-loading adalah hal yang benar untuk dilakukan. NHibernate, misalnya, dapat memutuskan untuk tidak meminta apa pun dan hanya menghasilkan proxy untuk ID tertentu. Jika yang Anda butuhkan hanyalah ID itu sendiri, mengapa harus meminta lebih banyak? Di sisi lain, jika Anda mencoba untuk mencetak pohon dari setiap elemen tunggal dalam hirarki 3-level, pemuatan malas menjadi operasi O (N²), yang sangat buruk untuk kinerja.
Satu manfaat menarik untuk menggunakan "SQL murni" (yaitu, permintaan / prosedur tersimpan ADO.NET mentah) adalah pada dasarnya memaksa Anda untuk berpikir secara tepat data apa yang diperlukan untuk menampilkan layar atau halaman tertentu. ORMs dan fitur malas-loading tidak mencegah Anda dari melakukan hal ini, tetapi mereka tidak memberi Anda kesempatan untuk menjadi ... baik, malas , dan tidak sengaja meledak jumlah pertanyaan yang Anda jalankan. Jadi, Anda perlu memahami fitur pemuatan cepat ORM Anda dan selalu waspada tentang jumlah kueri yang Anda kirim ke server untuk setiap permintaan halaman yang diberikan.
Caching
Semua ORM utama mempertahankan cache tingkat pertama, "cache identitas" AKA, yang berarti bahwa jika Anda meminta entitas yang sama dua kali dengan ID-nya, itu tidak memerlukan perjalanan pulang-pergi kedua, dan juga (jika Anda mendesain database Anda dengan benar ) memberi Anda kemampuan untuk menggunakan konkurensi optimis.
Cache L1 cukup buram di L2S dan EF, Anda harus percaya bahwa itu berfungsi. NHibernate lebih eksplisit tentang hal itu ( Get
/ Load
vs. Query
/ QueryOver
). Namun, selama Anda mencoba untuk query dengan ID sebanyak mungkin, Anda harus baik-baik saja di sini. Banyak orang lupa tentang cache L1 dan berulang kali mencari entitas yang sama berulang-ulang dengan sesuatu selain ID-nya (yaitu bidang pencarian). Jika Anda perlu melakukan ini maka Anda harus menyimpan ID atau bahkan seluruh entitas untuk pencarian di masa mendatang.
Ada juga cache level 2 ("cache permintaan"). NHibernate memiliki fitur bawaan ini. Linq to SQL dan Entity Framework telah mengkompilasi kueri , yang dapat membantu mengurangi banyak server aplikasi dengan mengkompilasi ekspresi kueri itu sendiri, tetapi itu tidak menyimpan data. Microsoft tampaknya menganggap ini masalah aplikasi daripada masalah akses data, dan ini adalah titik kelemahan utama dari L2S dan EF. Tak perlu dikatakan itu juga merupakan titik lemah dari SQL "mentah". Untuk mendapatkan kinerja yang benar-benar bagus dengan ORM apa pun selain NHibernate, Anda perlu mengimplementasikan façade caching Anda sendiri.
Ada juga "ekstensi" cache L2 untuk EF4 yang baik - baik saja , tetapi tidak benar-benar pengganti grosir untuk cache tingkat aplikasi.
Jumlah Pertanyaan
Database relasional didasarkan pada set data. Mereka benar-benar hebat dalam menghasilkan data dalam jumlah besar dalam waktu singkat, tetapi mereka sama sekali tidak sebagus dalam hal latensi kueri karena ada sejumlah overhead tertentu yang terlibat dalam setiap perintah. Aplikasi yang dirancang dengan baik harus memainkan kekuatan dari DBMS ini dan mencoba untuk meminimalkan jumlah pertanyaan dan memaksimalkan jumlah data di masing-masing.
Sekarang saya tidak mengatakan untuk menanyakan seluruh database ketika Anda hanya perlu satu baris. Apa yang saya katakan adalah, jika Anda memerlukan Customer
, Address
, Phone
, CreditCard
, dan Order
baris semua pada waktu yang sama untuk melayani satu halaman, maka Anda harus meminta untuk mereka semua pada waktu yang sama, tidak mengeksekusi setiap query secara terpisah. Terkadang lebih buruk dari itu, Anda akan melihat kode yang meminta Customer
catatan yang sama 5 kali berturut-turut, pertama untuk mendapatkan Id
, lalu Name
, lalu EmailAddress
, kemudian ... itu sangat tidak efisien.
Bahkan jika Anda perlu menjalankan beberapa query yang semuanya beroperasi pada set data yang sama sekali berbeda, biasanya masih lebih efisien untuk mengirim semuanya ke database sebagai "skrip" tunggal dan mengembalikan beberapa set hasil. Ini adalah overhead yang Anda khawatirkan, bukan jumlah total data.
Ini mungkin terdengar seperti akal sehat tetapi seringkali sangat mudah untuk kehilangan jejak semua pertanyaan yang sedang dieksekusi di berbagai bagian aplikasi; Penyedia Keanggotaan Anda kueri tabel pengguna / peran, tindakan Header Anda kueri keranjang belanja, tindakan Menu Anda kueri tabel peta situs, tindakan Sidebar Anda menanyakan daftar produk unggulan, dan kemudian mungkin halaman Anda dibagi menjadi beberapa area otonom terpisah yang kueri tabel Riwayat Pesanan, Baru Dilihat, Kategori, dan Inventaris secara terpisah, dan sebelum Anda mengetahuinya, Anda mengeksekusi 20 kueri bahkan sebelum Anda dapat mulai melayani halaman. Itu benar-benar menghancurkan kinerja.
Beberapa kerangka kerja - dan saya berpikir terutama dari NHibernate di sini - sangat pandai tentang hal ini dan memungkinkan Anda untuk menggunakan sesuatu yang disebut futures yang mengumpulkan seluruh pertanyaan dan mencoba mengeksekusi semuanya sekaligus, pada menit terakhir yang memungkinkan. AFAIK, Anda sendirian jika ingin melakukan ini dengan salah satu teknologi Microsoft; Anda harus membuatnya menjadi logika aplikasi Anda.
Pengindeksan, Predikat, dan Proyeksi
Setidaknya 50% dari devs yang saya ajak bicara dan bahkan beberapa DBA tampaknya memiliki masalah dengan konsep mencakup indeks. Mereka berpikir, "well, Customer.Name
kolomnya diindeks, jadi setiap pencarian yang saya lakukan atas nama harus cepat." Kecuali itu tidak berfungsi seperti itu kecuali Name
indeks mencakup kolom spesifik yang Anda cari. Dalam SQL Server, yang dilakukan dengan INCLUDE
dalam CREATE INDEX
pernyataan.
Jika Anda menggunakan secara naif di SELECT *
mana-mana - dan itu lebih atau kurang dari apa yang akan dilakukan setiap ORM kecuali Anda secara eksplisit menentukan lain menggunakan proyeksi - maka DBMS mungkin sangat memilih untuk sepenuhnya mengabaikan indeks Anda karena mengandung kolom yang tidak tercakup. Proyeksi berarti bahwa, misalnya, daripada melakukan ini:
from c in db.Customers where c.Name == "John Doe" select c
Anda melakukan ini sebagai gantinya:
from c in db.Customers where c.Name == "John Doe"
select new { c.Id, c.Name }
Dan kehendak ini, untuk sebagian besar ORMs modern, menginstruksikan untuk hanya pergi dan query Id
dan Name
kolom yang mungkin ditutupi oleh indeks (tapi bukan Email
, LastActivityDate
, atau apa pun kolom yang kebetulan menempel di sana lain).
Ini juga sangat mudah untuk sepenuhnya menghilangkan manfaat pengindeksan dengan menggunakan predikat yang tidak pantas. Sebagai contoh:
from c in db.Customers where c.Name.Contains("Doe")
... terlihat hampir identik dengan permintaan kami sebelumnya tetapi pada kenyataannya akan menghasilkan tabel penuh atau pemindaian indeks karena diterjemahkan menjadi LIKE '%Doe%'
. Demikian pula, permintaan lain yang terlihat sederhana dan mencurigakan adalah:
from c in db.Customers where (maxDate == null) || (c.BirthDate >= maxDate)
Dengan asumsi Anda memiliki indeks BirthDate
, predikat ini memiliki peluang bagus untuk menjadikannya benar-benar tidak berguna. Programmer hipotetis kami di sini jelas telah berusaha membuat semacam kueri dinamis ("hanya filter tanggal lahir jika parameter itu ditentukan"), tetapi ini bukan cara yang tepat untuk melakukannya. Ditulis seperti ini sebagai gantinya:
from c in db.Customers where c.BirthDate >= (maxDate ?? DateTime.MinValue)
... sekarang mesin DB tahu bagaimana membuat parameter ini dan melakukan pencarian indeks. Satu perubahan kecil, yang tampaknya tidak signifikan terhadap ekspresi kueri dapat secara drastis memengaruhi kinerja.
Sayangnya LINQ secara umum membuat semuanya terlalu mudah untuk menulis kueri buruk seperti ini karena kadang - kadang penyedia dapat menebak apa yang Anda coba lakukan dan mengoptimalkan kueri, dan kadang-kadang tidak. Jadi, Anda berakhir dengan hasil yang sangat tidak konsisten yang pasti sangat menyolok (untuk DBA yang berpengalaman), seandainya Anda baru saja menulis SQL lama.
Pada dasarnya itu semua bermuara pada fakta bahwa Anda benar-benar harus mengawasi baik-baik SQL yang dihasilkan dan rencana eksekusi yang mereka tuju, dan jika Anda tidak mendapatkan hasil yang Anda harapkan, jangan takut untuk mem-bypass Lapisan ORM sesekali dan tangan-kode SQL. Ini berlaku untuk ORM apa pun , tidak hanya EF.
Transaksi dan Penguncian
Apakah Anda perlu menampilkan data yang terkini hingga milidetik? Mungkin - itu tergantung - tetapi mungkin tidak. Sayangnya, Entity Framework tidak memberi Andanolock
, Anda hanya dapat menggunakan READ UNCOMMITTED
di level transaksi (bukan level tabel). Faktanya tidak ada ORM yang bisa diandalkan tentang hal ini; jika Anda ingin melakukan pembacaan yang kotor, Anda harus turun ke tingkat SQL dan menulis pertanyaan ad-hoc atau prosedur yang tersimpan. Jadi intinya, sekali lagi, adalah betapa mudahnya bagi Anda untuk melakukan itu dalam kerangka kerja.
Entity Framework telah datang jauh dalam hal ini - versi 1 dari EF (dalam .NET 3.5) sangat mengerikan, membuatnya sangat sulit untuk menembus abstraksi "entitas", tetapi sekarang Anda memiliki ExecuteStoreQuery dan Translate , jadi itu benar-benar lumayan. Bertemanlah dengan orang-orang ini karena Anda akan sering menggunakannya.
Ada juga masalah menulis penguncian dan kebuntuan dan praktik umum memegang kunci dalam database sesedikit mungkin. Dalam hal ini, sebagian besar ORM (termasuk Entity Framework) sebenarnya cenderung lebih baik daripada SQL mentah karena mereka merangkum unit pola Kerja , yang dalam EF adalah SaveChanges . Dengan kata lain, Anda dapat "menyisipkan" atau "memperbarui" atau "menghapus" entitas ke isi hati Anda, kapan pun Anda mau, aman dengan pengetahuan bahwa tidak ada perubahan yang akan didorong ke database hingga Anda melakukan unit kerja.
Perhatikan bahwa UOW tidak analog dengan transaksi yang sudah berjalan lama. UOW masih menggunakan fitur konkurensi optimis dari ORM dan melacak semua perubahan dalam memori . Tidak satu pun pernyataan DML yang dikeluarkan sampai komit terakhir. Ini menjaga waktu transaksi serendah mungkin. Jika Anda membangun aplikasi menggunakan SQL mentah, cukup sulit untuk mencapai perilaku yang ditangguhkan ini.
Apa artinya ini untuk EF secara spesifik: Jadikan unit kerja Anda seringkas mungkin dan jangan komit sampai Anda benar-benar perlu. Lakukan ini dan Anda akan berakhir dengan pertentangan kunci yang jauh lebih rendah daripada yang Anda akan gunakan perintah ADO.NET individu pada waktu yang acak.
EF benar-benar baik untuk aplikasi lalu lintas tinggi / kinerja tinggi, sama seperti setiap kerangka kerja lainnya baik untuk aplikasi lalu lintas tinggi / kinerja tinggi. Yang penting adalah bagaimana Anda menggunakannya. Berikut ini adalah perbandingan cepat kerangka kerja paling populer dan fitur apa yang mereka tawarkan dalam hal kinerja (legenda: N = Tidak didukung, P = Sebagian, Y = ya / didukung):
Seperti yang Anda lihat, EF4 (versi saat ini) tidak terlalu mahal, tetapi mungkin bukan yang terbaik jika kinerja menjadi perhatian utama Anda. NHibernate jauh lebih matang di bidang ini dan bahkan Linq to SQL menyediakan beberapa fitur peningkatan kinerja yang EF masih belum. Raw ADO.NET sering akan lebih cepat untuk skenario akses data yang sangat spesifik , tetapi, ketika Anda menggabungkan semua bagian, itu benar-benar tidak menawarkan banyak manfaat penting yang Anda dapatkan dari berbagai kerangka kerja.