Spring Data Rest - PATCH Postgres jsonb field Spring Data Rest - PATCH Postgres jsonb field postgresql postgresql

Spring Data Rest - PATCH Postgres jsonb field


well, your EntityManager doesn't knows that there is some structure inside of your jsonObject field, which is pure string for it. You should implement your own workarounds. One example of how you can start working is here https://github.com/bazar-nazar/pgjson But such approach will require you each time read the object from database, and make another serialize/deserialize roundtrip.

BUT IF you are on postgresql, you can use all its power (note: this will make your application tightly coupled with postgresql, and thus database will become harder to replace)

I would suggest to implement custom jdbc queries, like simple example:

public static class JsonPatchRequest {    String path;    String operation;    String value;}@Injectprivate 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;    }}

also note: the first approach will force you to make the jsonobjet field of predefined type, and thus it can be replaced with pure normalized entity, and so not much to do with it. The second approach doesn't force you to have any kind of structure inside of your json.

hope this will help you.


Assumes Hibernate 5 is used as JPA implementation

Make your jsonobjectfield to be a specific class type (with fields you want) instead of String.

Then you can add a custom Hibernate user type for a jsonb types.

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

The custom type implementation is quite verbose, but essentially it uses Jackson's ObjectMapper to pass the the object as String into JDBC statement (and vice versa when retrieving from 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();    }}

Finally, you need to tell hibernate to store Java objects as jsonb Postgre type. That means creating your custom dialect class (and configure that in).

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

With all of that, you should be fine and Spring Data Rest patching mechanism should work.

PS

Answer heavily inspired by this github repo, which does essentially the same, but with Hibernate 4. Take a look at that.