1. 序章

このクイックチュートリアルでは、Hibernateによって提供されるクエリプランキャッシュとそのパフォーマンスへの影響について説明します。

2. クエリプランキャッシュ

すべてのJPQLクエリまたはCriteriaクエリは、実行前に抽象構文ツリー(AST)に解析されるため、HibernateはSQLステートメントを生成できます。 クエリのコンパイルには時間がかかるため、Hibernateはパフォーマンスを向上させるためにQueryPlanCacheを提供します。

ネイティブクエリの場合、Hibernateは名前付きパラメーターとクエリリターンタイプに関する情報を抽出し、それをParameterMetadataに格納します。

実行のたびに、Hibernateは最初にプランキャッシュをチェックし、使用可能なプランがない場合にのみ、新しいプランを生成し、将来の参照のために実行プランをキャッシュに保存します。

3. 構成

クエリプランのキャッシュ構成は、次のプロパティによって制御されます。

  • hibernate.query.plan_cache_max_size –プランキャッシュ内のエントリの最大数を制御します(デフォルトは2048)
  • hibernate.query.plan_parameter_metadata_max_size –キャッシュ内の ParameterMetadata インスタンスの数を管理します(デフォルトは128)

したがって、アプリケーションがクエリプランキャッシュのサイズよりも多くのクエリを実行する場合、Hibernateはクエリのコンパイルに余分な時間を費やす必要があります。 したがって、全体的なクエリ実行時間は増加します。

4. テストケースの設定

ことわざが業界で起こっているように、パフォーマンスに関しては、主張を決して信用しないでください。 そこで、キャッシュ設定を変更すると、クエリのコンパイル時間がどのように変化するかをテストしましょう

4.1. テストに関与するエンティティクラス

まず、この例で使用するエンティティDeptEmployeeDepartmentを見てみましょう。

@Entity
public class DeptEmployee {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String employeeNumber;

    private String title;

    private String name;

    @ManyToOne
    private Department department;

   // standard getters and setters
}
@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

    @OneToMany(mappedBy="department")
    private List<DeptEmployee> employees;

    // standard getters and setters
}

4.2. テストに関与するHibernateクエリ

全体的なクエリコンパイル時間のみを測定することに関心があるため、テストには有効なHQLクエリの任意の組み合わせを選択できます。

この記事では、次の3つのクエリを使用します。

  •  findEmployeesByDepartmentName
session.createQuery("SELECT e FROM DeptEmployee e " +
  "JOIN e.department WHERE e.department.name = :deptName")
  .setMaxResults(30)
  .setHint(QueryHints.HINT_FETCH_SIZE, 30);
  • findEmployeesByDesignation
session.createQuery("SELECT e FROM DeptEmployee e " +
  "WHERE e.title = :designation")
  .setHint(QueryHints.SPEC_HINT_TIMEOUT, 1000);
  • findDepartmentOfAnEmployee
session.createQuery("SELECT e.department FROM DeptEmployee e " +
  "JOIN e.department WHERE e.employeeNumber = :empId");

5. パフォーマンスへの影響の測定

5.1. ベンチマークコードの設定

キャッシュサイズを1から3に変更します–その後、3つのクエリすべてがすでにキャッシュにあります。 したがって、それをさらに増やす意味はありません。

@State(Scope.Thread)
public static class QueryPlanCacheBenchMarkState {
    @Param({"1", "2", "3"})
    public int planCacheSize;
    
    public Session session;

    @Setup
    public void stateSetup() throws IOException {
       session = initSession(planCacheSize);
    }

    private Session initSession(int planCacheSize) throws IOException {
        Properties properties = HibernateUtil.getProperties();
        properties.put("hibernate.query.plan_cache_max_size", planCacheSize);
        properties.put("hibernate.query.plan_parameter_metadata_max_size", planCacheSize);
        SessionFactory sessionFactory = HibernateUtil.getSessionFactoryByProperties(properties);
        return sessionFactory.openSession();
    }
    //teardown...
}

5.2. テスト中のコード

次に、クエリのコンパイル中にHibernateがかかる平均時間を測定するために使用されるベンチマークコードを見てみましょう。

@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 5)
public void givenQueryPlanCacheSize_thenCompileQueries(
  QueryPlanCacheBenchMarkState state, Blackhole blackhole) {

    Query query1 = findEmployeesByDepartmentNameQuery(state.session);
    Query query2 = findEmployeesByDesignationQuery(state.session);
    Query query3 = findDepartmentOfAnEmployeeQuery(state.session);

    blackhole.consume(query1);
    blackhole.consume(query2);
    blackhole.consume(query3);
}

JMHを使用してベンチマークを作成したことに注意してください。

5.3. ベンチマーク結果

次に、上記のベンチマークを実行して作成したコンパイル時間とキャッシュサイズのグラフを視覚化します。

グラフから明らかなように、 Hibernateがキャッシュできるクエリの数を増やすと、結果的にコンパイル時間が短縮されます

キャッシュサイズが1の場合、平均コンパイル時間は709マイクロ秒で最も長く、次にキャッシュサイズが2の場合は409マイクロ秒に、キャッシュサイズが3の場合は0.637マイクロ秒に減少します。

6. Hibernate統計の使用

クエリプランキャッシュの有効性を監視するために、HibernateはStatisticsインターフェースを介して次のメソッドを公開します。

  • getQueryPlanCacheHitCount
  • getQueryPlanCacheMissCount

したがって、ヒットカウントが高く、ミスカウントが低い場合、ほとんどのクエリは、何度もコンパイルされるのではなく、キャッシュ自体から提供されます。

7. 結論

この記事では、クエリプランキャッシュがHibernateにあることと、それがアプリケーションの全体的なパフォーマンスにどのように貢献できるかを学びました。 全体として、アプリケーションで実行されているクエリの数に応じて、クエリプランのキャッシュサイズを維持するように努める必要があります。

いつものように、このチュートリアルのソースコードはGitHubからで入手できます。