ホームページ >Java >&#&チュートリアル >JOOQ は Hibernate に代わるものではありません。さまざまな問題を解決します

JOOQ は Hibernate に代わるものではありません。さまざまな問題を解決します

DDD
DDDオリジナル
2025-01-11 20:10:41521ブラウズ

私はもともとこの記事をロシア語で書きました。したがって、ネイティブ スピーカーの方は、このリンクから読むことができます。

ここ 1 年ほどで、JOOQ が Hibernate に代わる最新の優れた代替手段であることを示唆する記事や講演を目にしました。通常、引数には次のものが含まれます:

  1. JOOQ ではコンパイル時にすべてを検証できますが、Hibernate では検証できません!
  2. Hibernate は奇妙で常に最適とは限らないクエリを生成しますが、JOOQ ではすべてが透過的です!
  3. Hibernate エンティティは変更可能ですが、これは問題です。 JOOQ を使用すると、すべてのエンティティを不変にすることができます (関数型プログラミングです)!
  4. JOOQ には、注釈に関する「魔法」は一切含まれていません!

前もって言っておきますが、私は JOOQ を優れたライブラリ (具体的にはライブラリであり、Hibernate のようなフレームワークではありません) だと考えています。これは、静的に型指定された方法で SQL を操作してコンパイル時にほとんどのエラーを検出するという、そのタスクにおいて優れています。

しかし、Hibernate の時代は過ぎたので、今はすべて JOOQ を使用して記述すべきだという議論を聞くと、リレーショナル データベースの時代は終わった、今は NoSQL のみを使用すべきだと言っているように私には聞こえます。面白いと思いませんか?しかし、少し前までは、そのような議論は非常に深刻なものでした。

問題は、これら 2 つのツールが対処する核心的な問題の誤解にあると思います。この記事では、これらの疑問を明らかにすることを目的としています。以下について調査していきます:

  1. トランザクションスクリプトとは何ですか?
  2. ドメイン モデル パターンとは何ですか?
  3. Hibernate と JOOQ は具体的にどのような問題を解決しますか?
  4. なぜ一方が他方の代わりにならないのですか?また、どうすれば共存できるのでしょうか?

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

トランザクションスクリプト

データベースを操作する最も単純かつ直感的な方法は、トランザクション スクリプト パターンです。簡単に言うと、すべてのビジネス ロジックを、単一のトランザクションに結合された一連の 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);
    }
}

このコード内:

  1. 講演者がすでに提出した講演の数をカウントします。
  2. 送信されたトークの最大許容数を超えているかどうかを確認します。
  3. すべてが問題なければ、ステータスが SUBMITTED の新しいトークを作成します。

ここには競合状態の可能性がありますが、簡単にするためにそこには焦点を当てません。

このアプローチの長所:

  1. 実行される SQL は単純で予測可能です。必要に応じてパフォーマンスを向上させるために簡単に調整できます。
  2. 必要なデータのみをデータベースから取得します。
  3. JOOQ を使用すると、このコードを静的型付けでより簡単、簡潔に記述することができます!

短所:

  1. 単体テストだけでビジネス ロジックをテストすることは不可能です。統合テスト (そしてかなりの数のテスト) が必要になります。
  2. ドメインが複雑な場合、このアプローチはすぐにスパゲッティ コードになる可能性があります。
  3. コードの重複のリスクがあり、システムが進化するにつれて予期しないバグが発生する可能性があります。

このアプローチは有効であり、サービスのロジックが非常に単純で、時間の経過とともに複雑になることが予想されない場合には理にかなっています。ただし、ドメインは多くの場合これより大きくなります。したがって、代替手段が必要です。

ドメインモデル

ドメイン モデル パターンの考え方は、ビジネス ロジックを 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. ドメイン オブジェクトは実装の詳細 (データベースなど) から完全に切り離されています。これにより、定期的な単体テストで簡単にテストできるようになります。
  2. ビジネス ロジックはドメイン オブジェクト内で集中化されます。これにより、トランザクション スクリプトのアプローチとは異なり、ロジックがアプリケーション全体に広がるリスクが大幅に軽減されます。
  3. 必要に応じて、ドメイン オブジェクトを完全に不変にすることができます。これにより、ドメイン オブジェクトを操作する際の安全性が向上します (偶発的な変更を心配することなく、任意のメソッドに渡すことができます)。
  4. ドメイン オブジェクトのフィールドは値オブジェクトに置き換えることができます。これにより、可読性が向上するだけでなく、割り当て時のフィールドの有効性も保証されます (無効なコンテンツを含む値オブジェクトを作成することはできません)。

一言で言えば、メリットはたくさんあります。ただし、重要な課題が 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 など) を使用してデータベースと連携する個別の実装を作成します。

一見すると (おそらく二度目にも)、このソリューションは非常に優れているように見えるかもしれません。これを考慮してください:

  1. トランザクション スクリプトのアプローチと同様に、コードは高い透明性を保ちます。
  2. 統合テストを通じてのみビジネス ロジックをテストするという問題はもうありません。これらは、リポジトリの実装 (およびおそらくいくつかの E2E シナリオ) にのみ必要です。
  3. マッピングコードは目の前にあります。 Hibernate の魔法は関係しません。バグが見つかりましたか?適切な行を見つけて修正します。

休止状態の必要性

現実の世界では、次のようなシナリオに遭遇する可能性があるため、物事はさらに興味深いものになります。

  1. ドメイン オブジェクトは継承をサポートする必要がある場合があります。
  2. フィールドのグループは、個別の値オブジェクト (JPA/Hibernate に埋め込み) に結合できます。
  3. 一部のフィールドは、パフォーマンスを向上させる (遅延読み込み) ため、ドメイン オブジェクトをフェッチするたびにロードするのではなく、アクセスされたときにのみロードする必要があります。
  4. オブジェクト間には複雑な関係 (1 対多、多対多など) が存在する場合があります。
  5. 他のフィールドはほとんど変更されず、ネットワーク (DynamicUpdate アノテーション) 経由で送信しても意味がないため、UPDATE ステートメントには変更されたフィールドのみを含める必要があります。

それに加えて、ビジネス ロジックとドメイン オブジェクトの進化に応じてマッピング コードを保守する必要があります。

これらの各点を自分で処理しようとすると、最終的には (驚いたことに!) Hibernate のようなフレームワーク、またはそのはるかに単純なバージョンを作成することになります。

JOOQ と Hibernate の目標

JOOQ は、SQL クエリを作成する際の静的型付けの欠如に対処します。これは、コンパイル段階でのエラーの数を減らすのに役立ちます。データベース スキーマから直接コードを生成すると、スキーマが更新されると、コードを修正する必要がある箇所がすぐに表示されます (コンパイルされないだけです)。

Hibernate は、ドメイン オブジェクトをリレーショナル データベースにマッピングする、またはその逆の問題 (データベースからデータを読み取り、ドメイン オブジェクトにマッピングする) を解決します。

したがって、Hibernate の方が劣っている、または JOOQ の方が優れていると主張するのは意味がありません。これらのツールはさまざまな目的のために設計されています。アプリケーションがトランザクション スクリプト パラダイムに基づいて構築されている場合、JOOQ は間違いなく理想的な選択肢です。ただし、ドメイン モデル パターンを使用して Hibernate を回避したい場合は、カスタム リポジトリ実装での手動マッピングの楽しみに対処する必要があります。もちろん、雇用主がさらに別の Hibernate Killer を構築するためにお金を払っているのであれば、疑問はありません。しかしおそらく、オブジェクトとデータベースのマッピングのためのインフラストラクチャ コードではなく、ビジネス ロジックに焦点を当てることを期待されています。

ところで、CQRS には Hibernate と JOOQ の組み合わせがうまく機能すると思います。 CREATE/UPDATE/DELETE 操作などのコマンドを実行するアプリケーション (またはその論理部分) があるとします。これは Hibernate が最適な場所です。一方、データを読み取るクエリ サービスがあります。ここで、JOOQは素晴らしいです。 Hibernate よりも複雑なクエリの構築と最適化がはるかに簡単になります。

JOOQ の DAO についてはどうですか?

本当です。 JOOQ を使用すると、データベースからエンティティを取得するための標準クエリを含む DAO を生成できます。これらの DAO をメソッドで拡張することもできます。さらに、JOOQ は、Hibernate と同様にセッターを使用して設定できるエンティティを生成し、DAO の挿入メソッドまたは更新メソッドに渡します。 Spring Data と似ていませんか?

単純なケースでは、これは実際に機能します。ただし、リポジトリを手動で実装するのとそれほど変わりません。問題は似ています:

  1. エンティティには関係がありません。ManyToOne や OneToMany もありません。データベースの列だけなので、ビジネス ロジックの記述が非常に難しくなります。
  2. エンティティは個別に生成されます。これらを継承階層に整理することはできません。
  3. エンティティが DAO とともに生成されるということは、エンティティを自由に変更できないことを意味します。たとえば、フィールドを値オブジェクトに置き換えたり、別のエンティティにリレーションシップを追加したり、フィールドを Embeddable にグループ化したりすることは、エンティティを再生成すると変更が上書きされるため、実行できません。はい、少し異なる方法でエンティティを作成するようにジェネレーターを構成できますが、カスタマイズ オプションは限られています (コードを自分で記述するほど便利ではありません)。

したがって、複雑なドメイン モデルを構築したい場合は、手動で行う必要があります。 Hibernate を使用しない場合、マッピングの責任はすべてユーザーにあります。確かに、JOOQ の使用は JDBI よりも快適ですが、プロセスには依然として労力がかかります。

JOOQ の作成者である Lukas Eder でさえ、DAO がライブラリに追加されたのは、DAO が人気のパターンだからであり、必ずしも DAO の使用を推奨しているからではないとブログで述べています。

結論

記事をお読みいただきありがとうございます。私は Hibernate の大ファンで、Hibernate は優れたフレームワークだと考えています。ただし、JOOQ の方が便利だと感じる人もいると思います。私の記事の主なポイントは、Hibernate と JOOQ はライバルではないということです。これらのツールは、価値をもたらすものであれば、同じ製品内でも共存できます。

コンテンツについてご意見やフィードバックがございましたら、喜んでお話しさせていただきます。生産的な一日をお過ごしください!

リソース

  1. JDBI
  2. トランザクションスクリプト
  3. ドメインモデル
  4. 私の記事 – Spring Boot と Hibernate を使用したリッチ ドメイン モデル
  5. リポジトリ パターン
  6. 値オブジェクト
  7. JPA 埋め込み
  8. JPA 動的更新
  9. CQRS
  10. ルーカス・エダー: DAO するかしないか

以上がJOOQ は Hibernate に代わるものではありません。さまざまな問題を解決しますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。