com.github.starnowski.posjsonhelper.hibernate6.Hibernate6JsonUpdateStatementBuilder Maven / Gradle / Ivy
/**
* Posjsonhelper library is an open-source project that adds support of
* Hibernate query for https://www.postgresql.org/docs/10/functions-json.html)
*
* Copyright (C) 2023 Szymon Tarnowski
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*/
package com.github.starnowski.posjsonhelper.hibernate6;
import com.github.starnowski.posjsonhelper.core.HibernateContext;
import com.github.starnowski.posjsonhelper.hibernate6.functions.JsonbSetFunction;
import com.github.starnowski.posjsonhelper.hibernate6.functions.RemoveJsonValuesFromJsonArrayFunction;
import com.github.starnowski.posjsonhelper.hibernate6.operators.ConcatenateJsonbOperator;
import com.github.starnowski.posjsonhelper.hibernate6.operators.DeleteJsonbBySpecifiedPathOperator;
import com.github.starnowski.posjsonhelper.json.core.sql.*;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Path;
import org.hibernate.query.sqm.NodeBuilder;
import org.hibernate.query.sqm.tree.SqmTypedNode;
import org.json.JSONArray;
import java.util.Collection;
import static com.github.starnowski.posjsonhelper.json.core.sql.JsonUpdateStatementOperationType.*;
/**
* Builder for SQL statement part that allows to set particular json properties.
* The idea is to execute some kind of patch operation instead of full update operation for json column value.
* To set correct order for operation it uses {@link #jsonUpdateStatementConfigurationBuilder} component.
* For example lets imagine that there is entity class Item that has jsonbContent that stores json.
* It is possible to update json we below code:
*
{@code
* // GIVEN
* Item item = tested.findById(23L);
* DocumentContext document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
* assertThat(document.jsonString()).isEqualTo("{\"child\":{\"pets\":[\"dog\"]},\"inventory\":[\"mask\",\"fins\"],\"nicknames\":{\"school\":\"bambo\",\"childhood\":\"bob\"}}");
* CriteriaUpdate- criteriaUpdate = entityManager.getCriteriaBuilder().createCriteriaUpdate(Item.class);
* Root
- root = criteriaUpdate.from(Item.class);
*
* Hibernate6JsonUpdateStatementBuilder hibernate6JsonUpdateStatementBuilder = new Hibernate6JsonUpdateStatementBuilder(root.get("jsonbContent"), (NodeBuilder) entityManager.getCriteriaBuilder(), hibernateContext);
* hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("child").append("birthday").build(), quote("2021-11-23"));
* hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("child").append("pets").build(), "[\"cat\"]");
* hibernate6JsonUpdateStatementBuilder.appendDeleteBySpecificPath(new JsonTextArrayBuilder().append("inventory").append("0").build());
* hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("parents").append(0).build(), "{\"type\":\"mom\", \"name\":\"simone\"}");
* hibernate6JsonUpdateStatementBuilder.appendJsonbSet(new JsonTextArrayBuilder().append("parents").build(), "[]");
* hibernate6JsonUpdateStatementBuilder.appendDeleteBySpecificPath(new JsonTextArrayBuilder().append("nicknames").append("childhood").build());
*
* // Set the property you want to update and the new value
* criteriaUpdate.set("jsonbContent", hibernate6JsonUpdateStatementBuilder.build());
*
* // Add any conditions to restrict which entities will be updated
* criteriaUpdate.where(entityManager.getCriteriaBuilder().equal(root.get("id"), 23L));
*
* // WHEN
* entityManager.createQuery(criteriaUpdate).executeUpdate();
*
* // THEN
* entityManager.refresh(item);
* document = JsonPath.parse((Object) JsonPath.read(item.getJsonbContent(), "$"));
* assertThat(document.jsonString()).isEqualTo("{\"child\":{\"pets\":[\"cat\"],\"birthday\":\"2021-11-23\"},\"parents\":[{\"name\":\"simone\",\"type\":\"mom\"}],\"inventory\":[\"fins\"],\"nicknames\":{\"school\":\"bambo\"}}");
* }
*
* The above code is going to execute below sql statement for update:
*
*
{@code
* update
* item
* set
* jsonb_content=
* jsonb_set(
* jsonb_set(
* jsonb_set(
* jsonb_set(
* (
* (jsonb_content #- ?::text[]) -- the most nested #- operator
* #- ?::text[])
* , ?::text[], ?::jsonb) -- the most nested jsonb_set operation
* , ?::text[], ?::jsonb)
* , ?::text[], ?::jsonb)
* , ?::text[], ?::jsonb)
* where
* id=?
* }
*
*
* As it can be observed based on generated SQL, by default, the first operation is going to be an operation that deletes JSON content.
* The most nested jsonb_set operation is going to set property "parents" with value "[]".
*
* @param
* @param
* @see #build()
*/
public class Hibernate6JsonUpdateStatementBuilder {
/**
* path object that refers to json property that suppose to be modified
*/
private final Path rootPath;
/**
* hibernate component used to create modification operations
*/
private final NodeBuilder nodeBuilder;
/**
* Hibernate context
*/
private final HibernateContext hibernateContext;
private final JsonUpdateStatementConfigurationBuilder jsonUpdateStatementConfigurationBuilder;
private JsonbSetFunctionFactory jsonbSetFunctionFactory = new DefaultJsonbSetFunctionFactory<>();
private DeleteJsonbBySpecifiedPathOperatorFactory deleteJsonbBySpecifiedPathOperatorFactory = new DefaultDeleteJsonbBySpecifiedPathOperatorFactory<>();
private RemoveArrayItemsFunctionFactory removeArrayItemsFunctionFactory = new DefaultRemoveArrayItemsFunctionFactory<>();
private AddArrayItemsFunctionFactory addArrayItemsFunctionFactory = new DefaultAddArrayItemsFunctionFactory<>();
private final CollectionToJsonArrayStringMapper collectionToJsonArrayStringMapper = new CollectionToJsonArrayStringMapper() {
};
/**
* Construction initialize property {@link #jsonUpdateStatementConfigurationBuilder} and an instance of
* {@link DefaultJsonUpdateStatementOperationSort} as sort component ({@link JsonUpdateStatementConfigurationBuilder#sort}) and an instance
* of {@link DefaultJsonUpdateStatementOperationFilter} as filter component ({@link JsonUpdateStatementConfigurationBuilder#postSortFilter}).
*
* @param rootPath value for {@link #rootPath}
* @param nodeBuilder value for {@link #nodeBuilder}
* @param hibernateContext value for {@link #hibernateContext}
*/
public Hibernate6JsonUpdateStatementBuilder(Path rootPath, NodeBuilder nodeBuilder, HibernateContext hibernateContext) {
this.rootPath = rootPath;
this.nodeBuilder = nodeBuilder;
this.hibernateContext = hibernateContext;
jsonUpdateStatementConfigurationBuilder = new JsonUpdateStatementConfigurationBuilder()
.withSort(new DefaultJsonUpdateStatementOperationSort())
.withPostSortFilter(new DefaultJsonUpdateStatementOperationFilter());
}
public Hibernate6JsonUpdateStatementBuilder withAddArrayItemsFunctionFactory(AddArrayItemsFunctionFactory addArrayItemsFunctionFactory) {
this.addArrayItemsFunctionFactory = addArrayItemsFunctionFactory;
return this;
}
public Hibernate6JsonUpdateStatementBuilder withRemoveArrayItemsFunctionFactory(RemoveArrayItemsFunctionFactory removeArrayItemsFunctionFactory) {
this.removeArrayItemsFunctionFactory = removeArrayItemsFunctionFactory;
return this;
}
public Hibernate6JsonUpdateStatementBuilder withJsonbSetFunctionFactory(JsonbSetFunctionFactory jsonbSetFunctionFactory) {
this.jsonbSetFunctionFactory = jsonbSetFunctionFactory;
return this;
}
public Hibernate6JsonUpdateStatementBuilder withDeleteJsonbBySpecifiedPathOperatorFactory(DeleteJsonbBySpecifiedPathOperatorFactory deleteJsonbBySpecifiedPathOperatorFactory) {
this.deleteJsonbBySpecifiedPathOperatorFactory = deleteJsonbBySpecifiedPathOperatorFactory;
return this;
}
public JsonUpdateStatementConfigurationBuilder getJsonUpdateStatementConfigurationBuilder() {
return jsonUpdateStatementConfigurationBuilder;
}
/**
* Adding {@link JsonUpdateStatementOperationType#JSONB_SET} type operation that set value for specific json path
*
* @param jsonTextArray json array that specified path for property
* @param value json value that suppose to be set
* @return a reference to the constructor component for which the methods were executed
*/
public Hibernate6JsonUpdateStatementBuilder appendJsonbSet(JsonTextArray jsonTextArray, String value) {
return appendJsonbSet(jsonTextArray, value, null);
}
public Hibernate6JsonUpdateStatementBuilder appendJsonbSet(JsonTextArray jsonTextArray, String value, C customValue) {
jsonUpdateStatementConfigurationBuilder.append(JSONB_SET, jsonTextArray, value, customValue);
return this;
}
/**
* Adding {@link JsonUpdateStatementOperationType#DELETE_BY_SPECIFIC_PATH} type operation that deletes property for specific json path
*
* @param jsonTextArray json array that specified path for property
* @return a reference to the constructor component for which the methods were executed
*/
public Hibernate6JsonUpdateStatementBuilder appendDeleteBySpecificPath(JsonTextArray jsonTextArray) {
jsonUpdateStatementConfigurationBuilder.append(DELETE_BY_SPECIFIC_PATH, jsonTextArray, null);
return this;
}
/**
* Setting the {@link JsonUpdateStatementConfigurationBuilder#sort} property for {@link #jsonUpdateStatementConfigurationBuilder} component
*
* @param sort sorting component
* @return a reference to the constructor component for which the methods were executed
*/
public Hibernate6JsonUpdateStatementBuilder withSort(JsonUpdateStatementConfigurationBuilder.JsonUpdateStatementOperationSort sort) {
jsonUpdateStatementConfigurationBuilder.withSort(sort);
return this;
}
/**
* Setting the {@link JsonUpdateStatementConfigurationBuilder#postSortFilter} property for {@link #jsonUpdateStatementConfigurationBuilder} component
*
* @param postSortFilter postSortFilter filtering component
* @return a reference to the constructor component for which the methods were executed
*/
public Hibernate6JsonUpdateStatementBuilder withPostSortFilter(JsonUpdateStatementConfigurationBuilder.JsonUpdateStatementOperationFilter postSortFilter) {
jsonUpdateStatementConfigurationBuilder.withPostSortFilter(postSortFilter);
return this;
}
/**
* Build part of statement that set json property specified by {@link #rootPath}.
* Based on configuration produced by {@link #jsonUpdateStatementConfigurationBuilder} the method generates final expression.
* For example:
* Lest assume that method {@link JsonUpdateStatementConfigurationBuilder#build()} returns configuration with below list:
*
* {@code
* [
* JsonUpdateStatementOperation{jsonTextArray={parents}, operation=JSONB_SET, value='[]'},
* JsonUpdateStatementOperation{jsonTextArray={child,birthday}, operation=JSONB_SET, value='"2021-11-23"'},
* JsonUpdateStatementOperation{jsonTextArray={child,pets}, operation=JSONB_SET, value='["cat"]'},
* JsonUpdateStatementOperation{jsonTextArray={parents,0}, operation=JSONB_SET, value='{"type":"mom", "name":"simone"}'}
* ]
* }
*
* The expression generated on such would be translated to below sql part:
*
*
{@code
* jsonb_set(
* jsonb_set(
* jsonb_set(
* jsonb_set(jsonb_content, ?::text[], ?::jsonb) -- top operation
* , ?::text[], ?::jsonb)
* , ?::text[], ?::jsonb)
* , ?::text[], ?::jsonb)
* }
*
* @return expression object generated based on {@link #jsonUpdateStatementConfigurationBuilder} configuration
*/
public Expression extends T> build() {
JsonUpdateStatementConfiguration configuration = jsonUpdateStatementConfigurationBuilder.build();
SqmTypedNode current = null;
for (JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation : configuration.getOperations()) {
switch (operation.getOperation()) {
case DELETE_BY_SPECIFIC_PATH:
if (current == null) {
current = deleteJsonbBySpecifiedPathOperatorFactory.build(nodeBuilder, rootPath, operation, hibernateContext);
} else {
current = deleteJsonbBySpecifiedPathOperatorFactory.build(nodeBuilder, current, operation, hibernateContext);
}
break;
case JSONB_SET:
if (current == null) {
current = jsonbSetFunctionFactory.build(nodeBuilder, rootPath, operation, hibernateContext);
} else {
current = jsonbSetFunctionFactory.build(nodeBuilder, current, operation, hibernateContext);
}
break;
case REMOVE_ARRAY_ITEMS:
if (current == null) {
current = removeArrayItemsFunctionFactory.build(nodeBuilder, rootPath, operation, hibernateContext);
} else {
current = removeArrayItemsFunctionFactory.build(nodeBuilder, current, operation, hibernateContext);
}
break;
case ADD_ARRAY_ITEMS:
if (current == null) {
current = addArrayItemsFunctionFactory.build(nodeBuilder, rootPath, operation, hibernateContext);
} else {
current = addArrayItemsFunctionFactory.build(nodeBuilder, current, operation, hibernateContext);
}
break;
}
}
return (Expression extends T>) current;
}
public Hibernate6JsonUpdateStatementBuilder appendRemoveArrayItems(JsonTextArray jsonTextArray, String jsonArrayString) {
jsonUpdateStatementConfigurationBuilder.append(REMOVE_ARRAY_ITEMS, jsonTextArray, jsonArrayString);
return this;
}
public Hibernate6JsonUpdateStatementBuilder appendRemoveArrayItems(JsonTextArray jsonTextArray, Collection> collection) {
return appendRemoveArrayItems(jsonTextArray, collectionToJsonArrayStringMapper.map(collection));
}
public Hibernate6JsonUpdateStatementBuilder appendAddArrayItems(JsonTextArray jsonTextArray, String jsonArrayString) {
jsonUpdateStatementConfigurationBuilder.append(ADD_ARRAY_ITEMS, jsonTextArray, jsonArrayString);
return this;
}
public Hibernate6JsonUpdateStatementBuilder appendAddArrayItems(JsonTextArray jsonTextArray, Collection> collection) {
return appendAddArrayItems(jsonTextArray, collectionToJsonArrayStringMapper.map(collection));
}
public interface AddArrayItemsFunctionFactory {
default JsonbSetFunction build(NodeBuilder nodeBuilder, Path rootPath, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
ConcatenateJsonbOperator concatenateOperator = new ConcatenateJsonbOperator(nodeBuilder, new JsonBExtractPath(rootPath, nodeBuilder, operation.getJsonTextArray().getPathWithStringValues()), operation.getValue(), hibernateContext);
return new JsonbSetFunction(nodeBuilder, (SqmTypedNode) rootPath, operation.getJsonTextArray().toString(), concatenateOperator, hibernateContext);
}
default JsonbSetFunction build(NodeBuilder nodeBuilder, SqmTypedNode sqmTypedNode, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
ConcatenateJsonbOperator concatenateOperator = new ConcatenateJsonbOperator(nodeBuilder, new JsonBExtractPath(sqmTypedNode, nodeBuilder, operation.getJsonTextArray().getPathWithStringValues()), operation.getValue(), hibernateContext);
return new JsonbSetFunction(nodeBuilder, sqmTypedNode, operation.getJsonTextArray().toString(), concatenateOperator, hibernateContext);
}
}
public interface JsonbSetFunctionFactory {
default JsonbSetFunction build(NodeBuilder nodeBuilder, Path rootPath, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
return new JsonbSetFunction(nodeBuilder, rootPath, operation.getJsonTextArray().toString(), operation.getValue(), hibernateContext);
}
default JsonbSetFunction build(NodeBuilder nodeBuilder, SqmTypedNode sqmTypedNode, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
return new JsonbSetFunction(nodeBuilder, sqmTypedNode, operation.getJsonTextArray().toString(), operation.getValue(), hibernateContext);
}
}
public interface RemoveArrayItemsFunctionFactory {
default JsonbSetFunction build(NodeBuilder nodeBuilder, Path rootPath, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
RemoveJsonValuesFromJsonArrayFunction deleteOperator = new RemoveJsonValuesFromJsonArrayFunction(nodeBuilder, new JsonBExtractPath(rootPath, nodeBuilder, operation.getJsonTextArray().getPathWithStringValues()), operation.getValue(), hibernateContext);
return new JsonbSetFunction(nodeBuilder, (SqmTypedNode) rootPath, operation.getJsonTextArray().toString(), deleteOperator, hibernateContext);
}
default JsonbSetFunction build(NodeBuilder nodeBuilder, SqmTypedNode sqmTypedNode, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
RemoveJsonValuesFromJsonArrayFunction deleteOperator = new RemoveJsonValuesFromJsonArrayFunction(nodeBuilder, new JsonBExtractPath(sqmTypedNode, nodeBuilder, operation.getJsonTextArray().getPathWithStringValues()), operation.getValue(), hibernateContext);
return new JsonbSetFunction(nodeBuilder, sqmTypedNode, operation.getJsonTextArray().toString(), deleteOperator, hibernateContext);
}
}
public interface DeleteJsonbBySpecifiedPathOperatorFactory {
default DeleteJsonbBySpecifiedPathOperator build(NodeBuilder nodeBuilder, Path rootPath, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
return new DeleteJsonbBySpecifiedPathOperator(nodeBuilder, rootPath, operation.getJsonTextArray().toString(), hibernateContext);
}
default DeleteJsonbBySpecifiedPathOperator build(NodeBuilder nodeBuilder, SqmTypedNode sqmTypedNode, JsonUpdateStatementConfiguration.JsonUpdateStatementOperation operation, HibernateContext hibernateContext) {
return new DeleteJsonbBySpecifiedPathOperator(nodeBuilder, sqmTypedNode, operation.getJsonTextArray().toString(), hibernateContext);
}
}
public interface CollectionToJsonArrayStringMapper {
default String map(Collection> collection) {
return new JSONArray(collection).toString();
}
}
public static class DefaultJsonbSetFunctionFactory implements JsonbSetFunctionFactory {
}
public static class DefaultRemoveArrayItemsFunctionFactory implements RemoveArrayItemsFunctionFactory {
}
public static class DefaultDeleteJsonbBySpecifiedPathOperatorFactory implements DeleteJsonbBySpecifiedPathOperatorFactory {
}
public static class DefaultAddArrayItemsFunctionFactory implements AddArrayItemsFunctionFactory {
}
}