【问题标题】:Spring Data Rest - PATCH Postgres jsonb fieldSpring Data Rest - PATCH Postgres jsonb 字段
【发布时间】:2017-03-14 23:43:11
【问题描述】:

简短的版本是:如何使用 Spring Data Rest PATCH 方法修补 Postgres jsonb 字段中包含的 JSON 对象?

长版来了,请考虑以下实体:

@Entity
@Table(name = "examples")
public class Example {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private String jsonobject;

    @JsonRawValue
    public String getJsonobject() {
        return jsonobject == null ? null : jsonobject;
    }

    public void setJsonobject(JsonNode jsonobject) {
        this.jsonobject = jsonobject == null ? null : jsonobject.toString();
    }
}

jsonobject 是 Postgres 类型 jsonb。这些 getter/setter 是为 here 提到的 Spring Data Rest 序列化/反序列化它的方法。如these answers 中所述,我们还尝试为该字段提供自己的类型。

我们的目标是使用 Spring Data Rest 修补此字段包含的 JSON 对象。

例如:

GET /examples/1
{
    "id": 1,
    "jsonobject": {
         "foo": {"bar": "Hello"},
         "baz": 2
    }
}

PATCH /examples/1
{
    "jsonobject": {
        "foo": {"bar": "Welcome"}
    }
}

预期输出:

GET /examples/1
{
    "id": 1,
    "jsonobject": {
         "foo": {"bar": "Welcome"},
         "baz": 2
    }
}

当前输出:

GET /examples/1
{
    "id": 1,
    "jsonobject": {
         "foo": {"bar": "Welcome"}
    }
}

Spring Data Rest 修补 Example 资源并覆盖每个请求属性的值,而不是尝试挖掘 JSON 对象的属性以仅修补请求的嵌套属性。

这时我们认为 Spring Data Rest 对 application/merge-patch+jsonapplication/json-patch+json 媒体类型的支持会派上用场。以下是每种媒体类型的输出:

application/merge-patch+json:

PATCH /examples/1
{
    "jsonobject": {
        "foo": {"bar": "Welcome"}
    }
}

输出:

GET /examples/1
{
    "id": 1,
    "jsonobject": {
         "foo": {"bar": "Welcome"}
    }
}

application/json-patch+json:

PATCH /examples/1
[
    { "op": "replace", "path": "/jsonobject/foo/bar", "value": "Welcome" }
]

输出:

{
    "cause": {
        "cause": null,
        "message": "EL1008E:(pos 8): Property or field 'foo' cannot be found on object of type 'java.lang.String' - maybe not public?"
    },
    "message": "Could not read an object of type class com.example.Example from the request!; nested exception is org.springframework.expression.spel.SpelEvaluationException: EL1008E:(pos 8): Property or field 'foo' cannot be found on object of type 'java.lang.String' - maybe not public?"
}

这归结为相同的想法:只查找实体属性,并且要么完全覆盖,要么找不到。

问题如下:有没有办法让 Spring Data Rest 了解它正在处理 jsonb 字段,从而查找 JSON 嵌套属性,而不仅仅是查找实体属性?

注意:@Embeddable/@Embedded 注释最有可能被避免,因为它们意味着知道嵌套的属性名称,这会降低对 jsonb 字段的兴趣。

感谢您的阅读。

【问题讨论】:

    标签: spring postgresql spring-data-rest jsonb json-patch


    【解决方案1】:

    好吧,您的 EntityManager 不知道您的 jsonObject 字段中有一些结构,它是纯字符串。您应该实施自己的解决方法。您可以如何开始工作的一个示例在这里https://github.com/bazar-nazar/pgjson 但这种方法将要求您每次从数据库中读取对象,并进行另一次序列化/反序列化往返。

    但是如果你在 postgresql 上,你可以使用它的所有力量(注意:这将使你的应用程序与 postgresql 紧密耦合,因此数据库将变得更难替换)

    我建议实现自定义 jdbc 查询,例如简单示例:

    public static class JsonPatchRequest {
        String path;
        String operation;
        String value;
    }
    
    
    @Inject
    private JdbcTemplate jdbcTemplate;
    
    @PatchMapping(value = "/example/{id}") 
    public void doPatch(@PathVariable("id") Long id, @RequestBody JsonPatchRequest patchRequest) {
        // this line should transform your request path from  "/jsonobject/foo/bar"  to "{foo,bar}" string
        String postgresqlpath = "{" + patchRequest.path.replaceFirst("/jsonobject/", "").replaceAll("/", ",") + "}";
    
        switch(patchRequest.operation) {
            case "replace" :
                jdbcTemplate.execute("UPDATE example SET jsonobject = jsonb_set(jsonobject, ?, jsonb ?) WHERE id = ?", new PreparedStatementCallback<Void>() {
                    @Override
                    public Void doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException {
                        ps.setString(1, postgresqlpath);
    
                        // this one transforms pure value, to string-escaped value (manual workaround)   so  'value' should become '"value"'
                        ps.setString(2, "\"".concat(patchRequest.value).concat("\""));
    
                        ps.setLong(3, id);
    
                        ps.execute();
                        return null;
                    }
                });
                break;
            case "delete" :
                jdbcTemplate.execute("UPDATE example SET jsonobject = jsonobject #- ? WHERE id = ? ", new PreparedStatementCallback<Void>() {
                    @Override
                    public Void doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException {
                        ps.setString(1, postgresqlpath);
                        ps.setLong(2, id);
                        ps.execute();
                        return null;
                    }
                });
                break;
        }
    }
    

    另请注意:第一种方法将强制您将 jsonobjet 字段设置为预定义类型,因此可以将其替换为纯规范化实体,因此与它无关。第二种方法不会强迫您在 json 中包含任何类型的结构。

    希望这会对您有所帮助。

    【讨论】:

    • 是的,我们正在寻求同样的解决方案。唯一的问题是 PostgreSQL,即使在 9.6 中,也没有真正获得“JSON 合并补丁”RFC。 jsonb_set 回答了问题,但需要为每个嵌套属性执行。我们希望连接 || 运算符不会盲目地覆盖缺失的属性,这样会更有用。我们将在 David Siro 回复后接受您的答复。
    • 我想在这种情况下,您可以使用 PL/v8 并创建自定义函数来执行此类修补。使用已经建立的解决方案(如github.com/Starcounter-Jack/JSON-Patch
    【解决方案2】:

    假设 Hibernate 5 被用作 JPA 实现

    将您的 jsonobjectfield 设为特定的类类型(包含您想要的字段)而不是 String

    然后您可以为 jsonb 类型添加自定义 Hibernate 用户类型。

    @Entity
    @Table(name = "examples")
    public class Example {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @Basic
        @Type(type = "com.package.JsonObjectType")
        private JsonObject jsonobject;
    }
    

    自定义类型实现相当冗长,但本质上它使用 Jackson 的 ObjectMapper 将对象作为 String 传递到 JDBC 语句中(从 ResultSet 检索时反之亦然)。

    public class JsonObjectType implements UserType {
    
        private ObjectMapper mapper = new ObjectMapper();
    
        @Override
        public int[] sqlTypes() {
            return new int[]{Types.JAVA_OBJECT};
        }
    
        @Override
        public Class<JsonObject> returnedClass() {
            return JsonObject.class;
        }
    
        @Override
        public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException {
            final String cellContent = rs.getString(names[0]);
            if (cellContent == null) {
                return null;
            }
            try {
                return mapper.readValue(cellContent.getBytes("UTF-8"), returnedClass());
            } catch (final Exception ex) {
                throw new HibernateException("Failed to convert String to Invoice: " + ex.getMessage(), ex);
            }
        }
    
        @Override
        public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException {
            if (value == null) {
                st.setNull(index, Types.OTHER);
                return;
            }
            try {
                final StringWriter w = new StringWriter();
                mapper.writeValue(w, value);
                w.flush();
                st.setObject(index, w.toString(), Types.OTHER);
            } catch (final Exception ex) {
                throw new HibernateException("Failed to convert Invoice to String: " + ex.getMessage(), ex);
            }
        }
    
        @Override
        public Object deepCopy(final Object value) throws HibernateException {
            try {
                // use serialization to create a deep copy
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
                oos.writeObject(value);
                oos.flush();
                oos.close();
                bos.close();
    
                ByteArrayInputStream bais = new ByteArrayInputStream(bos.toByteArray());
                return new ObjectInputStream(bais).readObject();
            } catch (ClassNotFoundException | IOException ex) {
                throw new HibernateException(ex);
            }
        }
    
        @Override
        public boolean isMutable() {
            return true;
        }
    
        @Override
        public Serializable disassemble(final Object value) throws HibernateException {
            return (Serializable) this.deepCopy(value);
        }
    
        @Override
        public Object assemble(final Serializable cached, final Object owner) throws HibernateException {
            return this.deepCopy(cached);
        }
    
        @Override
        public Object replace(final Object original, final Object target, final Object owner) throws HibernateException {
            return this.deepCopy(original);
        }
    
        @Override
        public boolean equals(final Object obj1, final Object obj2) throws HibernateException {
            if (obj1 == null) {
                return obj2 == null;
            }
            return obj1.equals(obj2);
        }
    
        @Override
        public int hashCode(final Object obj) throws HibernateException {
            return obj.hashCode();
        }
    }
    

    最后,您需要告诉 hibernate 将 Java 对象存储为 jsonb Postgre 类型。这意味着创建您的自定义方言类(并在其中进行配置)。

    public class MyPostgreSQL94Dialect extends PostgreSQL94Dialect {
    
        public MyPostgreSQL94Dialect() {
            this.registerColumnType(Types.JAVA_OBJECT, "jsonb");
        }
    }
    

    所有这些,你应该没问题,Spring Data Rest 修补机制应该可以工作。

    附言

    Answer 受到this github repo 的极大启发,它的作用基本相同,但使用的是 Hibernate 4。看看那个。

    【讨论】:

    • 你好大卫。什么是 JsonObject 类?你写了“你想要的字段”,那么你的答案是否意味着知道存储的 JSON 对象的属性名称?
    • GitHub 存储库回答了评论:他们的 MyJson 类有两个字段来存储 JSON 属性。我们不希望这种行为: jsonb 字段的唯一兴趣是能够存储任何数据,只要它是正确的 JSON 对象。因此,最终用户,即 Spring Data Rest API 使用者,应该能够存储具有任何属性的对象,只要它们可序列化为 JsonObject、Gson 或其他 JSON 实现即可。在这个类上拥有静态属性会使 jsonb 类型毫无用处,至少对我们来说是这样。无论如何,谢谢你的时间:)
    猜你喜欢
    • 1970-01-01
    • 2018-01-08
    • 2022-10-21
    • 2018-03-31
    • 1970-01-01
    • 2019-04-22
    • 2018-12-14
    • 1970-01-01
    • 2019-04-01
    相关资源
    最近更新 更多