Hibernate 6와 함께 사용하는 Postgres JSON 함수

이것은 이전 기사의 연장으로, Postgres JSON 함수에 대한 지원을 추가하고 Hibernate 5를 사용하는 방법에 대해 설명되었습니다. 이 기사에서는 Hibernate 프레임워크를 사용하는 프로젝트에서 JSON 작업을 사용하는 방법에 중점을 둘 것입니다. 버전 6.

기본 지원

Hibernate 6은 아래 예제에서 보여주듯이 JSON 속성으로 쿼리하는 데 뛰어난 지원을 제공합니다.

우리는 하나의 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;
	//Getter와 Setter
}

이러한 모델을 가지고 있을 때 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으로 직렬화되어야 할 타입은 복잡한 유형을 속성으로 가질 수 없습니다: 현재 Aggregate components는 단순 기본 값과 단순 기본 값의 구성요소만 포함할 수 있습니다.

JSON 타입이 복잡한 타입의 속성을 필요로 하지 않는 경우에는 기본 지원이 충분합니다.

자세한 내용은 아래 링크를 확인하세요:

그러나 때로는 배열 속성으로 쿼리하는 기능이 있는 것이 좋습니다. 물론 Hibernate에서 기본 SQL 쿼리를 사용하고 이전 글에서 설명한 Postgres JSON 함수를 사용할 수 있습니다. 그러나 HQL 쿼리에서 또는 프로그래밍 방식 조건자를 사용할 때 이러한 기능이 유용합니다. 이 두 번째 접근 방식은 동적 쿼리 기능을 구현해야 하는 경우에 특히 더 유용합니다. HQL 쿼리로 사용될 문자열을 동적으로 연결하는 것은 쉬울 수 있지만 더 나은 방법은 구현된 조건자를 사용하는 것입니다. 이때 posjsonhelper 라이브러리를 사용하면 편리합니다.

Posjsonhelper

이 프로젝트는 Maven 중앙 저장소에 존재하므로, Maven 프로젝트에 의존성으로 추가하기만 하면 쉽게 사용할 수 있습니다.

XML

 

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

FunctionContributor 등록

라이브러리를 사용하려면 FunctionContributor 컴포넌트를 연결해야 합니다. 이를 위해 두 가지 방법을 사용할 수 있습니다. 가장 권장되는 방법은 org.hibernate.boot.model.FunctionContributor라는 이름의 파일을 resources/META-INF/services 디렉토리 아래에 생성하는 것입니다.

파일의 내용은 posjsonhelper라는 이름의 org.hibernate.boot.model.FunctionContributor 유형의 구현체를 입력하면 됩니다.

Plain Text

 

com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor

대안적인 방법은 애플리케이션 시작 시 com.github.starnowski.posjsonhelper.hibernate6.SqmFunctionRegistryEnricher 컴포넌트를 사용하는 것으로, 아래 Spring Framework를 사용한 예시와 같습니다.

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를 연결하는 방법“을 확인하세요.

예시 모델

우리의 모델은 아래 예시와 같습니다:

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 속성은 아래와 같은 사용자 정의 유형이지만, 문자열 유형일 수도 있습니다.

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, '{}');

-- 정수 값
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"]}}');

Criteria 구성 요소 사용

아래는 처음에 제시된 동일한 쿼리의 예입니다. 하지만 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 속성이 배열을 포함하는 경우, 찾고자 하는 요소 중 최소 한 개를 포함하는지 확인할 수 있습니다.

네이티브 SQL 쿼리 실행 외에도, Hibernate 6은 위의 작업을 지원하지 않습니다.

필요한 DDL 변경

위의 연산자는 HQL에서 특수 문자 때문에 사용할 수 없습니다. 그렇기 때문에 이들을 감싸야 하며, 예를 들어 사용자 정의 SQL 함수로 감싸야 합니다. Posjsonhelper 라이브러리는 이러한 연산자를 감싸는 두 개의 사용자 정의 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();
    }

태그에 두 요소가 포함된 경우, 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 속성이 배열을 포함하고 있으며, 이를 검색하는 데 사용하는 문자열 요소를 적어도 하나 포함하고 있는 레코드를 조회하는 쿼리를 생성하는 방법을 보여줍니다.

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();
    }

태그에 두 요소가 포함된 경우, 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 객체dao 테스트를 확인하십시오.

Hibernate가 JSON 속성 쿼리에 대한 일부 지원이 있음에도 불구하고 posjsonhelper 라이브러리를 사용하는 이유

위에서 언급한 배열 타입을 지원하는 두 가지 연산자 외에도 라이브러리에는 두 가지 추가적으로 유용한 연산자가 있습니다. jsonb_extract_pathjsonb_extract_path_text#>#>> 연산자를 감싸는 래퍼입니다. Hibernate는 ->> 연산자를 지원합니다. 이러한 연산자 간의 차이점을 보려면 앞서 링크된 Postgres 문서를 확인하십시오.

그러나 이 글의 시작 부분에서 읽은 것처럼, JSON 속성에 대한 네이티브 쿼리 지원은 JSON 클래스가 단순 타입의 속성을 가질 때만 허용됩니다. 더 중요한 것은, JSON 타입에서 속성이 속성으로 매핑되지 않은 경우 속성을 쿼리할 수 없다는 것입니다. 만약 JSON 구조가 더 동적이고 어떤 스키마에 의해 정의되지 않은 탄력적인 구조를 가질 수 있다고 가정한다면 문제가 될 수 있습니다. 

이 문제는 posjsonhelper 연산자로 해결됩니다. 원하는 어떤 속성에 대해서도 쿼리할 수 있습니다. JSON 타입에서 속성으로 정의될 필요가 없습니다. 또한, 우리 엔티티에서 JSON 컬럼을 저장하는 속성은 우리 예제에서 JsonbConent처럼 복잡한 객체가 될 필요가 없습니다. 자바에서 단순 문자열이 될 수 있습니다.

결론

이전 글에서 언급한 바와 같이, 어떤 경우에는 Postgres JSON 타입과 함수가 NoSQL 데이터베이스에 대한 좋은 대안이 될 수 있습니다. 이는 NoSQL 솔루션을 기술 스택에 추가하는 결정으로부터 우리를 구제하고, 더 많은 복잡성과 추가 비용을 초래하지 않을 수 있습니다.

또한, 관계형 데이터베이스에서 구조화되지 않은 데이터를 저장할 필요가 있을 때 유연성을 제공하며, 그러한 구조에서 쿼리할 수 있는 가능성을 열어줍니다.

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