2016-11-25 105 views
3

我试图在春季启动时在MySQL数据库中存储JSON对象。我知道自己做错了什么,但我无法弄清楚它是什么,因为我对Spring很陌生。使用休眠和JPA持久化JSON对象

我有一个休息端点,我得到下面的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和DB坚持每个Animal对象。但我正在寻找一种方法将整个HashMap保存在一个记录中。我已经做了一些阅读,他们建议我使用@ManyToMany映射。

任何人都可以用不同的方式指向我以坚持HashMap的方向吗? (或正在使用该@ManyToMany做到这一点的唯一和正确的方式?)

+0

“整个HashMap在单个记录中”?或者单个数据库事务中的整个hashmap? –

+0

我的意思是单一记录。 – Fawzan

回答

8

正如我在this article中解释的那样,使用Hibernate持久化JSON对象非常容易。

您不必手动创建所有这些类型的,你可以通过Maven的中央使用以下依赖简单地得到 他们:

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

欲了解更多信息,请查看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用于定义一个新的自定义Hibernate类型,jsonb-node这是由处理JsonNodeBinaryType
  • properties属性具有jsonb列类型,它被映射为Jackson 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源代码可以在this article可视化。现在

,所述JsonNodeTypeDescriptor是负责转换成JsonNode可能由潜在的JDBC驱动结合参数或从底层ResultSet从JSON对象提取过程中可以使用各种表示。

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); 

休眠会生成以下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/\"" + 
     "}" 
    ) 
); 

休眠正在为您的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上都可用。

+0

谢谢你的提示!但是,您可以使用不知道任何JSON类型的H2数据库进行此项工作吗?我使用Spring Boot和H2进行测试。也许重写H2方言以某种方式将JSON类型视为文本?但是没有计算出结果..谢谢 – vernjan

+0

如果你在生产环境中没有运行H2并且你使用它来进行测试,而你的生产系统使用PostgreSQL或MySQL,那么你的集成测试根本没有任何价值。您需要使用您在生产中用于测试的相同数据库。看看[这篇文章](https://vladmihalcea.com/2017/02/09/how-to-run-integration-tests-at-warp-speed-with-docker-and-tmpfs/),看看你是如何cna在PostgreSQL和MySQL上运行集成测试几乎和内存数据库一样快。 –

1

可以使用FasterXML(或类似)将JSON解析成一个实际的对象(你需要定义类),并使用Json.toJson(yourObj).toString()检索Json字符串。它还简化了对象的工作,因为您的数据类也可能具有功能。

1

您的JSON结构良好,因此通常无需将整个地图保存在一条记录中。您将无法使用Hibernate/JPA查询功能等等。

如果你真的要坚持在一个单独的记录整个地图,你可以坚持在地图中的字符串表示,而且前面已经提出,使用JSON解析器像杰克逊重建你的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"; 
} 
+0

我认为这对我来说是最好的选择。将对象保存为字符串并将其映射到Map。 – Fawzan