Bagaimana saya menolak db4o dalam sistem industri

gambar


Kami adalah departemen perusahaan besar yang mengembangkan sistem Java SE / MS SQL / db4o yang penting. Selama beberapa tahun, proyek beralih dari prototipe ke operasi industri dan db4o berubah menjadi rem perhitungan, saya ingin beralih dari db4o ke teknologi noSQL modern. Trial and error mengarah jauh dari rencana semula - adalah mungkin untuk menolak db4o, tetapi dengan mengorbankan kompromi. Di bawah refleksi kucing dan detail implementasi.


Apakah teknologi db4o mati?


Di Habré dimungkinkan untuk menemukan tidak begitu banyak publikasi tentang db4o. Di Stackoverflow, ada beberapa aktivitas residual seperti komentar baru pada pertanyaan lama atau pertanyaan baru yang belum terjawab . Wiki umumnya percaya bahwa versi stabil saat ini tertanggal 2011.


Ini membentuk kesan umum: teknologinya tidak relevan. Bahkan ada konfirmasi resmi : Actian memutuskan untuk tidak secara aktif mengejar dan mempromosikan penawaran produk db4o komersial untuk pelanggan baru lagi.


Bagaimana db4o bisa dihitung


Artikel Pengantar Database Berorientasi Objek berbicara tentang fitur utama db4o - tidak adanya skema data. Anda dapat membuat objek apa pun


User user1 = new User("Vasya", "123456", 25); 

dan kemudian hanya menulisnya ke file database


 db.Store(user1) 

Objek yang direkam kemudian dapat diambil menggunakan metode Query.execute () dalam bentuk di mana ia disimpan.


Pada awal proyek, ini memungkinkan untuk dengan cepat memastikan tampilan jejak audit dengan semua data yang disampaikan, tanpa mengganggu struktur tabel relasional. Ini membantu proyek bertahan. Lalu ada beberapa sumber daya di kotak pasir, dan segera setelah akhir perhitungan hari ini, data untuk besok mulai memuat di MS SQL. Semuanya terus berubah - cari tahu apa yang sebenarnya disajikan secara otomatis di malam hari. Dan file db4o dapat diakses
dalam debugging, ekstrak snapshot dari hari yang diinginkan dan jawab pertanyaan "kami mengirimkan semua data, tetapi Anda tidak memesan apa pun."


Seiring waktu, masalah kelangsungan hidup menghilang, proyek lepas landas, pekerjaan dengan permintaan pengguna telah berubah. Buka file db4o dalam debugging dan parse pertanyaan yang sulit bisa pengembang yang selalu sibuk. Sebagai gantinya, ada kerumunan analis, yang dipersenjatai dengan deskripsi logika pesanan dan hanya dapat menggunakan bagian data yang dapat dilihat pengguna. Segera db4o menjadi digunakan hanya untuk menampilkan sejarah perhitungan. Sama seperti Pareto - sebagian kecil dari kemampuan menyediakan beban utama.


Dalam operasi tempur, file histori memakan waktu ~ 35 GB / hari, pembongkaran membutuhkan waktu sekitar satu jam. File itu sendiri kompres dengan baik (1:10), tetapi untuk beberapa alasan perpustakaan com.db4o.ObjectContainer tidak melakukan kompresi. Di utara CentOS, perpustakaan com.db4o.query.Query menulis / membaca file secara eksklusif untuk satu aliran. Kecepatan adalah hambatan.


Diagram skematis perangkat


Model informasi sistem adalah hierarki objek A, B, C, dan D. Hirarki bukan pohon, tautan C1 -> B1 diperlukan untuk operasi.


 ROOT || | ==>A1 | || | | ==> B1 <------ | | || | | | | ======> C1 | | | | | | | ===> C1.$D | | =======> C2 | | | | ==> B2 ==> C2.$D | | ===>A2 =======> C3 | | ==> B3 ===> C3.$D | ======> C4 | ===> C4.$D 

Pengguna berinteraksi dengan server melalui antarmuka pengguna (GUI), yang disediakan oleh com.sun.net.httpserver.HttpsServer, klien dan server bertukar dokumen XML. Pada tampilan pertama, server menetapkan pengidentifikasi ke tingkat pengguna, yang tidak berubah lebih lanjut. Jika pengguna memerlukan riwayat beberapa tingkat, GUI mengirimkan server pengenal yang dibungkus XML. Server menentukan nilai kunci untuk mencari basis data, memindai file db4o untuk hari yang diinginkan dan mengambil objek yang diminta dalam memori ditambah semua objek yang dirujuk. Membangun presentasi XML dari tingkat yang diekstraksi dan mengembalikannya ke klien.


Saat memindai file, db40 secara default membaca semua objek anak hingga kedalaman tertentu, mengekstraksi hierarki yang agak besar bersama-sama dengan objek yang diinginkan. Waktu membaca dapat dikurangi dengan menetapkan kedalaman aktivasi minimum untuk kelas Foo yang tidak perlu dengan conf.common (). ObjectClass (Foo.class) .maximumActivationDepth (1).


Menggunakan kelas anonim mengarah ke pembuatan referensi implisit ke kelas terlampir $ 0 ini . Db4o memproses dan mengembalikan tautan seperti itu dengan benar (tetapi lambat).


0. Ide


Jadi, admin memiliki ekspresi aneh di wajah mereka ketika datang untuk mendukung atau mengelola db4o. Ekstraksi data lambat, teknologinya tidak terlalu hidup. Tugas: alih-alih db4o, terapkan teknologi NoSQL saat ini. Sepasang Spring Data + MongoDB menarik perhatian saya.


1. Pendekatan frontal


Pikiran pertama saya adalah menggunakan org.springframework.data.mongodb.core.MongoOperations dan metode save (), karena terlihat seperti com.db4o.ObjectContainer.db.Store (user1). Dokumentasi MongoDB mengatakan bahwa dokumen disimpan dalam koleksi, adalah logis untuk menyajikan objek sistem yang diperlukan sebagai dokumen dari koleksi yang sesuai. Ada juga penjelasan @ DBRef yang memungkinkan Anda untuk menerapkan hubungan antar dokumen secara umum dalam semangat 3NF . Ayo pergi.


1.1. Bongkar Jenis Referensi Kunci


Sistem ini terdiri dari kelas POJO yang dirancang sejak lama dan tanpa memperhitungkan semua teknologi baru ini. Bidang tipe Peta <POJO, POJO> digunakan, ada logika bercabang untuk bekerja dengannya. Saya menyimpan bidang ini, saya mendapatkan kesalahan


 org.springframework.data.mapping.MappingException: Cannot use a complex object as a key value. 

Pada kesempatan ini, hanya korespondensi tahun 2011 yang ditemukan , di mana ia diusulkan untuk mengembangkan MappingMongoConverter non-standar. Tercatat sejauh ini di bidang masalah @ Transient, saya akan melanjutkan. Ternyata untuk menyelamatkan, mempelajari hasilnya.


Penghematan terjadi dalam koleksi, nama yang bertepatan dengan nama kelas yang disimpan. Saya belum menggunakan anotasi @DBRef, jadi hanya ada satu koleksi, dokumen JSON cukup besar dan bercabang. Saya perhatikan bahwa ketika Anda menyimpan objek, MongoOperations menelusuri semua tautan non-kosong (termasuk yang diwarisi) dan menulisnya sebagai dokumen terlampir.


1.2. Bongkar Kolom atau array bernama?


Model sistem sedemikian rupa sehingga kelas C dapat berisi referensi ke kelas D yang sama beberapa kali. Dalam bidang defaultMode yang terpisah dan di antara tautan lainnya di ArrayList, sesuatu seperti ini

 public class C { private D defaultMode; private List<D> listOfD = new ArrayList<D>(); public class D { .. } public C(){ this.defaultMode = new D(); listOfD.add(defaultMode); } } 

Setelah membongkar, dokumen JSON akan memiliki dua salinannya: dokumen terlampir bernama defaultMode dan elemen yang tidak disebutkan namanya dari array dokumen. Dalam kasus pertama, dokumen dapat diakses dengan nama, dalam yang kedua - dengan nama array dengan indeks. Anda dapat mencari koleksi MongoDB dalam kedua kasus. Bekerja hanya dengan Spring Data dan MongoDB, saya sampai pada kesimpulan bahwa Anda dapat menggunakan ArrayList, jika hati-hati; Saya tidak melihat adanya pembatasan penggunaan array. Fitur muncul kemudian, di tingkat Konektor MongoDB untuk BI.


1.3. Unduh Argumen konstruktor


Saya mencoba membaca dokumen yang disimpan menggunakan metode MongoOperations.findOne (). Memuat objek A dari database melempar pengecualian


 "No property name found on entity class A to bind constructor parameter to!" 

Ternyata kelas memiliki bidang corpName, dan konstruktor memiliki parameter nama String, dan this.corpName = nama ditugaskan di badan konstruktor. MongoOperations mensyaratkan bahwa nama bidang di kelas cocok dengan nama argumen konstruktor. Jika ada beberapa konstruktor, Anda perlu memilih satu dengan anotasi @PersistenceConstructor. Saya membawa nama bidang dan parameter ke dalam korespondensi.


1.4. Unduh Dengan $ D dan $ 0 ini


Kelas D bersarang bagian dalam merangkum perilaku default kelas C dan tidak masuk akal secara terpisah dari kelas C. Sebuah instance dari D dibuat untuk setiap instance dari C dan sebaliknya - untuk setiap instance dari D ada instance dari C yang menghasilkannya. Kelas D masih memiliki turunan yang menerapkan perilaku alternatif dan dapat disimpan dalam listOfD. Konstruktor kelas turunan D membutuhkan objek yang ada C.


Selain kelas dalam bersarang, sistem menggunakan kelas dalam anonim . Seperti yang Anda ketahui , keduanya berisi referensi implisit ke instance kelas terlampir. Yaitu, sebagai bagian dari setiap instance objek CD, kompiler membuat tautan $ 0 ini, yang menunjuk ke objek induk C.


Sekali lagi saya mencoba membaca dokumen yang disimpan dari koleksi dan mendapatkan pengecualian


 "No property this$0 found on entity class $D to bind constructor parameter to!" 

Saya ingat bahwa metode kelas D menggunakan kekuatan penuh dari referensi C.this.fieldOfClassC ini, dan turunan dari kelas D membutuhkan konstruktor untuk membuat instance C sebagai argumen. Yaitu, saya perlu memberikan urutan tertentu untuk membuat objek di MongoOperations sehingga objek induk C dapat ditentukan dalam konstruktor D. Sekali lagi, MappingMongoConverter non-standar?


Mungkin tidak menggunakan kelas anonim dan membuat kelas dalam menjadi normal? Memperbaiki, atau lebih tepatnya memperbaiki arsitektur sistem yang sudah diterapkan adalah tugas ...


2. Pendekatan dari 3NF / @ DBRef


Saya mencoba untuk pergi di sisi lain, menyelamatkan setiap kelas dalam koleksi saya dan membuat koneksi di antara mereka dalam semangat 3NF.


2.1. Bongkar @DBRef itu indah


Kelas C berisi beberapa referensi ke D. Jika tautan defaultMode dan ArrayList ditandai sebagai @DBRef, maka ukuran dokumen akan berkurang, alih-alih dokumen terlampir yang besar akan ada tautan rapi. Di bidang json, dokumen kumpulan C bidang muncul

 "defaultMode" : DBRef("D", ObjectId("5c496eed2c9c212614bb8176")) 

Dalam database MongoDB, kumpulan D secara otomatis dibuat dan dokumen dengan bidang di dalamnya


 "_id" : ObjectId("5c496eed2c9c212614bb8176") 

Semuanya sederhana dan indah.


2.2. Unduh Konstruktor kelas D


Saat bekerja dengan tautan, objek C tahu bahwa objek D default dibuat tepat satu kali. Jika Anda perlu mem-bypass semua objek D kecuali yang default, cukup bandingkan tautannya:


 private D defaultMode; private ArrayList<D> listOfD; for (D currentD: listOfD){ if (currentD == defaultMode) continue; doSomething(currentD); } 

Saya memanggil findOne (), saya mempelajari kelas C. Ternyata MongoOperations membaca dokumen json dan memanggil konstruktor D untuk setiap penjelasan @DBRef yang dijumpainya, setiap kali membuat objek baru. Saya mendapatkan konstruksi aneh - dua referensi berbeda ke D di bidang defaultMode dan dalam array listOfD, di mana tautannya harus sama.


Belajar dari komunitas : "Dbref menurut saya harus dihindari ketika bekerja dengan mongodb." Pertimbangan lain dalam nada yang sama dari dokumentasi resmi: model data denormalized di mana data terkait disimpan dalam satu dokumen akan optimal untuk menyelesaikan DBRefs, aplikasi Anda harus melakukan pertanyaan tambahan untuk mengembalikan dokumen yang direferensikan.


Halaman dokumentasi yang disebutkan mengatakan di bagian paling awal: "Untuk banyak kasus penggunaan di MongoDB, model data denormalized di mana data terkait disimpan dalam satu dokumen akan menjadi optimal." Apakah ini ditulis untuk saya?


Fokus dengan perancang menyarankan bahwa Anda tidak perlu berpikir seperti dalam DBMS relasional. Pilihannya adalah:


  • jika Anda menentukan @DBRef:
    • konstruktor untuk setiap anotasi akan dipanggil dan beberapa objek identik akan dibuat;
    • MongoOperations akan menemukan dan membaca semua dokumen dari semua koleksi terkait. Akan ada permintaan untuk indeks oleh ObjectId dan kemudian membaca dari banyak koleksi database (besar);
  • jika Anda tidak menentukan, maka json "abnormal" akan disimpan dengan pengulangan data yang sama.

Saya perhatikan sendiri: Anda tidak bisa mengandalkan @DBRef, tetapi gunakan bidang tipe ObjectId, mengisinya secara manual. Dalam hal ini, alih-alih


 "defaultMode" : DBRef("D", ObjectId("5c496eed2c9c212614bb8176")) 

dokumen json akan berisi


 "defaultMode" : ObjectId("5c496eed2c9c212614bb8176") 

Tidak akan ada pemuatan otomatis - MongoOperations tidak tahu dalam koleksi mana untuk mencari dokumen. Dokumen harus dimuat dalam permintaan (malas) terpisah yang menunjukkan koleksi dan ObjectId. Permintaan tunggal harus mengembalikan hasil dengan cepat, di samping itu, indeks otomatis dibuat untuk setiap koleksi oleh ObjectId.


2.3. Jadi bagaimana sekarang?


Subtotal. Itu tidak mungkin untuk dengan cepat dan mudah mengimplementasikan fungsi db4o di MongoDB:


  • Tidak jelas cara menggunakan POJO kustom sebagai Kunci - Nilai daftar kunci;
  • Tidak jelas cara mengatur urutan objek yang dibuat di MappingMongoConverter;
  • tidak jelas apakah akan mengunggah dokumen "non-normalisasi" tanpa DBRef dan apakah perlu untuk membuat mekanisme sendiri untuk inisialisasi malas.

Anda dapat menambahkan pemuatan malas. Anda dapat mencoba melakukan MappingMongoConverter. Anda dapat memodifikasi konstruktor / bidang / daftar yang ada. Tetapi ada bertahun-tahun pelapisan logika bisnis - bukan perubahan yang lemah dan risiko tidak pernah diuji.


Solusi kompromi: untuk membuat mekanisme baru untuk menyimpan data untuk masalah yang sedang dipecahkan, sambil mempertahankan mekanisme untuk berinteraksi dengan GUI.


3. Upaya ketiga, pengalaman dua yang pertama


Pareto menyarankan bahwa menyelesaikan masalah dengan kecepatan pengguna akan berarti keberhasilan seluruh tugas. Tugasnya adalah ini: Anda perlu mempelajari cara menyimpan dan mengembalikan data presentasi pengguna dengan cepat tanpa db4o.


Ini akan kehilangan kemampuan untuk memeriksa objek yang disimpan dalam debugging. Di satu sisi, ini buruk. Di sisi lain, tugas seperti itu jarang terjadi, dan di git semua pengiriman pertempuran ditandai. Untuk toleransi kesalahan, setiap kali sebelum membongkar, sistem membuat serial perhitungan untuk file. Jika Anda perlu memeriksa objek dalam debugging, Anda dapat mengambil serialisasi, mengkloning unit sistem yang sesuai dan mengembalikan perhitungan.


3.1. Data Presentasi Kustom


Untuk membangun presentasi tingkat pengguna, sistem memiliki kelas Penampil khusus. Metode Viewer.getXML () menerima level sebagai input, mengekstraksi nilai numerik dan string yang diperlukan dari itu, dan menghasilkan XML.


Jika pengguna diminta untuk menunjukkan level perhitungan hari ini, maka level tersebut akan ditemukan dalam RAM. Untuk menampilkan perhitungan dari masa lalu, metode com.db4o.query.Query.execute () akan menemukan level dalam file. Level dari file hampir tidak berbeda dari level yang baru saja dibuat dan Penampil akan membangun presentasi tanpa memperhatikan substitusi.


Untuk mengatasi masalah saya, saya membutuhkan perantara antara tingkat perhitungan dan presentasinya - kerangka presentasi (Bingkai), yang akan menyimpan data dan membangun data XML yang tersedia. Rantai tindakan untuk membangun presentasi akan menjadi lebih lama, setiap kali bingkai akan dihasilkan dan bingkai akan menghasilkan XML:


  : < > -> Viewer.getXML() : < > -> Viewer.getFrame() -> Frame.getXML() 

Saat menyimpan cerita, Anda perlu membuat bingkai dari semua level dan menulis ke basis data.


3.2. Bongkar


Tugasnya relatif sederhana dan tidak ada masalah dengan itu. Mengulangi struktur presentasi XML, frame menerima perangkat rekursif dalam bentuk hierarki elemen dengan bidang String, Integer dan Double. Frame meminta getXML () dari semua elemennya, mengumpulkannya menjadi satu dokumen, dan kembali. MongoOperations melakukan pekerjaan besar dengan sifat rekursif dari frame dan tidak mengajukan pertanyaan baru saat itu berkembang.


Akhirnya, semuanya lepas landas! Mesin WiredTiger secara default memampatkan koleksi dokumen MongoDB, pada sistem file, pembongkaran membutuhkan ~ 3,5 GB per hari. Penurunan sepuluh kali lipat dari db4o tidak buruk.


Pada awalnya, pembongkaran diatur hanya - traversal rekursif dari pohon level, MongoOperations.save () untuk masing-masing. Pembongkaran seperti itu memakan waktu 5,5 jam, dan ini terlepas dari kenyataan bahwa membangun presentasi hanya melibatkan objek membaca. Saya menambahkan multithreading: melintasi pohon level secara rekursif, membagi semua level yang tersedia menjadi paket dengan ukuran tertentu, membuat implementasi Callable.call () sesuai dengan jumlah paket, mentransfer setiap paket ke paket kami sendiri dan melakukannya melalui ExecutorService.invokeAll ().


MongoOperations kembali tidak mengajukan pertanyaan dan melakukan pekerjaan hebat dengan mode multi-utas. Memilih ukuran paket secara empiris, memberikan kecepatan bongkar terbaik. Ternyata 15 menit untuk paket 1000 level.


3.3. Konektor Mongo BI, atau bagaimana orang bekerja dengannya


Bahasa permintaan MongoDB besar dan kuat, saya pasti mendapatkan pengalaman bekerja dengannya, mencapai tempat ini. Konsol mendukung JavaScript, Anda dapat menulis desain yang indah dan kuat. Ini satu sisi. Di sisi lain, saya dapat mematahkan otak setengah dari sesama analis dengan permintaan


 db.users.find( { numbers: { $in: [ 390, 754, 454 ] } } ); 

bukannya yang biasa


 SELECT * FROM users WHERE numbers IN (390, 754, 454) 

MongoDB Connector untuk BI datang untuk menyelamatkan, di mana Anda dapat menyajikan dokumen koleksi dalam bentuk tabel. Basis data MongoDB disebut basis data berbasis dokumen, tidak tahu bagaimana menyajikan hierarki bidang / dokumen dalam bentuk tabel. Agar konektor berfungsi, perlu untuk menggambarkan struktur tabel di masa mendatang dalam file .drdl yang terpisah, format yang sangat mirip dengan yaml. Dalam file, Anda harus menentukan korespondensi antara bidang tabel relasional di output dan jalur ke bidang dokumen JSON di input.


3.4. Fitur menggunakan array


Dikatakan di atas bahwa untuk MongoDB itu sendiri tidak ada perbedaan khusus antara array dan bidang. Dari perspektif konektor, array sangat berbeda dari bidang bernama; Aku bahkan harus memperbaiki kelas Frame yang sudah jadi. Sejumlah dokumen harus digunakan hanya jika perlu untuk memasukkan bagian informasi ke dalam tabel tertaut.


Jika dokumen JSON adalah hierarki bidang bernama, maka bidang apa pun dapat diakses dengan menentukan lintasan dari akar dokumen melalui periode, misalnya xy. Jika korespondensi xy => fieldXY ditentukan dalam file DRDL, maka tabel output akan memiliki banyak baris seperti ada dokumen dalam koleksi di pintu masuk. Jika dalam beberapa dokumen tidak ada bidang xy, NULL akan berada di baris tabel yang sesuai.


Misalkan kita memiliki database MongoDB yang disebut Frames, ada koleksi A dalam database, dan MongoOperations telah menulis dua contoh kelas A ke koleksi ini. Berikut dokumen-dokumennya: pertama


 { "_id": ObjectId("5cdd51e2394faf88a01bd456"), "x": { "y": "xy string value 1"}, "days": [{ "k": "0", "v": 0.0 }, { "k": "1", "v": 0.1 }], "_class": "A" } 

dan kedua (ObjectId berbeda dengan digit terakhir):


 { "_id": ObjectId("5cdd51e2394faf88a01bd457"), "x": { "y": "xy string value 2"}, "days": [{ "k": "0", "v": 0.3 }, { "k": "1", "v": 0.4 }], "_class": "A" } 

Konektor BI tidak dapat mengakses elemen-elemen array dengan indeks, dan itu tidak mungkin untuk mengekstrak, misalnya, hari-hari [1] .v bidang dari array ke dalam tabel. Sebagai gantinya, konektor dapat mewakili setiap elemen array hari sebagai baris dalam tabel terpisah menggunakan operator $ relax . Tabel terpisah ini akan dikaitkan dengan hubungan satu-ke-banyak yang asli melalui pengidentifikasi baris. Dalam contoh kami, table tableA didefinisikan untuk kumpulan dokumen dan tableA_days untuk dokumen array hari. File .drdl terlihat seperti ini:


 schema: - db: Frames tables: - table: tableA collection: A pipeline: [] columns: - Name: _id MongoType: bson.ObjectId SqlName: _id SqlType: objectid - Name: xy MongoType: string SqlName: fieldXY SqlType: varchar - table: tableA_days collection: A pipeline: - $unwind: path: $days columns: - Name: _id #   MongoType: bson.ObjectId SqlName: tableA_id SqlType: objectid - Name: days.k MongoType: string SqlName: tableA_dayNo SqlType: varchar - Name: days.v MongoType: string SqlName: tableA_dayVal SqlType: varchar 

Isi tabel akan menjadi: table tableA


_idfieldXY
5cdd51e2394faf88a01bd456nilai string xy 1
5cdd51e2394faf88a01bd457nilai string xy 2

dan tabel tableA_days


tableA_idtableA_dayTidaktableA_dayVal
5cdd51e2394faf88a01bd45600,0
5cdd51e2394faf88a01bd45610,1
5cdd51e2394faf88a01bd45700,3
5cdd51e2394faf88a01bd45710,4

Total


Itu tidak mungkin untuk mengimplementasikan tugas dalam formulasi asli, Anda tidak bisa hanya mengambil dan mengganti db4o dengan MongoDB. MongoOperations tidak dapat secara otomatis mengembalikan objek apa pun seperti db4o. Anda mungkin dapat melakukan ini, tetapi biaya tenaga kerja tidak akan sebanding dengan memanggil metode store / query perpustakaan db4o.


Jejak audit. Db4o adalah alat yang sangat berguna pada awal proyek. Anda cukup menulis objek, lalu mengembalikannya dan pada saat yang sama tidak ada kekhawatiran dan tabel. Semua ini dengan peringatan penting: jika Anda perlu mengubah hierarki kelas (tambahkan kelas E antara A dan B), maka semua informasi yang disimpan sebelumnya menjadi tidak dapat dibaca. Tetapi untuk memulai sebuah proyek, ini tidak terlalu penting, selama tidak ada array besar dari file-file lama.


Ketika ada cukup pengalaman dengan MongoOperations, menulis unggahan tidak menyebabkan masalah. Menulis kode baru untuk kerangka kerja jauh lebih mudah daripada mengulangi kode lama, yang juga diproduksi.

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


All Articles