Mengambil data dengan ORM itu mudah! Atau tidak?


Pendahuluan


Hampir semua sistem informasi dalam satu atau lain cara berinteraksi dengan penyimpanan data eksternal. Dalam kebanyakan kasus, ini adalah database relasional, dan, seringkali, semacam kerangka kerja ORM digunakan untuk bekerja dengan data. ORM menghilangkan sebagian besar operasi rutin, alih-alih menawarkan serangkaian kecil abstraksi tambahan untuk bekerja dengan data.


Martin Fowler menerbitkan sebuah artikel yang menarik, salah satu pemikiran utama di sana: "ORM membantu kami memecahkan sejumlah besar masalah dalam aplikasi perusahaan ... Alat ini tidak bisa disebut cantik, tetapi masalah yang dihadapinya juga tidak baik. Saya pikir ORM layak mendapatkan lebih banyak rasa hormat dan pengertian.


Kami menggunakan ORM dengan sangat intensif dalam kerangka kerja CUBA , jadi kami tahu secara langsung masalah dan keterbatasan teknologi ini, karena CUBA digunakan dalam berbagai proyek di seluruh dunia. Ada banyak topik yang dapat didiskusikan sehubungan dengan ORM, tetapi kami akan fokus pada salah satu dari mereka: pilihan antara metode "malas" (malas) dan "rakus" (bersemangat) dalam pengambilan sampel data. Kami akan berbicara tentang berbagai pendekatan untuk menyelesaikan masalah ini dengan ilustrasi dari JPA API dan Spring, dan juga menjelaskan bagaimana (dan mengapa tepatnya) ORM digunakan dalam CUBA dan pekerjaan apa yang kami lakukan untuk meningkatkan pekerjaan dengan data dalam kerangka kerja kami.


Pengambilan sampel data: malas atau tidak?


Jika model data Anda hanya memiliki satu entitas, maka kemungkinan besar Anda tidak akan melihat adanya masalah saat bekerja dengan ORM. Mari kita lihat contoh kecil. Misalkan kita memiliki entitas User () yang memiliki dua atribut: ID dan Name () :


 public class User { @Id @GeneratedValue private int id; private String name; //Getters and Setters here } 

Untuk mendapatkan instance entitas ini dari database, kita hanya perlu memanggil satu metode objek EntityManager :


 EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, id); 

Hal-hal menjadi sedikit lebih menarik ketika hubungan satu-ke-banyak muncul:


 public class User { @Id @GeneratedValue private int id; private String name; @OneToMany private List<Address> addresses; //Getters and Setters here } 

Jika kita perlu mengekstrak instance pengguna dari database, muncul pertanyaan: "Apakah kita juga memilih alamat?". Dan jawaban "benar" di sini adalah: "Tergantung pada ..." Dalam beberapa kasus kita akan memerlukan alamat, dalam beberapa - tidak. Biasanya, ORM menyediakan dua cara untuk mengambil catatan yang bergantung: malas dan serakah. Secara default, sebagian besar ORM menggunakan cara malas. Tetapi, jika kita menulis kode ini:


 EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, 1); em.close(); System.out.println(user.getAddresses().get(0)); 

... lalu kita mendapatkan pengecualian “LazyInitException” , yang sangat membingungkan pendatang baru yang baru saja mulai bekerja dengan ORM. Dan inilah saatnya ketika Anda perlu memulai sebuah cerita tentang apa yang "Terlampir" dan "Terpisah" contoh entitas, apa sesi dan transaksi.
Ya, itu berarti entitas harus "dilampirkan" ke sesi sehingga Anda dapat memilih data dependen. Baiklah, jangan langsung menutup transaksi, dan hidup akan segera menjadi lebih mudah. Dan di sini muncul masalah lain - transaksi menjadi lebih lama, yang meningkatkan risiko kebuntuan. Buat transaksi lebih pendek? Itu mungkin, tetapi jika Anda membuat banyak, banyak transaksi kecil, kita mendapatkan "Kisah Komar Komarovich - hidung panjang dan tentang Misha berbulu - ekor pendek" tentang bagaimana gerombolan nyamuk beruang kecil menang - itu akan terjadi dengan database. Jika jumlah transaksi kecil meningkat secara signifikan, maka masalah kinerja akan muncul.
Seperti yang dikatakan, ketika mengambil data tentang pengguna, alamat mungkin diperlukan atau tidak, oleh karena itu, tergantung pada logika bisnis, Anda harus memilih koleksi atau tidak. Penting untuk menambahkan kondisi baru ke kode ... Hmmm ... Ada sesuatu yang semakin rumit.


Jadi, bagaimana jika Anda mencoba jenis sampel yang berbeda?


 public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.EAGER) private List<Address> addresses; //Getters and Setters here } 

Baiklah ... Anda tidak bisa mengatakan itu banyak membantu. Ya, kami akan menyingkirkan LazyInit dibenci dan tidak perlu memeriksa apakah entitas melekat pada sesi atau tidak. Tetapi sekarang kita mungkin memiliki masalah kinerja, karena kita tidak selalu memerlukan alamat, tetapi kita masih memilih objek-objek ini dalam memori server.
Ada ide lagi?


Spring jdbc


Beberapa pengembang sangat bosan dengan ORM sehingga mereka beralih ke kerangka kerja alternatif. Misalnya, pada Spring JDBC, yang menyediakan kemampuan untuk mengkonversi data relasional ke data objek dalam mode "semi-otomatis". Pengembang menulis kueri untuk setiap kasus di mana set atribut tertentu diperlukan (atau kode yang sama digunakan kembali untuk kasus di mana struktur data yang sama diperlukan).


Ini memberi kita fleksibilitas besar. Misalnya, Anda dapat memilih hanya satu atribut tanpa membuat objek entitas yang sesuai:


 String name = this.jdbcTemplate.queryForObject( "select name from t_user where id = ?", new Object[]{1L}, String.class); 

Atau pilih objek dalam bentuk biasa:


 User user = this.jdbcTemplate.queryForObject( "select id, name from t_user where id = ?", new Object[]{1L}, new RowMapper<User>() { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setName(rs.getString("name")); user.setId(rs.getInt("id")); return user; } }); 

Anda juga dapat memilih daftar alamat untuk pengguna, Anda hanya perlu menulis sedikit lebih banyak kode dan menyusun query SQL dengan benar untuk menghindari masalah n + 1 query .


Soooo, rumit lagi. Ya, kami mengontrol semua kueri dan bagaimana data dipetakan ke objek, tetapi kami perlu menulis lebih banyak kode, mempelajari SQL, dan mengetahui bagaimana kueri dieksekusi dalam database. Secara pribadi, saya berpikir bahwa pengetahuan tentang SQL adalah keterampilan yang diperlukan untuk seorang programmer aplikasi, tetapi tidak semua orang berpikir seperti itu, dan saya tidak akan terlibat dalam polemik. Bagaimanapun, pengetahuan tentang instruksi perakitan x86 hari ini juga opsional. Lebih baik kita pikirkan bagaimana membuat hidup lebih mudah bagi programmer.


JPA EntityGraph


Dan mari kita mundur selangkah dan berpikir, apa yang kita butuhkan? Tampaknya kita hanya perlu menunjukkan atribut apa yang kita butuhkan dalam setiap kasus. Baiklah, mari kita lakukan! JPA 2.1 memperkenalkan API baru - EntityGraph (grafik entitas). Idenya sangat sederhana: kami menggunakan anotasi untuk menggambarkan apa yang akan kami pilih dari basis data. Berikut ini sebuah contoh:


 @Entity @NamedEntityGraphs({ @NamedEntityGraph(name = "user-only-entity-graph"), @NamedEntityGraph(name = "user-addresses-entity-graph", attributeNodes = {@NamedAttributeNode("addresses")}) }) public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.LAZY) private Set<Address> addresses; //Getters and Setters here } 

Dua grafik dijelaskan untuk entitas ini: user-only-entity-graph tidak memilih atribut Addresses (ditandai sebagai malas), sedangkan grafik kedua memberitahu ORM untuk memilih atribut ini. Jika kami menandai Addresses sebagai keinginan, grafik akan diabaikan dan alamat akan tetap dipilih.


Jadi, di JPA 2.1, Anda dapat mengambil sampel data seperti ini:


 EntityManager em = entityManagerFactory.createEntityManager(); EntityGraph graph = em.getEntityGraph("user-addresses-entity-graph"); Map<String, Object> properties = Map.of("javax.persistence.fetchgraph", graph); User user = em.find(User.class, 1, properties); em.close(); 

Pendekatan ini sangat menyederhanakan pekerjaan, tidak perlu berpikir secara terpisah tentang atribut malas, dan panjang transaksi. Bonus tambahan adalah grafik diterapkan pada tingkat permintaan SQL, sehingga data "ekstra" tidak dipilih dalam aplikasi Java. Tetapi ada satu masalah kecil: Anda tidak bisa mengatakan atribut mana yang dipilih dan mana yang tidak. Ada API untuk memeriksa, ini dilakukan menggunakan kelas PersistenceUtil :


 PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil(); System.out.println("User.addresses loaded: " + pu.isLoaded(user, "addresses")); 

Tapi ini sangat membosankan dan tidak semua orang siap untuk melakukan pemeriksaan seperti itu. Apakah ada hal lain yang dapat Anda sederhanakan dan tidak menunjukkan atribut yang tidak dipilih?


Proyeksi Musim Semi


Kerangka Kerja Pegas memiliki hal hebat yang disebut Proyeksi (dan ini tidak sama dengan proyeksi di Hibernate ). Jika Anda hanya perlu memilih beberapa atribut dari suatu entitas, sebuah antarmuka dengan atribut yang diperlukan dibuat, dan Spring memilih "instance" dari antarmuka ini dari database. Sebagai contoh, pertimbangkan antarmuka berikut:


 interface NamesOnly { String getName(); } 

Sekarang Anda dapat mendefinisikan repositori Spring JPA untuk mengambil entitas Pengguna sebagai berikut:


 interface UserRepository extends CrudRepository<User, Integer> { Collection<NamesOnly> findByName(String lastname); } 

Dalam hal ini, setelah memanggil metode findByName, dalam daftar yang dihasilkan kita mendapatkan entitas yang hanya memiliki akses ke atribut yang didefinisikan dalam antarmuka! Menurut prinsip yang sama, seseorang dapat memilih entitas dependen, yaitu segera pilih hubungan "detail utama". Selain itu, Spring menghasilkan SQL "benar" dalam banyak kasus, yaitu hanya atribut-atribut yang dijelaskan dalam proyeksi yang dipilih dari database, ini sangat mirip dengan cara kerja entitas graph.
Ini adalah API yang sangat kuat. Saat mendefinisikan antarmuka, Anda dapat menggunakan ekspresi SpEL, menggunakan kelas dengan semacam logika bawaan, bukan antarmuka, dan banyak lagi, semuanya dijelaskan secara rinci dalam dokumentasi .
Satu-satunya masalah dengan proyeksi adalah bahwa di dalam mereka diimplementasikan sebagai pasangan kunci-nilai, yaitu hanya baca. Ini berarti bahwa bahkan jika kita mendefinisikan metode penyetel untuk proyeksi, kita tidak akan dapat menyimpan perubahan baik melalui repositori CRUD atau melalui EntityManager. Jadi proyeksi adalah DTO yang dapat dikonversi kembali ke Entitas dan disimpan hanya jika Anda menulis kode Anda sendiri untuk ini.


Cara memilih data di CUBA


Sejak awal pengembangan kerangka kerja CUBA, kami mencoba untuk mengoptimalkan bagian dari kode yang bekerja dengan basis data. Di CUBA, kami menggunakan EclipseLink sebagai dasar untuk API akses data. Apa yang baik tentang EclipseLink adalah bahwa ia mendukung pemuatan entitas parsial sejak awal, dan ini merupakan faktor penentu dalam memilih antara itu dan Hibernate. Di EclipseLink, Anda bisa menentukan atribut untuk memuat jauh sebelum standar JPA 2.1 muncul. CUBA memiliki caranya sendiri untuk menggambarkan grafik entitas, yang disebut CUBA Views . Representasi CUBA adalah API yang agak maju, Anda dapat mewarisi beberapa representasi dari yang lain, menggabungkannya, melamar ke entitas master dan detail. Motivasi lain untuk membuat tampilan CUBA adalah bahwa kami ingin menggunakan transaksi pendek sehingga kami dapat bekerja dengan entitas terpisah di antarmuka pengguna web.
Dalam CUBA, tampilan dijelaskan dalam file XML, seperti dalam contoh di bawah ini:


 <view class="com.sample.User" extends="_minimal" name="user-minimal-view"> <property name="name"/> <property name="addresses" view="address-street-only-view"/> </property> </view> 

Tampilan ini memilih entitas User dan name atribut lokalnya, dan juga memilih alamat dengan menerapkan tampilan address-street-only-view . Semua ini terjadi (perhatian!) Di tingkat permintaan SQL. Saat tampilan dibuat, Anda bisa menggunakannya dalam pemilihan data menggunakan kelas DataManager:


 List<User> users = dataManager.load(User.class).view("user-edit-view").list(); 

Pendekatan ini berfungsi dengan baik, sementara memakan lalu lintas jaringan secara ekonomis, karena atribut yang tidak digunakan tidak ditransfer dari database ke aplikasi, tetapi, seperti dalam kasus JPA, ada masalah: tidak dapat dikatakan atribut entitas yang mana yang dimuat. Dan di CUBA ada pengecualian “IllegalStateException: Cannot get unfetched attribute [...] from detached object” , yang, seperti LazyInit , pasti ditemui oleh semua orang yang menulis menggunakan kerangka kerja kami. Seperti dalam JPA, ada cara untuk memeriksa atribut mana yang dimuat dan mana yang tidak, tetapi, sekali lagi, menulis cek semacam itu adalah tugas yang membosankan dan melelahkan yang membuat banyak pengembang kesal. Sesuatu yang lain perlu diciptakan agar tidak membebani orang dengan pekerjaan yang, secara teori, dapat dilakukan mesin.


Konsep - Antarmuka Tampilan CUBA


Tetapi bagaimana jika Anda mencoba untuk menggabungkan grafik entitas dan proyeksi? Kami memutuskan untuk mencoba ini dan mengembangkan antarmuka untuk antarmuka tampilan entitas yang mengikuti pendekatan proyeksi Spring. Antarmuka ini diterjemahkan ke dalam tampilan CUBA saat startup aplikasi dan dapat digunakan dalam DataManager. Idenya sederhana: kita menggambarkan sebuah antarmuka (atau satu set antarmuka), yang merupakan grafik entitas.


 interface UserMinimalView extends BaseEntityView<User, Integer> { String getName(); void setName(String val); List<AddressStreetOnly> getAddresses(); interface AddressStreetOnly extends BaseEntityView<Address, Integer> { String getStreet(); void setStreet(String street); } } 

Perlu dicatat bahwa untuk beberapa kasus tertentu, Anda dapat membuat antarmuka lokal, seperti dalam kasus AddressStreetOnly dari contoh di atas, agar tidak "mencemari" API publik aplikasi Anda.


Dalam proses memulai aplikasi CUBA (yang sebagian besar merupakan inisialisasi konteks Spring), kami secara terprogram membuat tampilan CUBA dan menempatkannya dalam repositori kacang internal dalam konteks.
Sekarang Anda perlu sedikit memodifikasi implementasi kelas DataManager sehingga menerima tampilan antarmuka, dan Anda dapat memilih entitas dengan cara ini:


 List<UserMinimalView> users = dataManager.load(UserMinimalView.class).list(); 

Di bawah tenda, objek proxy dihasilkan yang mengimplementasikan antarmuka dan membungkus instance entitas yang dipilih dari basis data (dengan cara yang hampir sama seperti di Hibernate). Dan, ketika pengembang memanggil nilai atribut, proxy mendelegasikan panggilan metode ke instance "real" entitas.


Dalam mengembangkan konsep ini, kami mencoba membunuh dua burung dengan satu batu:


  • Data yang tidak dijelaskan dalam antarmuka tidak dimuat ke dalam aplikasi, sehingga menghemat sumber daya server.
  • Pengembang hanya dapat menggunakan atribut-atribut yang dapat diakses melalui antarmuka (dan, karena itu, dipilih dari database), sehingga menghilangkan pengecualian UnfetchedAttribute yang kami tulis di atas.

Tidak seperti proyeksi Spring, kami membungkus entitas dalam objek proxy, selain itu, setiap antarmuka mewarisi antarmuka CUBA standar - Entity . Ini berarti bahwa atribut Entity View dapat diubah, dan kemudian menyimpan perubahan ini ke database menggunakan API CUBA standar untuk bekerja dengan data.
Dan, omong-omong, "kelinci ketiga" - Anda dapat membuat atribut read-only jika Anda mendefinisikan antarmuka dengan metode pengambil saja. Dengan demikian, kami telah menetapkan aturan modifikasi di tingkat API entitas.
Selain itu, Anda dapat melakukan beberapa operasi lokal untuk entitas terpisah menggunakan atribut yang tersedia, misalnya, konversi string nama, seperti dalam contoh di bawah ini:


 @MetaProperty default String getNameLowercase() { return getName().toLowerCase(); } 

Perhatikan bahwa atribut yang dikomputasi dapat dikeluarkan dari model kelas entitas dan ditransfer ke antarmuka yang berlaku untuk logika bisnis tertentu.


Fitur lain yang menarik adalah pewarisan antarmuka. Anda dapat membuat beberapa tampilan dengan set atribut yang berbeda, dan kemudian menggabungkannya. Misalnya, Anda bisa membuat antarmuka untuk entitas Pengguna dengan atribut nama dan email, dan lainnya dengan atribut nama dan alamat. Sekarang, jika Anda perlu memilih nama, email, dan alamat, maka Anda tidak perlu menyalin atribut ini ke antarmuka ketiga, Anda hanya perlu mewarisi dari dua tampilan pertama. Dan ya, contoh antarmuka ketiga dapat diteruskan ke metode yang menerima parameter dengan jenis antarmuka induk, aturan OOP adalah sama untuk semua orang.


Konversi antara tampilan juga diterapkan - setiap antarmuka memiliki metode reload (), di mana Anda dapat melewati kelas tampilan sebagai parameter:


 UserFullView userFull = userMinimal.reload(UserFullView.class); 

UserFullView dapat berisi atribut tambahan, sehingga entitas akan dimuat ulang dari database, jika perlu. Dan proses ini tertunda. Akses ke database hanya akan dilakukan ketika akses pertama ke atribut entitas terjadi. Ini akan sedikit memperlambat panggilan pertama, tetapi pendekatan ini dipilih dengan sengaja - jika instance entitas digunakan dalam modul "web", yang berisi UI dan pengendali REST sendiri, modul ini dapat digunakan pada server yang terpisah. Dan ini berarti bahwa overload paksa entitas akan membuat lalu lintas jaringan tambahan - akses ke modul inti dan kemudian ke database. Jadi, menunda kelebihan hingga saat diperlukan, kami menghemat lalu lintas dan mengurangi jumlah kueri basis data.


Konsep ini dirancang sebagai modul untuk CUBA, contoh penggunaan dapat diunduh dari GitHub .


Kesimpulan


Tampaknya dalam waktu dekat kita masih akan secara masif menggunakan ORM dalam aplikasi perusahaan hanya karena kita membutuhkan sesuatu yang akan mengubah data relasional menjadi objek. Tentu saja, solusi spesifik akan dikembangkan untuk aplikasi yang kompleks, unik, dan berkapasitas sangat tinggi, tetapi tampaknya kerangka kerja ORM akan hidup selama basis data relasional.
Di CUBA, kami mencoba menyederhanakan pekerjaan dengan ORM secara maksimal, dan dalam versi yang akan datang kami akan memperkenalkan fitur baru untuk bekerja dengan data. Akan sulit untuk mengatakan apakah ini akan menjadi antarmuka presentasi atau sesuatu yang lain, tetapi saya yakin akan satu hal: kami akan terus menyederhanakan pekerjaan dengan data di versi kerangka kerja yang akan datang.

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


All Articles