Saya pada asalnya menulis artikel ini dalam bahasa Rusia. Jadi, jika anda penutur asli, anda boleh membacanya melalui pautan ini.
Pada tahun lalu atau lebih, saya telah menjumpai artikel dan ceramah yang mencadangkan bahawa JOOQ ialah alternatif moden dan unggul kepada Hibernate. Hujah biasanya termasuk:
Biar saya nyatakan terlebih dahulu bahawa saya menganggap JOOQ sebagai perpustakaan yang sangat baik (khususnya perpustakaan, bukan rangka kerja seperti Hibernate). Ia cemerlang dalam tugasnya — bekerja dengan SQL dalam cara yang ditaip secara statik untuk menangkap kebanyakan ralat pada masa penyusunan.
Namun, apabila saya mendengar hujah bahawa masa Hibernate telah berlalu dan kita kini harus menulis segala-galanya menggunakan JOOQ, saya rasa seperti mengatakan era pangkalan data hubungan sudah berakhir dan kita hanya perlu menggunakan NoSQL sekarang. Kedengaran lucu? Namun, tidak lama dahulu, perbincangan seperti itu agak serius.
Saya percaya isu ini terletak pada salah faham tentang masalah teras yang ditangani oleh kedua-dua alat ini. Dalam artikel ini, saya berhasrat untuk menjelaskan soalan-soalan ini. Kami akan meneroka:
Cara paling mudah dan paling intuitif untuk bekerja dengan pangkalan data ialah corak Skrip Transaksi. Secara ringkasnya, anda menyusun semua logik perniagaan anda sebagai satu set perintah SQL digabungkan menjadi satu transaksi. Biasanya, setiap kaedah dalam kelas mewakili operasi perniagaan dan terhad kepada satu transaksi.
Andaikan kami sedang membangunkan aplikasi yang membenarkan penceramah menyerahkan ceramah mereka ke persidangan (untuk memudahkan, kami hanya akan merekodkan tajuk ceramah). Mengikuti corak Skrip Transaksi, kaedah untuk menghantar ceramah mungkin kelihatan seperti ini (menggunakan JDBI untuk SQL):
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
Dalam kod ini:
Terdapat keadaan perlumbaan yang berpotensi di sini, tetapi untuk kesederhanaan, kami tidak akan menumpukan pada itu.
Kebaikan pendekatan ini:
Keburukan:
Pendekatan ini sah dan masuk akal jika perkhidmatan anda mempunyai logik yang sangat mudah yang tidak dijangka menjadi lebih kompleks dari semasa ke semasa. Walau bagaimanapun, domain selalunya lebih besar. Oleh itu, kita memerlukan alternatif.
Idea corak Model Domain ialah kami tidak lagi mengikat logik perniagaan kami terus kepada arahan SQL. Sebaliknya, kami mencipta objek domain (dalam konteks Java, kelas) yang menerangkan tingkah laku dan menyimpan data tentang entiti domain.
Dalam artikel ini, kami tidak akan membincangkan perbezaan antara model anemia dan kaya. Jika anda berminat, saya telah menulis bahagian terperinci mengenai topik itu.
Senario (perkhidmatan) perniagaan hendaklah menggunakan objek ini sahaja dan elakkan daripada terikat dengan pertanyaan pangkalan data tertentu.
Sudah tentu, pada hakikatnya, kami mungkin mempunyai gabungan interaksi dengan objek domain dan pertanyaan pangkalan data langsung untuk memenuhi keperluan prestasi. Di sini, kami membincangkan pendekatan klasik untuk melaksanakan Model Domain, di mana pengkapsulan dan pengasingan tidak dilanggar.
Sebagai contoh, jika kita bercakap tentang entiti Speaker dan Talk, seperti yang dinyatakan sebelum ini, objek domain mungkin kelihatan seperti ini:
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
Di sini, kelas Penceramah mengandungi logik perniagaan untuk menghantar ceramah. Interaksi pangkalan data diasingkan, membenarkan model domain memfokus pada peraturan perniagaan.
Andaikan antara muka repositori ini:
@AllArgsConstructor public class Speaker { private Long id; private String firstName; private String lastName; private List<Talk> talks; public Talk submitTalk(String title) { boolean experienced = countTalksByStatus(Status.ACCEPTED) >= 10; int maxSubmittedTalksCount = experienced ? 3 : 5; if (countTalksByStatus(Status.SUBMITTED) >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException( "Submitted talks count is maximum: " + maxSubmittedTalksCount); } Talk talk = Talk.newTalk(this, Status.SUBMITTED, title); talks.add(talk); return talk; } private long countTalksByStatus(Talk.Status status) { return talks.stream().filter(t -> t.getStatus().equals(status)).count(); } } @AllArgsConstructor public class Talk { private Long id; private Speaker speaker; private Status status; private String title; private int talkNumber; void setStatus(Function<Status, Status> fnStatus) { this.status = fnStatus.apply(this.status); } public enum Status { SUBMITTED, ACCEPTED, REJECTED } }
Kemudian SpeakerService boleh dilaksanakan dengan cara ini:
public interface SpeakerRepository { Speaker findById(Long id); void save(Speaker speaker); }
Kebaikan Model Domain:
Ringkasnya, terdapat banyak kelebihan. Walau bagaimanapun, terdapat satu cabaran penting. Menariknya, dalam buku mengenai Reka Bentuk Dipacu Domain, yang sering mempromosikan corak Model Domain, masalah ini sama ada tidak disebut langsung atau hanya disentuh secara ringkas.
Masalahnya ialah bagaimanakah anda menyimpan objek domain ke pangkalan data dan kemudian membacanya kembali? Dalam erti kata lain, bagaimana anda melaksanakan repositori?
Kini, jawapannya sudah jelas. Hanya gunakan Hibernate (atau lebih baik lagi, Spring Data JPA) dan selamatkan masalah anda. Tetapi mari bayangkan kita berada dalam dunia di mana rangka kerja ORM belum dicipta. Bagaimanakah kami akan menyelesaikan masalah ini?
Untuk melaksanakan SpeakerRepository saya juga menggunakan JDBI:
@Service @RequiredArgsConstructor public class TalkService { private final Jdbi jdbi; public TalkSubmittedResult submitTalk(Long speakerId, String title) { var talkId = jdbi.inTransaction(handle -> { // Count the number of accepted talks by the speaker var acceptedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // Check if the speaker is experienced var experienced = acceptedTalksCount >= 10; // Determine the maximum allowable number of submitted talks var maxSubmittedTalksCount = experienced ? 5 : 3; var submittedTalksCount = handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'") .bind("id", speakerId) .mapTo(Long.class) .one(); // If the maximum number of submitted talks is exceeded, throw an exception if (submittedTalksCount >= maxSubmittedTalksCount) { throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount); } return handle.createUpdate( "INSERT INTO talk (speaker_id, status, title) " + "VALUES (:id, 'SUBMITTED', :title)" ).bind("id", speakerId) .bind("title", title) .executeAndReturnGeneratedKeys("id") .mapTo(Long.class) .one(); }); return new TalkSubmittedResult(talkId); } }
Pendekatannya mudah. Untuk setiap repositori, kami menulis pelaksanaan berasingan yang berfungsi dengan pangkalan data menggunakan mana-mana perpustakaan SQL (seperti JOOQ atau JDBI).
Pada pandangan pertama (dan mungkin juga yang kedua), penyelesaian ini mungkin kelihatan agak bagus. Pertimbangkan ini:
Perkara menjadi lebih menarik di dunia nyata, di mana anda mungkin menghadapi senario seperti ini:
Selain itu, anda perlu mengekalkan kod pemetaan semasa logik perniagaan dan objek domain anda berkembang.
Jika anda cuba mengendalikan setiap mata ini sendiri, anda akhirnya akan mendapati diri anda (terkejut!) menulis rangka kerja seperti Hibernate anda — atau lebih berkemungkinan, versi yang lebih ringkas daripadanya.
JOOQ menangani kekurangan penaipan statik semasa menulis pertanyaan SQL. Ini membantu mengurangkan bilangan ralat pada peringkat penyusunan. Dengan penjanaan kod terus daripada skema pangkalan data, sebarang kemas kini pada skema akan segera menunjukkan tempat kod itu perlu diperbaiki (ia tidak akan disusun).
Hibernate menyelesaikan masalah memetakan objek domain ke pangkalan data hubungan dan sebaliknya (membaca data daripada pangkalan data dan memetakannya ke objek domain).
Oleh itu, tidak masuk akal untuk berhujah bahawa Hibernate lebih teruk atau JOOQ lebih baik. Alat ini direka untuk tujuan yang berbeza. Jika aplikasi anda dibina berdasarkan paradigma Skrip Transaksi, JOOQ sudah pasti pilihan yang ideal. Tetapi jika anda ingin menggunakan corak Model Domain dan mengelakkan Hibernate, anda perlu berurusan dengan kegembiraan pemetaan manual dalam pelaksanaan repositori tersuai. Sudah tentu, jika majikan anda membayar anda untuk membina satu lagi pembunuh Hibernate, tiada soalan di sana. Tetapi kemungkinan besar, mereka mengharapkan anda menumpukan pada logik perniagaan, bukan kod infrastruktur untuk pemetaan objek ke pangkalan data.
Dengan cara ini, saya percaya gabungan Hibernate dan JOOQ berfungsi dengan baik untuk CQRS. Anda mempunyai aplikasi (atau sebahagian logiknya) yang melaksanakan arahan, seperti operasi CREATE/UPDATE/DELETE — di sinilah Hibernate sesuai dengan sempurna. Sebaliknya, anda mempunyai perkhidmatan pertanyaan yang membaca data. Di sini, JOOQ adalah cemerlang. Ia menjadikan membina pertanyaan kompleks dan mengoptimumkannya lebih mudah berbanding dengan Hibernate.
Memang benar. JOOQ membolehkan anda menjana DAO yang mengandungi pertanyaan standard untuk mengambil entiti daripada pangkalan data. Anda juga boleh melanjutkan DAO ini dengan kaedah anda. Selain itu, JOOQ akan menjana entiti yang boleh diisi menggunakan setter, serupa dengan Hibernate, dan dihantar ke kaedah sisipan atau kemas kini dalam DAO. Bukankah itu seperti Data Musim Bunga?
Untuk kes mudah, ini memang boleh berfungsi. Walau bagaimanapun, ia tidak jauh berbeza daripada melaksanakan repositori secara manual. Masalahnya serupa:
Jadi, jika anda ingin membina model domain yang kompleks, anda perlu melakukannya secara manual. Tanpa Hibernate, tanggungjawab untuk pemetaan akan jatuh sepenuhnya kepada anda. Sudah tentu, menggunakan JOOQ lebih menyenangkan daripada JDBI, tetapi prosesnya masih memerlukan tenaga kerja.
Malah Lukas Eder, pencipta JOOQ, menyebut dalam blognya bahawa DAO telah ditambahkan pada perpustakaan kerana ia adalah corak yang popular, bukan kerana dia semestinya mengesyorkan menggunakannya.
Terima kasih kerana membaca artikel. Saya peminat tegar Hibernate dan menganggapnya sebagai rangka kerja yang sangat baik. Walau bagaimanapun, saya faham bahawa sesetengah orang mungkin mendapati JOOQ lebih mudah. Perkara utama artikel saya ialah Hibernate dan JOOQ bukan saingan. Alat ini boleh wujud bersama walaupun dalam produk yang sama jika ia membawa nilai.
Jika anda mempunyai sebarang ulasan atau maklum balas tentang kandungan, saya berbesar hati untuk membincangkannya. Selamat hari yang produktif!
Atas ialah kandungan terperinci JOOQ Bukan Pengganti untuk Hibernate. Mereka Menyelesaikan Masalah Berbeza. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!