1. 序章

このチュートリアルでは、Hibernateの操作中に発生する可能性のあるいくつかの一般的な例外について説明します。

それらの目的といくつかの一般的な原因を確認します。 さらに、それらのソリューションを調べます。

2. Hibernate例外の概要

多くの条件により、Hibernateの使用中に例外がスローされる可能性があります。 これらは、マッピングエラー、インフラストラクチャの問題、SQLエラー、データ整合性違反、セッションの問題、およびトランザクションエラーである可能性があります。

これらの例外は主にHibernateExceptionから拡張されます。ただし、JPA永続性プロバイダーとしてHibernateを使用している場合、これらの例外はPersistenceExceptionにラップされる可能性があります。

これらの基本クラスは両方ともRuntimeExceptionから拡張されています。 したがって、それらはすべてチェックされていません。 したがって、使用するすべての場所でそれらをキャッチまたは宣言する必要はありません。

さらに、 これらのほとんどは回復不能です。 その結果、操作を再試行しても効果はありません。 これは、それらに遭遇したときに現在のセッションを放棄する必要があることを意味します。

次に、これらを1つずつ調べてみましょう。

3. マッピングエラー

オブジェクトリレーショナルマッピングは、Hibernateの主な利点です。 具体的には、SQLステートメントを手動で作成する必要がなくなります。

同時に、Javaオブジェクトとデータベーステーブル間のマッピングを指定する必要があります。 したがって、注釈またはマッピングドキュメントを使用してそれらを指定します。 これらのマッピングは手動でコーディングできます。 または、ツールを使用してそれらを生成することもできます。

これらのマッピングを指定する際に、間違いを犯す可能性があります。 これらはマッピング仕様に含まれている可能性があります。 または、Javaオブジェクトと対応するデータベーステーブルの間に不一致がある可能性があります。

このようなマッピングエラーは例外を生成します。 私たちは初期の開発中にそれらに頻繁に出くわします。 さらに、環境間で変更を移行しているときに、それらに遭遇する可能性があります。

いくつかの例でこれらのエラーを調べてみましょう。

3.1. MappingException

オブジェクトリレーショナルマッピングの問題により、MappingExceptionがスローされます

public void whenQueryExecutedWithUnmappedEntity_thenMappingException() {
    thrown.expectCause(isA(MappingException.class));
    thrown.expectMessage("Unknown entity: java.lang.String");

    Session session = sessionFactory.getCurrentSession();
    NativeQuery<String> query = session
      .createNativeQuery("select name from PRODUCT", String.class);
    query.getResultList();
}

上記のコードでは、 createNativeQuery メソッドは、クエリ結果を指定されたJavaタイプにマップしようとします弦。 の暗黙的なマッピングを使用しますからのクラスメタモデルマッピングを行います。

ただし、Stringクラスにはマッピングが指定されていません。 したがって、Hibernateはname列をStringにマップする方法を知らず、例外をスローします。

考えられる原因と解決策の詳細な分析については、 Hibernate Mapping Exception – UnknownEntityを確認してください。

同様に、他のエラーもこの例外を引き起こす可能性があります。

  • フィールドとメソッドの注釈の混合
  • @ManyToManyアソシエーションに@JoinTableを指定できない
  • マップされたクラスのデフォルトのコンストラクターは、マッピング処理中に例外をスローします

さらに、 MappingExceptionには、特定のマッピングの問題を示す可能性のあるいくつかのサブクラスがあります。

  • AnnotationException –注釈の問題
  • DuplicateMappingException – クラス、テーブル、またはプロパティ名の重複マッピング
  • InvalidMappingException –マッピングが無効です
  • MappingNotFoundException –マッピングリソースが見つかりませんでした
  • PropertyNotFoundException –予期されたゲッターまたはセッターメソッドがクラスで見つかりませんでした

したがって、この例外が発生した場合は、最初にマッピングを確認する必要があります。

3.2. AnnotationException

AnnotationExceptionを理解するために、フィールドまたはプロパティに識別子注釈のないエンティティを作成しましょう。

@Entity
public class EntityWithNoId {
    private int id;
    public int getId() {
        return id;
    }

    // standard setter
}

Hibernateはすべてのエンティティに識別子があることを想定しているため、エンティティを使用するとAnnotationExceptionが発生します。

public void givenEntityWithoutId_whenSessionFactoryCreated_thenAnnotationException() {
    thrown.expect(AnnotationException.class);
    thrown.expectMessage("No identifier specified for entity");

    Configuration cfg = getConfiguration();
    cfg.addAnnotatedClass(EntityWithNoId.class);
    cfg.buildSessionFactory();
}

さらに、他の考えられる原因は次のとおりです。

  • @GeneratedValueアノテーションで使用されている不明なシーケンスジェネレーター
  • Java 8 Date / Timeクラスで使用される@Temporalアノテーション
  • @ManyToOneまたは@OneToManyのターゲットエンティティが見つからないか存在しません
  • 関係注釈@OneToManyまたは@ManyToManyで使用される未加工のコレクションクラス
  • Hibernateがコレクションインターフェイスを想定しているため、コレクションアノテーション @OneToMany @ManyToMany 、または@ElementCollectionで使用される具象クラス

この例外を解決するには、最初にエラーメッセージに記載されている特定の注釈を確認する必要があります。

4. スキーマ管理エラー

自動データベーススキーマ管理は、Hibernateのもう1つの利点です。 たとえば、データベースオブジェクトを作成または検証するためのDDLステートメントを生成できます。

この機能を使用するには、hibernate.hbm2ddl.autoプロパティを適切に設定する必要があります。

スキーマ管理の実行中に問題が発生した場合、例外が発生します。 これらのエラーを調べてみましょう。

4.1. SchemaManagementException

スキーマ管理の実行におけるインフラストラクチャ関連の問題により、SchemaManagementExceptionが発生します。

実例を示すために、データベーススキーマを検証するようにHibernateに指示しましょう。

public void givenMissingTable_whenSchemaValidated_thenSchemaManagementException() {
    thrown.expect(SchemaManagementException.class);
    thrown.expectMessage("Schema-validation: missing table");

    Configuration cfg = getConfiguration();
    cfg.setProperty(AvailableSettings.HBM2DDL_AUTO, "validate");
    cfg.addAnnotatedClass(Product.class);
    cfg.buildSessionFactory();
}

Product に対応するテーブルがデータベースに存在しないため、S essionFactoryの構築中にスキーマ検証例外が発生します。

さらに、この例外には他にも考えられるシナリオがあります。

  • データベースに接続してスキーマ管理タスクを実行できません
  • スキーマがデータベースに存在しません

4.2. CommandAcceptanceException

特定のスキーマ管理コマンドに対応するDDLの実行で問題が発生すると、CommandAcceptanceExceptionが発生する可能性があります。

例として、 SessionFactory を設定するときに、間違った方言を指定してみましょう。

public void whenWrongDialectSpecified_thenCommandAcceptanceException() {
    thrown.expect(SchemaManagementException.class);
        
    thrown.expectCause(isA(CommandAcceptanceException.class));
    thrown.expectMessage("Halting on error : Error executing DDL");

    Configuration cfg = getConfiguration();
    cfg.setProperty(AvailableSettings.DIALECT,
      "org.hibernate.dialect.MySQLDialect");
    cfg.setProperty(AvailableSettings.HBM2DDL_AUTO, "update");
    cfg.setProperty(AvailableSettings.HBM2DDL_HALT_ON_ERROR,"true");
    cfg.getProperties()
      .put(AvailableSettings.HBM2DDL_HALT_ON_ERROR, true);

    cfg.addAnnotatedClass(Product.class);
    cfg.buildSessionFactory();
}

ここでは、間違った方言を指定しました。 MySQLDialect。 また、スキーマオブジェクトを更新するようにHibernateに指示しています。 その結果、H2データベースを更新するためにHibernateによって実行されるDDLステートメントは失敗し、例外が発生します。

デフォルトでは、Hibernateはこの例外をサイレントにログに記録し、次に進みます。後で SessionFactoryを使用すると、例外が発生します。

このエラーで例外がスローされるようにするために、プロパティHBM2DDL_HALT_ON_ERRORtrueに設定しました。

同様に、これらはこのエラーのその他の一般的な原因です。

  • マッピングとデータベースの間で列名に不一致があります
  • 2つのクラスが同じテーブルにマップされます
  • クラスまたはテーブルに使用される名前は、 USER など、データベース内の予約語です。
  • データベースへの接続に使用されたユーザーに必要な権限がありません

5. SQL実行エラー

Hibernateを使用してデータを挿入、更新、削除、またはクエリすると、JDBCを使用してデータベースに対してDMLステートメントが実行されます。 このAPIは、操作によってエラーまたは警告が発生した場合にSQLExceptionを発生させます。

Hibernateは、この例外をJDBCExceptionまたはその適切なサブクラスの1つに変換します。

  • ConstraintViolationException
  • DataException
  • JDBCConnectionException
  • LockAcquisitionException
  • PessimisticLockException
  • QueryTimeoutException
  • SQLGrammarException
  • GenericJDBCException

一般的なエラーについて説明しましょう。

5.1. JDBCException

JDBCException は、常に特定のSQLステートメントによって発生します。 getSQL メソッドを呼び出して、問題のあるSQLステートメントを取得できます。

さらに、 getSQLException メソッドを使用して、基になるSQLExceptionを取得できます。

5.2. SQLGrammarException

SQLGrammarException は、データベースに送信されたSQLが無効であることを示します。 構文エラーまたは無効なオブジェクト参照が原因である可能性があります。

たとえば、テーブルがない場合、データのクエリ中にこのエラーが発生する可能性があります

public void givenMissingTable_whenQueryExecuted_thenSQLGrammarException() {
    thrown.expect(isA(PersistenceException.class));
    thrown.expectCause(isA(SQLGrammarException.class));
    thrown.expectMessage("SQLGrammarException: could not prepare statement");

    Session session = sessionFactory.getCurrentSession();
    NativeQuery<Product> query = session.createNativeQuery(
      "select * from NON_EXISTING_TABLE", Product.class);
    query.getResultList();
}

また、テーブルが欠落している場合、データの保存中にこのエラーが発生する可能性があります。

public void givenMissingTable_whenEntitySaved_thenSQLGrammarException() {
    thrown.expect(isA(PersistenceException.class));
    thrown.expectCause(isA(SQLGrammarException.class));
    thrown
      .expectMessage("SQLGrammarException: could not prepare statement");

    Configuration cfg = getConfiguration();
    cfg.addAnnotatedClass(Product.class);

    SessionFactory sessionFactory = cfg.buildSessionFactory();
    Session session = null;
    Transaction transaction = null;
    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();
        Product product = new Product();
        product.setId(1);
        product.setName("Product 1");
        session.save(product);
        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
        closeSessionFactoryQuietly(sessionFactory);
    }
}

その他の考えられる原因は次のとおりです。

  • 使用される命名戦略は、クラスを正しいテーブルにマップしません
  • @JoinColumnで指定された列が存在しません

5.3. ConstraintViolationException

ConstraintViolationException は、要求されたDML操作によって整合性制約に違反したことを示します。 この制約の名前は、getConstraintNameメソッドを呼び出すことで取得できます。

この例外の一般的な原因は、重複するレコードを保存しようとしていることです。

public void whenDuplicateIdSaved_thenConstraintViolationException() {
    thrown.expect(isA(PersistenceException.class));
    thrown.expectCause(isA(ConstraintViolationException.class));
    thrown.expectMessage(
      "ConstraintViolationException: could not execute statement");

    Session session = null;
    Transaction transaction = null;

    for (int i = 1; i <= 2; i++) {
        try {
            session = sessionFactory.openSession();
            transaction = session.beginTransaction();
            Product product = new Product();
            product.setId(1);
            product.setName("Product " + i);
            session.save(product);
            transaction.commit();
        } catch (Exception e) {
            rollbackTransactionQuietly(transaction);
            throw (e);
        } finally {
            closeSessionQuietly(session);
        }
    }
}

また、null値をデータベースのNOTNULL 列に保存すると、このエラーが発生する可能性があります。

このエラーを解決するには、ビジネスレイヤーですべての検証を実行する必要があります。 さらに、データベースの制約を使用してアプリケーションの検証を行うことはできません。

5.4. DataException

DataException は、SQLステートメントの評価により、不正な操作、タイプの不一致、または不適切なカーディナリティが発生したことを示します。

たとえば、数値列に対して文字データを使用すると、次のエラーが発生する可能性があります。

public void givenQueryWithDataTypeMismatch_WhenQueryExecuted_thenDataException() {
    thrown.expectCause(isA(DataException.class));
    thrown.expectMessage(
      "org.hibernate.exception.DataException: could not prepare statement");

    Session session = sessionFactory.getCurrentSession();
    NativeQuery<Product> query = session.createNativeQuery(
      "select * from PRODUCT where id='wrongTypeId'", Product.class);
    query.getResultList();
}

このエラーを修正するには、アプリケーションコードとデータベースの間でデータ型と長さが一致していることを確認する必要があります。

5.5. JDBCConnectionException

JDBCConectionException は、データベースとの通信に問題があることを示します。

たとえば、データベースまたはネットワークがダウンすると、この例外がスローされる可能性があります。

さらに、データベースの設定が正しくないと、この例外が発生する可能性があります。 そのようなケースの1つは、サーバーが長時間アイドル状態だったためにデータベース接続がサーバーによって閉じられていることです。 これは、接続プーリングを使用していて、プールのアイドルタイムアウト設定がデータベースの接続タイムアウト値を超えている場合に発生する可能性があります。

この問題を解決するには、最初にデータベースホストが存在し、稼働していることを確認する必要があります。 次に、データベース接続に正しい認証が使用されていることを確認する必要があります。 最後に、タイムアウト値が接続プールに正しく設定されていることを確認する必要があります。

5.6. QueryTimeoutException

データベースクエリがタイムアウトすると、この例外が発生します。 表領域がいっぱいになるなど、他のエラーが原因で発生することもあります。

これは、回復可能な数少ないエラーの1つです。つまり、同じトランザクションでステートメントを再試行できます。

この問題を修正するために、複数の方法で長時間実行クエリのクエリタイムアウトを増やすことができます。

  • timeout要素を@NamedQueryまたは@NamedNativeQueryアノテーションに設定します
  • クエリインターフェイスのsetHintメソッドを呼び出します
  • TransactionインターフェースのsetTimeoutメソッドを呼び出します
  • QueryインターフェースのsetTimeoutメソッドを呼び出します

6. セッション状態関連のエラー

次に、Hibernateセッションの使用エラーによるエラーを調べてみましょう。

6.1. NonUniqueObjectException

Hibernateは、単一のセッションで同じ識別子を持つ2つのオブジェクトを許可しません。

同じJavaクラスの2つのインスタンスを1つのセッションで同じ識別子に関連付けようとすると、NonUniqueObjectExceptionが発生します。  getEntityName()メソッドと getIdentifier()メソッドを呼び出すことで、エンティティの名前と識別子を取得できます。

このエラーを再現するために、Productの2つのインスタンスを同じIDでセッションに保存してみましょう。

public void 
givenSessionContainingAnId_whenIdAssociatedAgain_thenNonUniqueObjectException() {
    thrown.expect(isA(NonUniqueObjectException.class));
    thrown.expectMessage(
      "A different object with the same identifier value was already associated with the session");

    Session session = null;
    Transaction transaction = null;

    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(1);
        product.setName("Product 1");
        session.save(product);

        product = new Product();
        product.setId(1);
        product.setName("Product 2");
        session.save(product);

        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

期待どおりにNonUniqueObjectException、が発生します。

この例外は、updateメソッドを呼び出してデタッチされたオブジェクトをセッションに再アタッチしているときに頻繁に発生します。セッションに同じ識別子を持つ別のインスタンスがロードされている場合、このエラーが発生します。 これを修正するために、マージメソッドを使用して、分離されたオブジェクトを再アタッチできます。

6.2. StaleStateException

バージョン番号またはタイムスタンプのチェックが失敗すると、HibernateはStaleStateExceptionをスローします。 これは、セッションに古いデータが含まれていることを示しています。

これがOptimisticLockExceptionにラップされることがあります。

このエラーは通常、バージョン管理で長時間実行されるトランザクションを使用しているときに発生します。

さらに、対応するデータベース行が存在しない場合、エンティティを更新または削除しようとしているときにも発生する可能性があります。

public void whenUpdatingNonExistingObject_thenStaleStateException() {
    thrown.expect(isA(OptimisticLockException.class));
    thrown.expectMessage(
      "Batch update returned unexpected row count from update");
    thrown.expectCause(isA(StaleStateException.class));

    Session session = null;
    Transaction transaction = null;

    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(15);
        product.setName("Product1");
        session.update(product);
        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

その他の考えられるシナリオは次のとおりです。

  • エンティティに適切な未保存価値戦略を指定しませんでした
  • 2人のユーザーがほぼ同時に同じ行を削除しようとしました
  • 自動生成されたIDまたはバージョンフィールドに手動で値を設定します

7. 遅延初期化エラー

通常、アプリケーションのパフォーマンスを向上させるために、関連付けが遅延ロードされるように構成します。 アソシエーションは、最初に使用されたときにのみフェッチされます。

ただし、Hibernateはデータをフェッチするためにアクティブなセッションを必要とします。 初期化されていないアソシエーションにアクセスしようとしたときにセッションがすでに閉じられている場合は、例外が発生します。

この例外とそれを修正するためのさまざまな方法を調べてみましょう。

7.1. LazyInitializationException

LazyInitializationExceptionは、アクティブなセッションの外部で初期化されていないデータを読み込もうとしたことを示します。多くのシナリオでこのエラーが発生する可能性があります。

まず、プレゼンテーション層の怠惰な関係にアクセスしているときに、この例外が発生する可能性があります。 その理由は、エンティティがビジネスレイヤーに部分的にロードされ、セッションが閉じられたためです。

次に、 getOne メソッドを使用すると、 SpringDataでこのエラーが発生する可能性があります。 このメソッドは、インスタンスを遅延フェッチします。

この例外を解決する方法はたくさんあります。

まず第一に、私たちはすべての関係を熱心にロードすることができます。 ただし、使用されないデータをロードするため、これはアプリケーションのパフォーマンスに影響を与えます。

次に、ビューがレンダリングされるまでセッションを開いたままにすることができます。 これは「OpenSessionin View 」と呼ばれ、アンチパターンです。 いくつかの欠点があるため、これは避ける必要があります。

第三に、関係を取得するために、別のセッションを開いてエンティティを再接続できます。 これを行うには、セッションでmergeメソッドを使用します。

最後に、ビジネスレイヤーで必要な関連付けを初期化できます。 これについては、次のセクションで説明します。

7.2. ビジネスレイヤーでの関連する怠惰な関係の初期化

怠惰な関係を初期化する方法はたくさんあります。

1つのオプションは、エンティティの対応するメソッドを呼び出してそれらを初期化することです。 この場合、Hibernateは複数のデータベースクエリを発行し、パフォーマンスを低下させます。 これを「N+1SELECT」問題と呼びます。

次に、 Fetch Join を使用して、単一のクエリでデータを取得できます。 ただし、これを実現するにはカスタムコードを作成する必要があります。

最後に、エンティティグラフを使用して、フェッチするすべての属性を定義できます。 アノテーション@NamedEntityGraph、@ NamedAttributeNode 、および @NamedEntitySubgraph を使用して、エンティティグラフを宣言的に定義できます。 JPAAPIを使用してプログラムで定義することもできます。 次に、フェッチ操作で指定することにより、1回の呼び出しでグラフ全体を取得します。

8. トランザクションの問題

Transactions は、作業単位と同時アクティビティ間の分離を定義します。 それらを2つの異なる方法で区別することができます。 まず、アノテーションを使用して宣言的に定義できます。 次に、 Hibernate TransactionAPIを使用してプログラムでそれらを管理できます。

さらに、Hibernateはトランザクション管理をトランザクションマネージャーに委任します。 何らかの理由でトランザクションを開始、コミット、またはロールバックできなかった場合、Hibernateは例外をスローします。

通常、トランザクションマネージャに応じて、TransactionExceptionまたはIllegalArgumentExceptionが発生します。

例として、ロールバックのマークが付けられたトランザクションをコミットしてみましょう。

public void 
givenTxnMarkedRollbackOnly_whenCommitted_thenTransactionException() {
    thrown.expect(isA(TransactionException.class));
    thrown.expectMessage(
        "Transaction was marked for rollback only; cannot commit");

    Session session = null;
    Transaction transaction = null;
    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(15);
        product.setName("Product1");
        session.save(product);
        transaction.setRollbackOnly();

        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

同様に、他のエラーも例外を引き起こす可能性があります。

  • 宣言型トランザクションとプログラム型トランザクションの混合
  • セッションで別のトランザクションがすでにアクティブになっているときにトランザクションを開始しようとしています
  • トランザクションを開始せずにコミットまたはロールバックしようとしています
  • トランザクションを複数回コミットまたはロールバックしようとしています

9. 並行性の問題

Hibernateは2つのlocking戦略をサポートして、同時トランザクションによるデータベースの不整合を防ぎます– optimisticpessimistic。 どちらも、ロックの競合が発生した場合に例外を発生させます。

高い同時実行性と高いスケーラビリティをサポートするために、通常、バージョンチェックで楽観的同時実行制御を使用します。 これは、バージョン番号またはタイムスタンプを使用して、競合する更新を検出します。

OptimisticLockingExceptionがスローされ、楽観的なロックの競合が示されます。 たとえば、最初の操作後に同じエンティティを更新せずに2つの更新または削除を実行すると、このエラーが発生します。

public void whenDeletingADeletedObject_thenOptimisticLockException() {
    thrown.expect(isA(OptimisticLockException.class));
    thrown.expectMessage(
        "Batch update returned unexpected row count from update");
    thrown.expectCause(isA(StaleStateException.class));

    Session session = null;
    Transaction transaction = null;

    try {
        session = sessionFactory.openSession();
        transaction = session.beginTransaction();

        Product product = new Product();
        product.setId(12);
        product.setName("Product 12");
        session.save(product1);
        transaction.commit();
        session.close();

        session = sessionFactory.openSession();
        transaction = session.beginTransaction();
        product = session.get(Product.class, 12);
        session.createNativeQuery("delete from Product where id=12")
          .executeUpdate();
        // We need to refresh to fix the error.
        // session.refresh(product);
        session.delete(product);
        transaction.commit();
    } catch (Exception e) {
        rollbackTransactionQuietly(transaction);
        throw (e);
    } finally {
        closeSessionQuietly(session);
    }
}

同様に、2人のユーザーがほぼ同時に同じエンティティを更新しようとすると、このエラーが発生する可能性があります。 この場合、最初のエラーが成功し、2番目のエラーが発生します。

したがって、悲観的なロックを導入せずに、このエラーを完全に回避することはできません。 ただし、次のようにすることで、その発生の可能性を最小限に抑えることができます。

  • 更新操作をできるだけ短くします
  • クライアントのエンティティ表現をできるだけ頻繁に更新します
  • エンティティまたはそれを表す値オブジェクトをキャッシュしないでください
  • 更新後は常にクライアントのエンティティ表現を更新してください

10. 結論

この記事では、Hibernateの使用中に発生するいくつかの一般的な例外について説明しました。 さらに、考えられる原因と解決策を調査しました。

いつものように、完全なソースコードはGitHubにあります。