Rumah > Java > javaTutorial > JOOQ Bukan Pengganti untuk Hibernate. Mereka Menyelesaikan Masalah Berbeza

JOOQ Bukan Pengganti untuk Hibernate. Mereka Menyelesaikan Masalah Berbeza

DDD
Lepaskan: 2025-01-11 20:10:41
asal
471 orang telah melayarinya

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:

  1. JOOQ membolehkan anda mengesahkan segala-galanya pada masa penyusunan, manakala Hibernate tidak!
  2. Hibernate menjana pertanyaan pelik dan tidak selalu optimum, manakala dengan JOOQ, semuanya telus!
  3. Entiti hibernate boleh berubah, yang tidak baik. JOOQ membenarkan semua entiti menjadi tidak berubah (hello, pengaturcaraan berfungsi)!
  4. JOOQ tidak melibatkan sebarang "sihir" dengan anotasi!

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:

  1. Apakah Skrip Transaksi?
  2. Apakah corak Model Domain?
  3. Apakah masalah khusus yang Hibernate dan JOOQ selesaikan?
  4. Mengapa satu tidak menggantikan yang lain, dan bagaimana ia boleh wujud bersama?

JOOQ Is Not a Replacement for Hibernate. They Solve Different Problems

Skrip Transaksi

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);
    }
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Dalam kod ini:

  1. Kami mengira berapa banyak ceramah yang telah diserahkan oleh penceramah.
  2. Kami menyemak sama ada bilangan maksimum yang dibenarkan bagi ceramah yang dihantar telah melebihi.
  3. Jika semuanya okey, kami buat ceramah baharu dengan status DISERAHKAN.

Terdapat keadaan perlumbaan yang berpotensi di sini, tetapi untuk kesederhanaan, kami tidak akan menumpukan pada itu.

Kebaikan pendekatan ini:

  1. SQL yang sedang dilaksanakan adalah mudah dan boleh diramal. Ia mudah untuk mengubah suai untuk peningkatan prestasi jika perlu.
  2. Kami hanya mengambil data yang diperlukan daripada pangkalan data.
  3. Dengan JOOQ, kod ini boleh ditulis dengan lebih ringkas, padat dan dengan penaipan statik!

Keburukan:

  1. Adalah mustahil untuk menguji logik perniagaan dengan ujian unit sahaja. Anda memerlukan ujian penyepaduan (dan beberapa daripadanya).
  2. Jika domain itu rumit, pendekatan ini boleh membawa kepada kod spageti dengan cepat.
  3. Terdapat risiko pertindihan kod, yang boleh membawa kepada pepijat yang tidak dijangka semasa sistem berkembang.

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.

Model Domain

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);
    }
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

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
    }
}
Salin selepas log masuk

Kemudian SpeakerService boleh dilaksanakan dengan cara ini:

public interface SpeakerRepository {
    Speaker findById(Long id);
    void save(Speaker speaker);
}
Salin selepas log masuk

Kebaikan Model Domain:

  1. Objek domain dipisahkan sepenuhnya daripada butiran pelaksanaan (iaitu, pangkalan data). Ini menjadikannya mudah untuk diuji dengan ujian unit biasa.
  2. Logik perniagaan dipusatkan dalam objek domain. Ini sangat mengurangkan risiko logik merebak ke seluruh aplikasi, tidak seperti dalam pendekatan Skrip Transaksi.
  3. Jika mahu, objek domain boleh dibuat tidak berubah sepenuhnya, yang meningkatkan keselamatan apabila bekerja dengannya (anda boleh menghantarnya kepada sebarang kaedah tanpa perlu risau tentang pengubahsuaian yang tidak disengajakan).
  4. Medan dalam objek domain boleh digantikan dengan Objek Nilai, yang bukan sahaja meningkatkan kebolehbacaan tetapi juga memastikan kesahihan medan pada masa tugasan (anda tidak boleh membuat Objek Nilai dengan kandungan tidak sah).

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?

Pemetaan manual

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);
    }
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

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:

  1. Kod ini kekal sangat telus, sama seperti dalam pendekatan Skrip Transaksi.
  2. Tiada lagi isu dengan menguji logik perniagaan hanya melalui ujian integrasi. Ini hanya diperlukan untuk pelaksanaan repositori (dan mungkin beberapa senario E2E).
  3. Kod pemetaan ada di hadapan kami. Tiada sihir Hibernate terlibat. Menemui pepijat? Cari garisan yang betul dan betulkan.

Keperluan untuk Hibernate

Perkara menjadi lebih menarik di dunia nyata, di mana anda mungkin menghadapi senario seperti ini:

  1. Objek domain mungkin perlu menyokong pewarisan.
  2. Sekumpulan medan boleh digabungkan menjadi Objek Nilai yang berasingan (Terbenam dalam JPA/Hibernate).
  3. Sesetengah medan tidak seharusnya dimuatkan setiap kali anda mengambil objek domain, tetapi hanya apabila diakses, untuk meningkatkan prestasi (pemuatan malas).
  4. Boleh wujud hubungan yang kompleks antara objek (satu-ke-banyak, banyak-ke-banyak, dsb.).
  5. Anda perlu memasukkan hanya medan yang telah berubah dalam pernyataan KEMASKINI kerana medan lain jarang berubah dan tiada gunanya menghantarnya melalui rangkaian (anotasi DynamicUpdate).

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.

Matlamat JOOQ dan Hibernate

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.

Bagaimana dengan DAO dalam JOOQ?

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:

  1. Entiti tidak akan mempunyai sebarang perhubungan: tiada ManyToOne, tiada OneToMany. Hanya lajur pangkalan data, yang menjadikan penulisan logik perniagaan lebih sukar.
  2. Entiti dijana secara individu. Anda tidak boleh menyusunnya ke dalam hierarki warisan.
  3. Hakikat bahawa entiti dijana bersama DAO bermakna anda tidak boleh mengubah suainya mengikut kehendak anda. Contohnya, menggantikan medan dengan Objek Nilai, menambah perhubungan kepada entiti lain atau mengumpulkan medan ke dalam Boleh Dibenam tidak akan dapat dilakukan kerana menjana semula entiti akan menimpa perubahan anda. Ya, anda boleh mengkonfigurasi penjana untuk mencipta entiti secara berbeza sedikit, tetapi pilihan penyesuaian adalah terhad (dan tidak semudah menulis kod sendiri).

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.

Kesimpulan

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!

Sumber

  1. JDBI
  2. Skrip Transaksi
  3. Model Domain
  4. Artikel saya – Model Domain Kaya dengan But Spring dan Hibernate
  5. Corak repositori
  6. Objek Nilai
  7. JPA Terbenam
  8. JPA DynamicUpdate
  9. CQRS
  10. Lukas Eder: Kepada DAO atau tidak kepada DAO

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!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan