PostgresデータベースはいくつかのJSON型とそれらの型に対する特殊な操作をサポートしています。
場合によっては、これらの操作はMongoDBのようなドキュメントデータベースや他のNoSQLデータベースの良い代替となることがあります。もちろん、MongoDBのようなデータベースはより優れたレプリケーションプロセスを持っているかもしれませんが、このトピックはこの記事の範囲外です。
この記事では、Hibernateフレームワークのバージョン5を使用するプロジェクトでJSON操作を使用する方法に焦点を当てます。
例: モデル
私たちのモデルは以下の例のようになります:
@Entity
@Table(name = "item")
public class Item {
@Id
private Long id;
@Column(name = "jsonb_content", columnDefinition = "jsonb")
private String jsonbContent;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getJsonbContent() {
return jsonbContent;
}
public void setJsonbContent(String jsonbContent) {
this.jsonbContent = jsonbContent;
}
}
重要!:特定のJSON型をjsonbContent
プロパティに使用することもできますが、Hibernateバージョン5では、操作の観点からはそれ以上の利点はありません。
DDL操作:
create table item (
id int8 not null,
jsonb_content jsonb,
primary key (id)
)
表示目的のため、以下のようなレコードがデータベースに含まれていると仮定しましょう:
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, '{}');
-- 整数値
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}');
-- 倍精度値
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}');
-- 列挙値
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"]}}');
ネイティブクエリアプローチ
Hibernate 5では、ネイティブアプローチを使用して直接SQLコマンドを実行できます。
重要!: プレゼンテーションの目的のため、以下のコードがLIKE
演算子に対してSQLインジェクションを許可していることを省略してください。もちろん、そのような操作の場合、パラメーターとPreparedStatement
を使用すべきです。
private EntityManager entityManager;
public List<Item> findAllByStringValueAndLikeOperatorWithNativeQuery(String expression) {
return entityManager.createNativeQuery("SELECT * FROM item i WHERE i.jsonb_content#>>'{string_value}' LIKE '" + expression + "'", Item.class).getResultList();
}
上記の例では、指定されたパスのJSONサブオブジェクトをテキストとして抽出する#>>
演算子の使用があります(詳細についてはPostgresドキュメントを参照してください)。
ほとんどの場合、そのようなクエリ(もちろんエスケープされた値を含む)で十分です。ただし、APIで渡されたパラメータに基づいて動的なクエリを作成する必要がある場合、ある種の基準ビルダーを使用する方が良いでしょう。
Posjsonhelper
Hibernate 5はデフォルトでPostgres JSON関数をサポートしていません。幸いなことに、自分で実装することもできますし、posjsonhelper ライブラリを使用することもできます。これはオープンソースプロジェクトです。
プロジェクトはMaven中央リポジトリに存在しており、Mavenプロジェクトに依存関係として簡単に追加できます。
<dependency>
<groupId>com.github.starnowski.posjsonhelper</groupId>
<artifactId>hibernate5</artifactId>
<version>0.1.0</version>
</dependency>
プロジェクトでposjsonhelper
ライブラリを使用するには、プロジェクトで実装されているPostgresダイアレクトを使用する必要があります。例:
com.github.starnowski.posjsonhelper.hibernate5.dialects.PostgreSQL95DialectWrapper ...
プロジェクトに既にカスタムダイアレクトクラスがある場合、次のように使用する可能性もあります:
com.github.starnowski.posjsonhelper.hibernate5.PostgreSQLDialectEnricher;
Criteria Componentsの使用
以下の例は、ネイティブクエリを使用した前の例と同様の動作をします。しかし、この場合、criteria builderを使用します。
private EntityManager entityManager;
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((CriteriaBuilderImpl) cb, singletonList("string_value"), root.get("jsonbContent")), expression));
return entityManager.createQuery(query).getResultList();
}
Hibernateは以下のSQLコードを生成します:
select
item0_.id as id1_0_,
item0_.jsonb_content as jsonb_co2_0_
from
item item0_
where
jsonb_extract_path_text(item0_.jsonb_content,?) like ?
jsonb_extract_path_text
は、#>>
演算子と同等のPostgres関数です(詳細については、以前にリンクされたPostgresドキュメントを確認してください)。
配列上の操作
ライブラリは、次のようないくつかのPostgres JSON関数演算子をサポートしています:
?&
– テキスト配列内のすべての文字列がトップレベルのキーまたは配列要素として存在するかどうかを確認します。したがって、一般的にJSONプロパティが配列を含む場合、検索しているすべての要素を含むかどうかを確認できます。?|
– テキスト配列内の文字列のいずれかがトップレベルのキーまたは配列要素として存在するかどうかを確認します。したがって、一般的にJSONプロパティが配列を含む場合、検索している要素の少なくとも1つを含むかどうかを確認できます。
必要なDDL変更
上記の演算子は特殊文字のためHQLで使用できません。そのため、これらをカスタムSQL関数でラップする必要があります。Posjsonhelper
ライブラリは、これらの演算子をラップする2つのカスタム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プロパティに含まれる配列の全ての文字列要素を検索するレコードを調べるクエリを作成する方法を示しています。
private EntityManager entityManager;
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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
タグが2つの要素を含む場合、Hibernateは以下のSQLを生成します。
select
item0_.id as id1_0_,
item0_.jsonb_content as jsonb_co2_0_
from
item item0_
where
jsonb_all_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true
“?|”ラッパー
以下のコード例は、JSONプロパティに含まれる配列の少なくとも1つの文字列要素を検索するレコードを調べるクエリを作成する方法を示しています。
private EntityManager entityManager;
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, (CriteriaBuilderImpl) cb, new JsonBExtractPath((CriteriaBuilderImpl) cb, singletonList("top_element_with_set_of_values"), root.get("jsonbContent")), tags.toArray(new String[0])));
return entityManager.createQuery(query).getResultList();
}
タグが2つの要素を含む場合、Hibernateは以下のSQLを生成します。
select
item0_.id as id1_0_,
item0_.jsonb_content as jsonb_co2_0_
from
item item0_
where
jsonb_any_array_strings_exist(jsonb_extract_path(item0_.jsonb_content,?), array[?,?])=true
数値演算子の使用方法のさらなる例については、デモdao objectおよびdao testsを確認してください。
結論
場合によっては、PostgresのJSON型や関数がNoSQLデータベースの良い代替案になることがあります。これにより、NoSQLソリューションを技術スタックに追加する決定から私たちを救うことができ、それはさらに複雑さと追加コストをもたらす可能性があります。
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-5