ホームページ >Java >&#&チュートリアル >JOOQ は Hibernate に代わるものではありません。さまざまな問題を解決します
私はもともとこの記事をロシア語で書きました。したがって、ネイティブ スピーカーの方は、このリンクから読むことができます。
ここ 1 年ほどで、JOOQ が Hibernate に代わる最新の優れた代替手段であることを示唆する記事や講演を目にしました。通常、引数には次のものが含まれます:
前もって言っておきますが、私は JOOQ を優れたライブラリ (具体的にはライブラリであり、Hibernate のようなフレームワークではありません) だと考えています。これは、静的に型指定された方法で SQL を操作してコンパイル時にほとんどのエラーを検出するという、そのタスクにおいて優れています。
しかし、Hibernate の時代は過ぎたので、今はすべて JOOQ を使用して記述すべきだという議論を聞くと、リレーショナル データベースの時代は終わった、今は NoSQL のみを使用すべきだと言っているように私には聞こえます。面白いと思いませんか?しかし、少し前までは、そのような議論は非常に深刻なものでした。
問題は、これら 2 つのツールが対処する核心的な問題の誤解にあると思います。この記事では、これらの疑問を明らかにすることを目的としています。以下について調査していきます:
データベースを操作する最も単純かつ直感的な方法は、トランザクション スクリプト パターンです。簡単に言うと、すべてのビジネス ロジックを、単一のトランザクションに結合された一連の SQL コマンドとして編成します。通常、クラス内の各メソッドはビジネス操作を表し、1 つのトランザクションに限定されます。
講演者が自分の講演をカンファレンスに提出できるアプリケーションを開発しているとします (話を簡単にするために、講演のタイトルのみを記録します)。トランザクション スクリプト パターンに従って、トークを送信する方法は次のようになります (JDBI for 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); } }
このコード内:
ここには競合状態の可能性がありますが、簡単にするためにそこには焦点を当てません。
このアプローチの長所:
短所:
このアプローチは有効であり、サービスのロジックが非常に単純で、時間の経過とともに複雑になることが予想されない場合には理にかなっています。ただし、ドメインは多くの場合これより大きくなります。したがって、代替手段が必要です。
ドメイン モデル パターンの考え方は、ビジネス ロジックを SQL コマンドに直接結びつけなくなるということです。代わりに、動作を記述し、ドメイン エンティティに関するデータを保存するドメイン オブジェクト (Java のコンテキストではクラス) を作成します。
この記事では、貧血モデルと富裕モデルの違いについては説明しません。ご興味がございましたら、このトピックに関する詳細な記事を書きました。
ビジネス シナリオ (サービス) では、これらのオブジェクトのみを使用し、特定のデータベース クエリに関連付けられることを避けてください。
もちろん、実際には、パフォーマンス要件を満たすために、ドメイン オブジェクトとの対話と直接データベース クエリを組み合わせる場合があります。ここでは、カプセル化と分離に違反しない、ドメイン モデルを実装するための古典的なアプローチについて説明します。
たとえば、前述したように、Speaker と Talk というエンティティについて話している場合、ドメイン オブジェクトは次のようになります。
@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); } }
ここで、Speaker クラスには、講演を送信するためのビジネス ロジックが含まれています。データベースの対話が抽象化され、ドメイン モデルがビジネス ルールに集中できるようになります。
次のリポジトリ インターフェースを想定します:
@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 } }
SpeakerService は次のように実装できます。
public interface SpeakerRepository { Speaker findById(Long id); void save(Speaker speaker); }
ドメイン モデルの長所:
一言で言えば、メリットはたくさんあります。ただし、重要な課題が 1 つあります。興味深いことに、ドメイン モデル パターンを推奨するドメイン駆動設計に関する書籍では、この問題についてはまったく言及されていないか、簡単に触れられているだけです。
問題は、ドメイン オブジェクトをデータベースに保存し、それをどのようにして読み戻すのかということです。言い換えれば、リポジトリはどのように実装すればよいのでしょうか?
今では、その答えは明らかです。 Hibernate (またはさらに良いのは Spring Data JPA) を使用するだけで、手間が省けます。しかし、ORM フレームワークがまだ発明されていない世界にいると想像してみましょう。この問題はどうやって解決すればいいでしょうか?
SpeakerRepository を実装するには、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); } }
アプローチは簡単です。リポジトリごとに、SQL ライブラリ (JOOQ や JDBI など) を使用してデータベースと連携する個別の実装を作成します。
一見すると (おそらく二度目にも)、このソリューションは非常に優れているように見えるかもしれません。これを考慮してください:
現実の世界では、次のようなシナリオに遭遇する可能性があるため、物事はさらに興味深いものになります。
それに加えて、ビジネス ロジックとドメイン オブジェクトの進化に応じてマッピング コードを保守する必要があります。
これらの各点を自分で処理しようとすると、最終的には (驚いたことに!) Hibernate のようなフレームワーク、またはそのはるかに単純なバージョンを作成することになります。
JOOQ は、SQL クエリを作成する際の静的型付けの欠如に対処します。これは、コンパイル段階でのエラーの数を減らすのに役立ちます。データベース スキーマから直接コードを生成すると、スキーマが更新されると、コードを修正する必要がある箇所がすぐに表示されます (コンパイルされないだけです)。
Hibernate は、ドメイン オブジェクトをリレーショナル データベースにマッピングする、またはその逆の問題 (データベースからデータを読み取り、ドメイン オブジェクトにマッピングする) を解決します。
したがって、Hibernate の方が劣っている、または JOOQ の方が優れていると主張するのは意味がありません。これらのツールはさまざまな目的のために設計されています。アプリケーションがトランザクション スクリプト パラダイムに基づいて構築されている場合、JOOQ は間違いなく理想的な選択肢です。ただし、ドメイン モデル パターンを使用して Hibernate を回避したい場合は、カスタム リポジトリ実装での手動マッピングの楽しみに対処する必要があります。もちろん、雇用主がさらに別の Hibernate Killer を構築するためにお金を払っているのであれば、疑問はありません。しかしおそらく、オブジェクトとデータベースのマッピングのためのインフラストラクチャ コードではなく、ビジネス ロジックに焦点を当てることを期待されています。
ところで、CQRS には Hibernate と JOOQ の組み合わせがうまく機能すると思います。 CREATE/UPDATE/DELETE 操作などのコマンドを実行するアプリケーション (またはその論理部分) があるとします。これは Hibernate が最適な場所です。一方、データを読み取るクエリ サービスがあります。ここで、JOOQは素晴らしいです。 Hibernate よりも複雑なクエリの構築と最適化がはるかに簡単になります。
本当です。 JOOQ を使用すると、データベースからエンティティを取得するための標準クエリを含む DAO を生成できます。これらの DAO をメソッドで拡張することもできます。さらに、JOOQ は、Hibernate と同様にセッターを使用して設定できるエンティティを生成し、DAO の挿入メソッドまたは更新メソッドに渡します。 Spring Data と似ていませんか?
単純なケースでは、これは実際に機能します。ただし、リポジトリを手動で実装するのとそれほど変わりません。問題は似ています:
したがって、複雑なドメイン モデルを構築したい場合は、手動で行う必要があります。 Hibernate を使用しない場合、マッピングの責任はすべてユーザーにあります。確かに、JOOQ の使用は JDBI よりも快適ですが、プロセスには依然として労力がかかります。
JOOQ の作成者である Lukas Eder でさえ、DAO がライブラリに追加されたのは、DAO が人気のパターンだからであり、必ずしも DAO の使用を推奨しているからではないとブログで述べています。
記事をお読みいただきありがとうございます。私は Hibernate の大ファンで、Hibernate は優れたフレームワークだと考えています。ただし、JOOQ の方が便利だと感じる人もいると思います。私の記事の主なポイントは、Hibernate と JOOQ はライバルではないということです。これらのツールは、価値をもたらすものであれば、同じ製品内でも共存できます。
コンテンツについてご意見やフィードバックがございましたら、喜んでお話しさせていただきます。生産的な一日をお過ごしください!
以上がJOOQ は Hibernate に代わるものではありません。さまざまな問題を解決しますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。