Bekerja dengan database dari suatu aplikasi

Pada awalnya saya akan menguraikan beberapa masalah dan fitur ketika bekerja dengan database, saya akan menunjukkan lubang pada abstraksi. Selanjutnya, kami akan menganalisis abstraksi yang lebih sederhana berdasarkan kekebalan.


Diasumsikan bahwa pembaca agak akrab dengan pola Rekaman Aktif , Pemeta Data , Peta Identitas, dan Unit Kerja .


Masalah dan solusi dipertimbangkan dalam konteks proyek yang cukup besar yang tidak dapat dibuang dan dengan cepat ditulis ulang.


Peta identitas


Masalah pertama adalah masalah mempertahankan identitas. Identitas adalah sesuatu yang secara unik mengidentifikasi entitas. Dalam database, ini adalah kunci utama, dan di memori, tautan (penunjuk). Ini bagus ketika tautan hanya menunjuk ke satu objek.


Untuk pustaka ActiveRecord ruby, ini tidak begitu:


post_a = Post.find 1 post_b = Post.find 1 post_a.object_id != post_b.object_id # true post_a.title = "foo" post_b.title != "foo" # true 

Yaitu kita mendapatkan 2 referensi ke 2 objek berbeda dalam memori.


Dengan demikian, kita bisa kehilangan perubahan jika kita secara tidak sengaja mulai bekerja dengan entitas yang sama, tetapi diwakili oleh objek yang berbeda.


Hibernate memiliki sesi, sebenarnya cache tingkat pertama yang menyimpan pemetaan pengenal entitas ke objek di memori. Jika kami meminta kembali entitas yang sama, kami akan mendapatkan tautan ke objek yang ada. Yaitu Hibernate mengimplementasikan pola Peta Identitas .


Transaksi panjang


Tetapi bagaimana jika kita tidak memilih dengan pengidentifikasi? Untuk mencegah keadaan objek dan keadaan database tidak sinkron, Hibernasi siram sebelum meminta pilihan.
yaitu dump objek kotor dalam database sehingga permintaan membaca data yang disepakati.


Pendekatan ini memaksa Anda untuk menjaga transaksi basis data tetap terbuka saat transaksi bisnis sedang berlangsung.
Jika transaksi bisnis panjang, maka proses yang bertanggung jawab untuk koneksi dalam database itu sendiri juga tidak aktif. Misalnya, ini dapat terjadi jika transaksi bisnis meminta data melalui jaringan atau melakukan perhitungan yang rumit.


N +1


Mungkin "lubang" terbesar dalam abstraksi ORM adalah masalah kueri N +1.


Contoh ruby โ€‹โ€‹untuk pustaka ActiveRecord:


 posts = Post.all # select * from posts posts.each do |post| like = post.likes.order(id: :desc).first # SELECT * FROM likes WHERE post_id = ? ORDER BY id DESC LIMIT 1 # ... end 

ORM menuntun programmer pada gagasan bahwa ia hanya bekerja dengan objek dalam memori. Tetapi bekerja dengan layanan yang tersedia melalui jaringan, dan pada pembentukan koneksi dan transfer data
itu butuh waktu. Bahkan jika permintaan dieksekusi 50 ms, maka 20 permintaan akan dieksekusi selama satu detik.


Data tambahan


Katakan untuk menghindari masalah N +1 yang dijelaskan di atas, Anda menulisnya
permintaan :


 SELECT * FROM posts JOIN LATERAL ( SELECT * FROM likes WHERE post_id = posts.id ORDER BY likes.id DESC LIMIT 1 ) as last_like ON true; 

Yaitu selain atribut posting, semua atribut seperti yang terakhir juga dipilih. Entitas apa yang dipetakan data ini? Dalam hal ini, Anda dapat mengembalikan pasangan dari pos dan suka, karena hasilnya berisi semua atribut yang diperlukan.


Tetapi bagaimana jika kita memilih hanya bagian dari bidang, atau bidang yang dipilih yang tidak ada dalam model, misalnya, jumlah suka publikasi? Apakah mereka perlu dipetakan ke entitas? Mungkin meninggalkan mereka hanya data?


Status & identitas


Pertimbangkan kode js:


 const alice = { id: 0, name: 'Alice' }; 

Di sini, referensi objek diberi nama alice . Karena itu adalah sebuah konstanta, maka tidak ada cara untuk memanggil Alice objek lain. Pada saat yang sama, objek itu sendiri tetap bisa berubah.


Misalnya, kami dapat menetapkan pengidentifikasi yang ada:


 const bob = { id: 1, name: 'Bob' }; alice.id = bob.id; 

Biarkan saya mengingatkan Anda bahwa suatu entitas memiliki 2 identitas: tautan dan kunci utama dalam database. Dan konstanta tidak bisa berhenti membuat Alice Bob, bahkan setelah menabung.


Objek, tautan yang kami sebut alice , melakukan 2 tugas: secara simultan memodelkan identitas dan status. Status adalah nilai yang menggambarkan entitas pada titik waktu tertentu.


Tetapi bagaimana jika kita memisahkan 2 tanggung jawab ini dan menggunakan struktur abadi untuk negara?


 function Ref(initialState, validator) { let state = initialState; this.deref = () => state; this.swap = (updater) => { const newState = updater(state); if (! validator(state, newState) ) throw "Invalid state"; state = newState; return newState; }; } const UserState = Immutable.Record({ id: null, name: '' }); const aliceState = new UserState({id: 0, name: 'Alice'}); const alice = new Ref( aliceState, (oldS, newS) => oldS.id === newS.id ); alice.swap( oldS => oldS.set('name', 'Queen Alice') ); alice.swap( oldS => oldS.set('id', 1) ); // BOOM! 

Ref - wadah untuk keadaan tidak berubah, memungkinkan penggantian yang terkontrol. Ref model identitas sama seperti kita beri nama objek. Kami menyebutnya Sungai Volga, tetapi setiap saat memiliki keadaan yang tidak berubah.


Penyimpanan


Pertimbangkan API berikut:


 storage.tx( t => { const alice = t.get(0); const bobState = new UserState({id: 1, name: 'Bob'}); const bob = t.create(bobState); alice.swap( oldS => oldS.update('friends', old => old.push(bob.deref.id)) ); }); 

t.get dan t.create kembalikan instance dari Ref .


Kami membuka transaksi bisnis t , menemukan Alice dengan pengenalnya, menciptakan Bob dan menunjukkan bahwa Alice menganggap Bob sebagai temannya.


Objek t mengontrol pembuatan ref .


t dapat menyimpan dalam dirinya sendiri pemetaan pengidentifikasi entitas ke negara ref yang ref . Yaitu dapat mengimplementasikan Identity Map. Dalam hal ini, t bertindak sebagai cache, atas permintaan Alice yang berulang, tidak akan ada permintaan ke database.


t dapat mengingat keadaan awal entitas untuk melacak pada akhir transaksi yang perubahannya harus ditulis ke database. Yaitu dapat mengimplementasikan Unit Kerja . Atau, jika dukungan pengamat ditambahkan ke Ref , dimungkinkan untuk mereset perubahan ke database dengan setiap perubahan dalam ref . Ini adalah pendekatan optimis dan pesimis untuk memperbaiki perubahan.


Dengan pendekatan optimis, Anda perlu melacak versi entitas yang ada.
Ketika mengubah dari database, kita harus mengingat versi, dan ketika melakukan perubahan, periksa bahwa versi entitas dalam database tidak berbeda dari yang awal. Jika tidak, Anda perlu mengulangi transaksi bisnis. Pendekatan ini memungkinkan penggunaan operasi penyisipan dan penghapusan grup dan transaksi basis data yang sangat singkat, yang menghemat sumber daya.


Dengan pendekatan pesimistis, transaksi basis data sepenuhnya konsisten dengan transaksi bisnis. Yaitu kami terpaksa menarik koneksi dari kolam semua pada saat transaksi bisnis selesai.


API memungkinkan Anda untuk mengekstraksi entitas satu per satu, yang sangat tidak optimal. Karena kami telah menerapkan pola Peta Identitas , maka kami dapat memasukkan metode preload di API:


 storage.tx( t => { t.preload([0, 1, 2, 3]); const alice = t.get(0); // from cache }); 

Pertanyaan


Jika kita tidak ingin transaksi lama, maka kita tidak dapat membuat pilihan dengan kunci arbitrer, karena memori mungkin berisi benda-benda kotor dan seleksi akan mengembalikan hasil yang tidak terduga.


Kami dapat menggunakan Kueri dan mengambil data (keadaan) di luar transaksi dan membaca kembali data saat dalam transaksi.


 const aliceId = userQuery.findByEmail('alice@mail.com'); storage.tx( t => { const alice = t.getOne(aliceId); }); 

Dengan demikian ada pembagian tanggung jawab. Untuk kueri, kita dapat menggunakan mesin pencari untuk membaca skala menggunakan replika. Dan API penyimpanan selalu bekerja dengan penyimpanan utama (master). Tentu, replika akan berisi data yang sudah ketinggalan zaman, membaca kembali data dalam transaksi memecahkan masalah ini.


Perintah


Ada situasi ketika operasi dapat dilakukan tanpa membaca data. Misalnya, kurangi biaya bulanan dari akun semua pelanggan. Atau masukkan dan perbarui data (siaga) jika terjadi konflik.


Jika terjadi masalah kinerja, bundel dari Storage and Query dapat diganti dengan perintah seperti itu.


Komunikasi


Jika entitas secara acak merujuk satu sama lain, sulit untuk memastikan konsistensi ketika mengubahnya. Hubungan mencoba menyederhanakan, merampingkan, mengabaikan yang tidak perlu.


Agregat adalah cara untuk mengatur hubungan. Setiap agregat memiliki entitas root dan entitas bersarang. Entitas eksternal apa pun dapat merujuk hanya ke akar agregat. Root memastikan integritas seluruh unit. Transaksi tidak dapat melewati batas agregat, dengan kata lain, seluruh agregat terlibat dalam transaksi.


Agregat dapat, misalnya, terdiri dari Prapaskah (root) dan terjemahannya. Atau Ordo dan Posisinya.


API kami bekerja dengan seluruh agregat. Pada saat yang sama, integritas referensial antara agregat terletak pada aplikasi. API tidak mendukung pemuatan tautan yang malas.
Tapi kita bisa memilih arah hubungan. Pertimbangkan hubungan satu-ke-banyak Pengguna - Posting. Kami dapat menyimpan ID pengguna di pos, tetapi apakah itu akan nyaman? Kami akan mendapatkan lebih banyak informasi jika kami menyimpan array pengidentifikasi pos di pengguna.


Kesimpulan


Saya menekankan masalah ketika bekerja dengan database, menunjukkan opsi untuk menggunakan kekebalan.
Format artikel tidak memungkinkan untuk mengungkapkan topik secara terperinci.


Jika Anda tertarik dengan pendekatan ini, maka perhatikan aplikasi buku saya dari awal , yang menjelaskan pembuatan aplikasi web dari awal dengan penekanan pada arsitektur. Ia memahami SOLID, Clean Architecture, pola bekerja dengan database. Sampel kode dalam buku dan aplikasi itu sendiri ditulis dalam bahasa Clojure, yang diilhami oleh ide-ide kekebalan dan kenyamanan pemrosesan data.

Source: https://habr.com/ru/post/id433100/


All Articles