2016-11-01 5 views
7

Краткая версия: Как исправить объект JSON, содержащийся в поле Postgres jsonb, используя метод PATCH Spring Data Rest PATCH?Spring Data Rest - PATCH Postgres jsonb field

Здесь идет длинная версия, пожалуйста, рассмотрим следующий объект:

@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. Эти геттер/сеттер - это способ сериализации/десериализации его для Spring Data Rest, упомянутого here. Мы также попытались дать поле своему собственному типу, как указано в these answers.

Наша цель - исправить объект JSON, который содержит это поле, используя Spring Data Rest.

Например:

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"} 
    } 
} 

Спринг данных Остальные патчи Пример ресурс и переопределяет значение для каждого запрашиваемого атрибута, вместо того, чтобы пытаться вникнуть в свойства объекта JSON только для исправления запрошенных вложенных свойства.

Именно тогда, когда мы подумали, что поддержка весенних данных для поддержки application/merge-patch+json и application/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, а не только ищет атрибуты сущности?

Nb: @Embeddable/@Embedded Аннотации, скорее всего, следует избегать, поскольку они подразумевают знание вложенных имен свойств, что снизит процент для поля jsonb.

Благодарим вас за чтение.

ответ

2

ну, ваш 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.

надеюсь, что это вам поможет.

+0

Да, мы идем по аналогичному решению. Единственная проблема, которая PostgreSQL, даже в 9.6, на самом деле не получает RFC слияния JSON. 'jsonb_set' отвечает на проблему, но ее нужно выполнить для каждого вложенного свойства. Мы желаем, чтобы оператор concactenate '' 'не слепо перезаписывал отсутствующие свойства, что было бы более полезным. Мы примем ваш ответ после ответа Дэвида Сиро. –

+0

Думаю, в этом случае вы можете пойти с PL/v8 и создать пользовательскую функцию для выполнения таких исправлений. используя уже установленные решения (например, https://github.com/Starcounter-Jack/JSON-Patch) –

1

Предполагает Hibernate 5 используется в качестве реализации JPA

Сделайте свой jsonobject поле, чтобы быть определенный тип класса (с полями, вы хотите) вместо String.

Затем вы можете добавить пользовательский тип Hibernate для типов jsonb.

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

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

    @Basic 
    @Type(type = "com.package.JsonObjectType") 
    private JsonObject jsonobject; 
} 

Реализация пользовательского типа является довольно громоздким, но в основном он использует Джексон 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(); 
    } 
} 

Наконец, нужно сказать, спящий режим для хранения объектов Java в качестве jsonb типа Postgre. Это означает создание вашего настраиваемого класса диалектов (и настройка этого).

public class MyPostgreSQL94Dialect extends PostgreSQL94Dialect { 

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

Со всем этим вы должны быть в порядке, а механизм патчей Spring Data Rest должен работать.

PS

Ответ сильно вдохновлен this GitHub репо, который делает по существу то же самое, но с Hibernate 4. Посмотрите на это.

+0

Привет, Дэвид. Что такое класс JsonObject? Вы написали «с полями, которые хотите», и ваш ответ подразумевает знание имен свойств хранимого объекта JSON? –

+0

Реплика GitHub отвечает на комментарий: их класс 'MyJson' имеет два поля для хранения свойств JSON. Мы не хотим этого поведения: единственный интерес для поля jsonb заключается в том, чтобы хранить любые данные до тех пор, пока они являются надлежащим объектом JSON. Поэтому конечный пользователь, пользователь Spring Data Rest API, должен иметь возможность хранить объекты с любым свойством, если они сериализуемы в JsonObject, Gson или другую реализацию JSON.Наличие статических свойств в этом классе делает тип jsonb бесполезным, по крайней мере для нас. Спасибо за ваше время в любом случае :) –

 Смежные вопросы

  • Нет связанных вопросов^_^