Postgres 데이터베이스는 몇 가지 JSON 유형 및 특수 연산을 해당 유형에 대해 지원합니다.
일부 경우 이러한 연산은 MongoDB 같은 문서 데이터베이스나 다른 NoSQL 데이터베이스에 대한 좋은 대안일 수 있습니다. 물론 MongoDB 같은 데이터베이스는 복제 프로세스가 더 우수할 수 있지만, 이 주제는 이 글의 범위를 벗어납니다.
이 글에서는 JSON 연산을 사용하는 방법에 대해 Hibernate 프레임워크와 함께 버전 5를 사용하는 프로젝트에 초점을 맞출 것입니다.
예시 모델
우리의 모델은 아래 예시와 같습니다:
@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;
}
}
중요!: 우리는 jsonbContent
속성에 특정 JSON 유형을 사용할 수 있지만, 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"]}}');
Native Query 접근법
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 사용
아래 예시는 기존 원시 쿼리를 사용한 이전 예시와 유사한 동작을 보입니다. 그러나 이 경우에는 조건 빌더를 사용할 것입니다.
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 속성이 배열을 포함하는 경우 검색하려는 요소 중 하나 이상을 포함하는지 확인할 수 있습니다.
필요한 DDL 변경
위의 연산자는 특수 문자 때문에 HQL에서 사용할 수 없습니다. 그렇기 때문에 이들을 커스텀 SQL 함수로 감싸야 합니다. Posjsonhelper
라이브러리는 이러한 연산자를 감싸는 두 가지 커스텀 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();
}
태그가 두 요소를 포함한다면, 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 속성에 배열이 포함되어 있고, 우리가 검색하고자 하는 문자열 요소 중 적어도 하나를 가진 레코드를 조회하는 쿼리를 어떻게 생성하는지 보여줍니다.
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();
}
태그가 두 요소를 포함한다면 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 객체 및 dao 테스트를 참조하십시오.
결론
몇몇 경우에는 Postgres JSON 타입과 함수가 NoSQL 데이터베이스의 좋은 대안이 될 수 있습니다. 이는 NoSQL 솔루션을 기술 스택에 추가하는 결정으로부터 우리를 구해줄 수 있으며, 이로 인해 더 많은 복잡성과 추가 비용이 발생할 수 있습니다.
Source:
https://dzone.com/articles/postgres-json-functions-with-hibernate-5