Сохранение объекта JSON с использованием Hibernate и JPA

Я пытаюсь сохранить объект JSON в базе данных MySQL весной загрузки. Я знаю, что делаю что-то неправильно, но я не могу понять, что это такое, потому что я довольно новичок в Spring.

У меня есть конечная точка отдыха, где я получаю следующий объект JSON (через HTTP PUT), и мне нужно сохранить его в базе данных, чтобы пользователь мог его позже получить (через HTTP GET).

{
  "A": {
    "Name": "Cat",
    "Age": "1"
  },
  "B": {
    "Name": "Dog",
    "Age": "2"
  },
  "C": {
    "Name": "Horse",
    "Age": "1"
  }
}

Следует отметить, что в приведенном выше случае количество ключей в объекте, может изменяться в связи с этим требованием Я использую HashMap, чтобы поймать объект в контроллере.

@RequestMapping(method = RequestMethod.POST)
    public String addPostCollection(@RequestBody HashMap<String, Animal> hp) {

        hp.forEach((x, y) -> {
            postRepository.save(hp.get(x));
        });

        return "OK";

    }

Как вы можете видеть в методе, я могу перебрать HashMap и сохранить каждый объект Animal в db. Но я ищу способ сохранить всю HashMap в одной записи. Я прочитал некоторое чтение, и они предлагают мне использовать сопоставление @ManyToMany.

Может ли кто-нибудь указать мне в направлении сохранения HashMap по-другому? (или использует @ManyToMany единственный и правильный способ сделать это?)

Ответ 1

Как я объяснял в этой статье, очень легко сохранять объекты JSON с помощью Hibernate.

Вам не нужно создавать все эти типы вручную, вы можете просто получить их через Maven Central, используя следующую зависимость:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version> 
</dependency> 

For more info, check out the hibernate-types open-source project.

Теперь, чтобы объяснить, как все это работает.

Предполагая, что у вас есть следующая сущность:

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(
    name = "jsonb-node", 
    typeClass = JsonNodeBinaryType.class
)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    @Type( type = "jsonb-node" )
    @Column(columnDefinition = "jsonb")
    private JsonNode properties;

    //Getters and setters omitted for brevity
}

Обратите внимание на две вещи в приведенном выше фрагменте кода:

  • @TypeDef используется для определения нового пользовательского Типа гибернации, jsonb-node, который обрабатывается JsonNodeBinaryType
  • атрибут properties имеет тип столбца jsonb и отображается как Джексон JsonNode

JsonNodeBinaryType реализован так:

public class JsonNodeBinaryType
    extends AbstractSingleColumnStandardBasicType<JsonNode> {

    public JsonNodeBinaryType() {
        super( 
            JsonBinarySqlTypeDescriptor.INSTANCE, 
            JsonNodeTypeDescriptor.INSTANCE 
        );
    }

    public String getName() {
        return "jsonb-node";
    }
}

JsonBinarySqlTypeDescriptor выглядит следующим образом:

public class JsonBinarySqlTypeDescriptor 
    extends AbstractJsonSqlTypeDescriptor {

    public static final JsonBinarySqlTypeDescriptor INSTANCE = 
        new JsonBinarySqlTypeDescriptor();

    @Override
    public <X> ValueBinder<X> getBinder(
            final JavaTypeDescriptor<X> javaTypeDescriptor
        ) {
        return new BasicBinder<X>(javaTypeDescriptor, this) {
            @Override
            protected void doBind(
                    PreparedStatement st, 
                    X value, 
                    int index, 
                    WrapperOptions options
                ) throws SQLException {
                st.setObject(
                    index, 
                    javaTypeDescriptor.unwrap(
                        value, 
                        JsonNode.class, 
                        options
                    ), 
                    getSqlType()
                );
            }

            @Override
            protected void doBind(
                    CallableStatement st, 
                    X value, 
                    String name, 
                    WrapperOptions options
                ) throws SQLException {
                st.setObject(
                    name, 
                    javaTypeDescriptor.unwrap(
                        value, 
                        JsonNode.class, 
                        options
                    ), 
                    getSqlType()
                );
            }
        };
    }
}

Исходный код AbstractJsonSqlTypeDescriptor можно визуализировать в этой статье.

Теперь JsonNodeTypeDescriptor отвечает за преобразование JsonNode в различные представления, которые могут использоваться базовым драйвером JDBC во время привязки параметров или выборки из объекта JSON из базового ResultSet.

public class JsonNodeTypeDescriptor
        extends AbstractTypeDescriptor<JsonNode> {

    public static final JsonNodeTypeDescriptor INSTANCE = 
        new JsonNodeTypeDescriptor();

    public JsonNodeTypeDescriptor() {
        super( 
            JsonNode.class, 
            new MutableMutabilityPlan<JsonNode>() {
                @Override
                protected JsonNode deepCopyNotNull(
                        JsonNode value
                    ) {
                    return JacksonUtil.clone(value);
                }
            }
        );
    }

    @Override
    public boolean areEqual(JsonNode one, JsonNode another) {
        if ( one == another ) {
            return true;
        }
        if ( one == null || another == null ) {
            return false;
        }
        return
            JacksonUtil.toJsonNode(
                JacksonUtil.toString(one)
            ).equals(
                JacksonUtil.toJsonNode(
                    JacksonUtil.toString(another)
                )
            );
    }

    @Override
    public String toString(JsonNode value) {
        return JacksonUtil.toString(value);
    }

    @Override
    public JsonNode fromString(String string) {
        return JacksonUtil.toJsonNode(string);
    }

    @SuppressWarnings({ "unchecked" })
    @Override
    public <X> X unwrap(
            JsonNode value, 
            Class<X> type, 
            WrapperOptions options
        ) {
        if ( value == null ) {
            return null;
        }
        if ( String.class.isAssignableFrom( type ) ) {
            return (X) toString(value);
        }
        if ( JsonNode.class.isAssignableFrom( type ) ) {
            return (X) JacksonUtil.toJsonNode(toString(value));
        }
        throw unknownUnwrap( type );
    }

    @Override
    public <X> JsonNode wrap(X value, WrapperOptions options) {
        if ( value == null ) {
            return null;
        }
        return fromString(value.toString());
    }

}

Вот оно!

Теперь, если вы сохраните объект:

Book book = new Book();
book.setIsbn( "978-9730228236" );
book.setProperties(
    JacksonUtil.toJsonNode(
        "{" +
        "   \"title\": \"High-Performance Java Persistence\"," +
        "   \"author\": \"Vlad Mihalcea\"," +
        "   \"publisher\": \"Amazon\"," +
        "   \"price\": 44.99" +
        "}"
    )
);

entityManager.persist( book );

Hibernate собирается сгенерировать следующий оператор SQL:

INSERT INTO
    book 
(
    isbn, 
    properties, 
    id
) 
VALUES
(
    '978-9730228236', 
    '{"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon","price":44.99}',  
    1
)

И вы также можете загрузить его обратно и изменить его:

Session session = entityManager.unwrap( Session.class );

Book book = session
    .bySimpleNaturalId( Book.class )
    .load( "978-9730228236" );

LOGGER.info( "Book details: {}", book.getProperties() );

book.setProperties(
    JacksonUtil.toJsonNode(
        "{" +
        "   \"title\": \"High-Performance Java Persistence\"," +
        "   \"author\": \"Vlad Mihalcea\"," +
        "   \"publisher\": \"Amazon\"," +
        "   \"price\": 44.99," +
        "   \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" +
        "}"
    )
);

Hibernate берет на себя инструкции UPDATE для вас:

SELECT  b.id AS id1_0_
FROM    book b
WHERE   b.isbn = '978-9730228236'

SELECT  b.id AS id1_0_0_ ,
        b.isbn AS isbn2_0_0_ ,
        b.properties AS properti3_0_0_
FROM    book b
WHERE   b.id = 1

-- Book details: {"price":44.99,"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon"}

UPDATE
    book 
SET
    properties = '{"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon","price":44.99,"url":"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/"}'
WHERE
    id = 1

Весь код доступен на GitHub.

Ответ 2

Ваш JSON хорошо структурирован, поэтому обычно нет необходимости сохранять всю карту в одной записи. Вы не сможете использовать функции запроса Hibernate/JPA и многое другое.

Если вы действительно хотите сохранить всю карту в одной записи, вы можете сохранить карту в ее строчном представлении и, как уже было предложено, использовать парсер JSON, такой как Jackson, для восстановления вашего HashMap

@Entity
public class Animals {

  private String animalsString;

  public void setAnimalsString(String val) {
    this.animalsString = val;
  }

  public String getAnimalsString() {
    return this.animalsMap;
  }

  public HashMap<String, Animal> getAnimalsMap() {
    ObjectMapper mapper = new ObjectMapper();
    TypeReference<HashMap<String,Animal>> typeRef = new TypeReference<HashMap<String,Animal>>() {};
    return mapper.readValue(animalsString, typeRef); 
  }

}

Ваш класс животных:

public class Animal {

  private String name;
  private int age;

  /* getter and setter */
  /* ... */
}

И вы можете изменить свой метод контроллера на

@RequestMapping(method = RequestMethod.POST)
public String addPostCollection(@RequestBody String hp) {
  Animals animals = new Animals();
  animals.setAnimalsString(hp);
  animalsRepository.save(hp);
  return "OK";
}

Ответ 3

Вы можете использовать FasterXML (или аналогичный) для разбора Json в реальном объекте (вам нужно определить класс) и использовать Json.toJson(yourObj).toString() для извлечения строки Json. Это также упрощает работу с объектами, так как ваш класс данных также может иметь функциональность.

Ответ 4

Одно животное - одна запись. Вы сохраняете больше записей, а не одну запись. Вы можете совершать больше записей в одной транзакции. См.: Как сохранить много объектов (JPA)