Hibernate 6によるPostgres JSON関数

これは、前回の記事の続きであり、Postgres JSON関数のサポートを追加し、Hibernate 5を使用する方法が説明されていました。本記事では、Hibernateフレームワークをバージョン6で使用するプロジェクトでJSON操作を使用する方法に焦点を当てます。

ネイティブサポート

Hibernate 6は、以下の例で示すように、JSON属性によるクエリに対して既に良いサポートを持っています。

通常のエンティティクラスがあり、そのクラスには1つのJSONプロパティがあります。

Java

 

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.Type;
import org.hibernate.type.SqlTypes;

import java.io.Serializable;


@Entity
@Table(name = "item")
public class Item implements Serializable {

    @Id
    private Long id;

    @JdbcTypeCode(SqlTypes.JSON)
    @Column(name = "jsonb_content")
    private JsonbContent jsonbContent;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public JsonbContent getJsonbContent() {
        return jsonbContent;
    }

    public void setJsonbContent(JsonbContent jsonbContent) {
        this.jsonbContent = jsonbContent;
    }
}

以下のようにJsonbContent型があります。

Java

 

import jakarta.persistence.Embeddable;
import jakarta.persistence.Enumerated;
import jakarta.persistence.EnumType;
import org.hibernate.annotations.Struct;

import java.io.Serializable;
import java.util.List;

@Embeddable
public class JsonbContent implements Serializable{

    private Integer integer_value;
    private Double double_value;
    @Enumerated(EnumType.STRING)
    private UserTypeEnum enum_value;
    private String string_value;
	//ゲッターズとセッターズ
}

このようなモデルがある場合、たとえばstring_value属性でクエリを実行できます。

Java

 

    public List<Item> findAllByStringValueAndLikeOperatorWithHQLQuery(String expression) {
        TypedQuery<Item> query = entityManager.createQuery("from Item as item_ where item_.jsonbContent.string_value like :expr", Item.class);
		query.setParameter("expr", expression);
        return query.getResultList();
    }

重要!-現在、属性によるクエリのサポートには制限があるようで、配列のような複雑な型でクエリを実行できません。ご覧の通り、JsonbContent型にはEmbeddableアノテーションがあり、リストのようなプロパティを追加しようとすると、以下のメッセージを含む例外が表示される可能性があります:JSONとしてシリアル化される予定の型は、そのプロパティとして複雑な型を持つことができません:集約コンポーネントは現在、単純な基本的な値とそのような値のコンポーネントのみを含むことができます。

ケースによっては、JSONタイプが複雑な型のプロパティを必要としない場合、ネイティブサポートで十分です。

詳細情報については以下のリンクをご確認ください:

しかし、時には配列属性でクエリを実行できる可能性があることが価値があります。もちろん、HibernateでネイティブSQLクエリを使用し、前の記事で説明したPostgresのJSON関数を使用することもできます。しかし、HQLクエリやプログラムによる述語の使用時にもその可能性があることは便利です。この2つ目のアプローチは、動的クエリの機能を実装する予定の場合に特に有用です。動的に連結された文字列がHQLクエリであるとされるのは簡単かもしれませんが、より良い方法は実装された述語を使用することです。これはposjsonhelperライブラリを使用するのに便利な場面です。

Posjsonhelper

プロジェクトはMavenセントラルリポジトリに存在しているため、Mavenプロジェクトの依存関係として簡単に追加できます。

XML

 

<dependency>
            <groupId>com.github.starnowski.posjsonhelper</groupId>
            <artifactId>hibernate6</artifactId>
            <version>0.2.1</version>
</dependency>

Register FunctionContributor

ライブラリを使用するためには、FunctionContributorコンポーネントをアタッチする必要があります。これは2つの方法で行うことができます。最も推奨される方法は、org.hibernate.boot.model.FunctionContributorという名前のファイルをresources/META-INF/servicesディレクトリに作成することです。

ファイルの内容は、posjsonhelperというorg.hibernate.boot.model.FunctionContributor型の実装を記述するだけです。

Plain Text

 

com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor

代替ソリューションは、Spring Frameworkを使用した以下の例のように、アプリケーションの起動時にcom.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricherコンポーネントを使用することです。

Java

 

import com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher;
import jakarta.persistence.EntityManager;
import org.hibernate.query.sqm.NodeBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;

@Configuration
public class FunctionDescriptorConfiguration implements
        ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        NodeBuilder nodeBuilder = (NodeBuilder) entityManager.getCriteriaBuilder();
        SqmFunctionRegistryEnricher sqmFunctionRegistryEnricher = new SqmFunctionRegistryEnricher();
        sqmFunctionRegistryEnricher.enrich(nodeBuilder.getQueryEngine().getSqmFunctionRegistry());
    }
}

詳細については、”FunctionContributorをアタッチする方法“をご覧ください。

Example Model

私たちのモデルは以下の例のようになっています。

Java

 

package com.github.starnowski.posjsonhelper.hibernate6.demo.model;

import io.hypersistence.utils.hibernate.type.json.JsonType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.annotations.Type;
import org.hibernate.type.SqlTypes;


@Entity
@Table(name = "item")
public class Item {

    @Id
    private Long id;

    @JdbcTypeCode(SqlTypes.JSON)
    @Type(JsonType.class)
    @Column(name = "jsonb_content", columnDefinition = "jsonb")
    private JsonbContent jsonbContent;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public JsonbContent getJsonbContent() {
        return jsonbContent;
    }

    public void setJsonbContent(JsonbContent jsonbContent) {
        this.jsonbContent = jsonbContent;
    }
}

重要!: この例では、JsonbConentプロパティはカスタム型(以下参照)ですが、String型でも構いません。

Java

 

package com.github.starnowski.posjsonhelper.hibernate6.demo.model;

import jakarta.persistence.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

import java.io.Serializable;
import java.util.List;
public class JsonbContent implements Serializable{

    private List top_element_with_set_of_values;
    private Integer integer_value;
    private Double double_value;
    @Enumerated(EnumType.STRING)
    private UserTypeEnum enum_value;
    private String string_value;
    private Child child;

// セッターとゲッター
}

テーブルのDDL操作:

SQL

 

create table item (
        id bigint not null,
        jsonb_content jsonb,
        primary key (id)
    )

表示のために、データベースには以下のようなレコードが含まれていると仮定しましょう:

SQL

 


INSERT INTO item (id, jsonb_content) VALUES (1, '{"top_element_with_set_of_values":["TAG1","TAG2","TAG11","TAG12","TAG21","TAG22"]}');
INSERT INTO item (id, jsonb_content) VALUES (2, '{"top_element_with_set_of_values":["TAG3"]}');
INSERT INTO item (id, jsonb_content) VALUES (3, '{"top_element_with_set_of_values":["TAG1","TAG3"]}');
INSERT INTO item (id, jsonb_content) VALUES (4, '{"top_element_with_set_of_values":["TAG22","TAG21"]}');
INSERT INTO item (id, jsonb_content) VALUES (5, '{"top_element_with_set_of_values":["TAG31","TAG32"]}');

-- プロパティのないアイテム、単に空のjson
INSERT INTO item (id, jsonb_content) VALUES (6, '{}');

-- int値
INSERT INTO item (id, jsonb_content) VALUES (7, '{"integer_value": 132}');
INSERT INTO item (id, jsonb_content) VALUES (8, '{"integer_value": 562}');
INSERT INTO item (id, jsonb_content) VALUES (9, '{"integer_value": 1322}');

-- double値
INSERT INTO item (id, jsonb_content) VALUES (10, '{"double_value": 353.01}');
INSERT INTO item (id, jsonb_content) VALUES (11, '{"double_value": -1137.98}');
INSERT INTO item (id, jsonb_content) VALUES (12, '{"double_value": 20490.04}');

-- enum値
INSERT INTO item (id, jsonb_content) VALUES (13, '{"enum_value": "SUPER"}');
INSERT INTO item (id, jsonb_content) VALUES (14, '{"enum_value": "USER"}');
INSERT INTO item (id, jsonb_content) VALUES (15, '{"enum_value": "ANONYMOUS"}');

-- 文字列値
INSERT INTO item (id, jsonb_content) VALUES (16, '{"string_value": "this is full sentence"}');
INSERT INTO item (id, jsonb_content) VALUES (17, '{"string_value": "this is part of sentence"}');
INSERT INTO item (id, jsonb_content) VALUES (18, '{"string_value": "the end of records"}');

-- 内部要素
INSERT INTO item (id, jsonb_content) VALUES (19, '{"child": {"pets" : ["dog"]}}');
INSERT INTO item (id, jsonb_content) VALUES (20, '{"child": {"pets" : ["cat"]}}');
INSERT INTO item (id, jsonb_content) VALUES (21, '{"child": {"pets" : ["dog", "cat"]}}');
INSERT INTO item (id, jsonb_content) VALUES (22, '{"child": {"pets" : ["hamster"]}}');

Criteria Componentsの使用

以下は、最初に提示された同じクエリの例ですが、SQMコンポーネントとクライテリアビルダーで作成されています:

Java

 

public List<Item> findAllByStringValueAndLikeOperator(String expression) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Item> query = cb.createQuery(Item.class);
        Root<Item> root = query.from(Item.class);
        query.select(root);
        query.where(cb.like(new JsonBExtractPathText(root.get("jsonbContent"), singletonList("string_value"), (NodeBuilder) cb), expression));
        return entityManager.createQuery(query).getResultList();
    }

Hibernateは以下のSQLコードを生成する予定です:

SQL

 

select
        i1_0.id,
        i1_0.jsonb_content 
    from
        item i1_0 
    where
        jsonb_extract_path_text(i1_0.jsonb_content,?) like ? escape ''

jsonb_extract_path_textは、#>>演算子に相当するPostgresの関数です(詳細については、以前リンクされたPostgresのドキュメントを確認してください)。

配列上の操作

このライブラリは、いくつかのPostgres JSON関数演算子をサポートしています。例:

  • ?& – これは、テキスト配列内のすべての文字列がトップレベルのキーまたは配列要素として存在するかどうかをチェックします。つまり、JSONプロパティが配列を含んでいる場合、検索対象のすべての要素を含んでいるかどうかを確認できます。
  • ?| – これは、テキスト配列内の文字列のいずれかがトップレベルのキーまたは配列要素として存在するかどうかをチェックします。つまり、JSONプロパティが配列を含んでいる場合、検索対象の少なくとも1つの要素を含んでいるかどうかを確認できます。

ネイティブSQLクエリの実行に加えて、Hibernate 6は上記の操作をサポートしていません。

必要なDDL変更

上記の演算子はHQLで使用できないため、特殊文字があるためです。そのため、これらをカスタムSQL関数でラップする必要があります。Posjsonhelperライブラリは、これらの演算子をラップする2つのカスタムSQL関数を必要とします。デフォルト設定では、これらの関数の実装は以下の通りです。

SQL

 

CREATE OR REPLACE FUNCTION jsonb_all_array_strings_exist(jsonb, text[]) RETURNS boolean AS $$
SELECT $1 ?& $2;
$$ LANGUAGE SQL;

CREATE OR REPLACE FUNCTION jsonb_any_array_strings_exist(jsonb, text[]) RETURNS boolean AS $$
SELECT $1 ?| $2;
$$ LANGUAGE SQL;

DDLをカスタマイズしたり、プログラムで追加したりする方法についてさらに詳しくは、「DDL変更の適用」セクションを参照してください。

“?&”ラッパー

以下のコード例は、JSONプロパティに配列が含まれ、検索に使用しているすべての文字列要素を持つレコードを調べるクエリを作成する方法を示しています。

Java

 

public List<Item> findAllByAllMatchingTags(Set<String> tags) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Item> query = cb.createQuery(Item.class);
        Root<Item> root = query.from(Item.class);
        query.select(root);
        query.where(new JsonbAllArrayStringsExistPredicate(hibernateContext, (NodeBuilder) cb, new JsonBExtractPath(root.get("jsonbContent"), (NodeBuilder) cb, singletonList("top_element_with_set_of_values")), tags.toArray(new String[0])));
        return entityManager.createQuery(query).getResultList();
    }

タグに2つの要素が含まれている場合、Hibernateは以下のSQLを生成します。

SQL

 

select
        i1_0.id,
        i1_0.jsonb_content 
    from
        item i1_0 
    where
        jsonb_all_array_strings_exist(jsonb_extract_path(i1_0.jsonb_content,?),array[?,?])

“?|”ラッパー

以下の例のコードは、JSONプロパティに配列が含まれ、検索に使用している少なくとも1つの文字列要素を持つレコードを調べるクエリを作成する方法を示しています。

Java

 

public List<Item> findAllByAnyMatchingTags(HashSet<String> tags) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Item> query = cb.createQuery(Item.class);
        Root<Item> root = query.from(Item.class);
        query.select(root);
        query.where(new JsonbAnyArrayStringsExistPredicate(hibernateContext, (NodeBuilder) cb, new JsonBExtractPath(root.get("jsonbContent"), (NodeBuilder) cb, singletonList("top_element_with_set_of_values")), tags.toArray(new String[0])));
        return entityManager.createQuery(query).getResultList();
    }

タグに2つの要素が含まれている場合、Hibernateは以下のSQLを生成します。

SQL

 

select
        i1_0.id,
        i1_0.jsonb_content 
    from
        item i1_0 
    where
        jsonb_any_array_strings_exist(jsonb_extract_path(i1_0.jsonb_content,?),array[?,?])

数値演算子の使用方法の詳細な例については、デモdao objectおよびdao testsを参照してください。

HibernateはJSON属性クエリに対していくつかのサポートを提供しているのに、なぜposjsonhelperライブラリを使用するのか

上記で述べた配列型をサポートする2つの演算子に加えて、このライブラリには2つの追加の便利な演算子があります。jsonb_extract_pathjsonb_extract_path_textは、#>および#>>演算子のラッパーです。Hibernateは->>演算子をサポートしています。これらの演算子の違いを確認するには、先にリンクされたPostgresのドキュメントを参照してください。

しかし、記事の冒頭で読んだように、JSON属性のネイティブクエリサポートは、JSONクラスが単純な型のプロパティを持つ場合にのみ許可されます。さらに重要なことに、JSONタイプでプロパティにマッピングされていない属性でクエリを実行することはできません。JSON構造がより動的であり、スキーマによって定義された柔軟な構造を持つと仮定する場合、それは問題になる可能性があります。

この問題は、posjsonhelper演算子を使用することで解決できます。どの属性でもクエリを実行できます。JSONタイプでプロパティとして定義する必要はありません。さらに、JSON列を保存するエンティティ内のプロパティは、例のJsonbConentのような複雑なオブジェクトである必要もありません。Javaでは単純な文字列であってもかまいません。

結論

前の記事で述べたように、場合によっては、PostgresのJSON型と関数はNoSQLデータベースの良い代替手段になり得ます。これにより、NoSQLソリューションを技術スタックに追加する決定から私たちを救い、さらに複雑さと追加コストを増やすことを回避できます。

また、リレーショナルベースに非構造化データを保存する必要がある場合や、それらの構造でクエリを実行する可能性がある場合に、柔軟性も提供します。

Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-6