【问题标题】:How to map a map JSON column to Java Object with JPA如何使用 JPA 将映射 JSON 列映射到 Java 对象
【发布时间】:2014-11-02 12:25:10
【问题描述】:

我们有一张大桌子,里面有很多列。迁移到 MySQL Cluster 后,无法创建表,原因是:

错误 1118 (42000):行大小太大。使用的表类型的最大行大小(不包括 BLOB)为 14000。这包括存储开销,请查看手册。您必须将某些列更改为 TEXT 或 BLOB

举个例子:

@Entity @Table (name = "appconfigs", schema = "myproject")
public class AppConfig implements Serializable
{
    @Id @Column (name = "id", nullable = false)
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private int id;

    @OneToOne @JoinColumn (name = "app_id")
    private App app;

    @Column(name = "param_a")
    private ParamA parama;

    @Column(name = "param_b")
    private ParamB paramb;
}

这是一个用于存储配置参数的表格。我在想我们可以将一些列合并为一个并将其存储为 JSON 对象并将其转换为一些 Java 对象。

例如:

@Entity @Table (name = "appconfigs", schema = "myproject")
public class AppConfig implements Serializable
{
    @Id @Column (name = "id", nullable = false)
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private int id;

    @OneToOne @JoinColumn (name = "app_id")
    private App app;

    @Column(name = "params")
    //How to specify that this should be mapped to JSON object?
    private Params params;
}

我们定义的地方:

public class Params implements Serializable
{
    private ParamA parama;
    private ParamB paramb;
}

通过使用它,我们可以将所有列合并为一个并创建我们的表。或者我们可以将整个表拆分成几个表。我个人更喜欢第一种解决方案。

无论如何,我的问题是如何映射作为文本并包含 Java 对象的 JSON 字符串的 Params 列?

【问题讨论】:

  • 如果您有很多配置参数,只需使用具有 2 列的普通表:键和值并将其加载到映射。如果您想将参数存储为 JSON 或 XML,只需将其存储/读取为文本并稍后转换。
  • @Rad 确实 this 帮助你
  • @user1516873 我们认为这是最终解决方案。如果我没记错的话,它会在尝试修改数据时增加复杂性。还是谢谢。
  • @ankur-singhal,我不确定。我们正在为我们的表使用带有 NDBCluster 引擎的 MySQL 集群。它仍然适用吗?

标签: java json jpa orm


【解决方案1】:

您可以使用 JPA 转换器将您的实体映射到数据库。 只需在您的 params 字段中添加与此类似的注释即可:

@Convert(converter = JpaConverterJson.class)

然后以类似的方式创建类(这会转换一个通用对象,您可能需要对其进行专门化):

@Converter(autoApply = true)
public class JpaConverterJson implements AttributeConverter<Object, String> {

  private final static ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public String convertToDatabaseColumn(Object meta) {
    try {
      return objectMapper.writeValueAsString(meta);
    } catch (JsonProcessingException ex) {
      return null;
      // or throw an error
    }
  }

  @Override
  public Object convertToEntityAttribute(String dbData) {
    try {
      return objectMapper.readValue(dbData, Object.class);
    } catch (IOException ex) {
      // logger.error("Unexpected IOEx decoding json from database: " + dbData);
      return null;
    }
  }

}

就是这样:你可以使用这个类将任何对象序列化为表中的json。

【讨论】:

  • 注意:如果您还使用 Hibernate Envers 和低于 5 的 Hibernate 版本(在撰写本文时尚未发布),则此解决方案不起作用。见HHH-9042
  • 如果存储在数据库中的json是一个数组,意思类似于:[{...},{...},{...}],转换器会抛出异常还是映射器会处理它吗?
  • 感觉应该有一个注释可以为我们做这件事。这是一个如此通用的用例,似乎是许多人想要的。唉,我还没有找到一个。谢谢您的回答。我最终自己这样做了,在寻找更好的方法时,我偶然发现了您的答案,这与我的基本相同。不幸的是,这让我放心,我的方式是“最干净的”。
  • @Converter(autoApply = true) 不要把它放在类上,因为它会在序列化和反序列化时自动将此转换器应用于所有对象,因为日期类型和日期时间 sql 类型有问题
  • JPA 将在LinkedHashMap 中返回结果,由于object.class,来自jso-db 的每个Json 属性都将转换为LinkedHashMap
【解决方案2】:

JPA AttributeConverter 过于受限,无法映射 JSON 对象类型,尤其是如果您想将它们保存为 JSON 二进制文件。

您无需创建自定义 Hibernate 类型即可获得 JSON 支持,您只需使用 Hibernate Types OSS project

例如,如果您使用的是 Hibernate 5.2 或更新版本,则需要在 Maven pom.xml 配置文件中添加以下依赖项:

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

现在,您需要在实体属性级别声明新类型,或者更好的是,使用@MappedSuperclass 在基类中的类级别声明新类型:

@TypeDef(name = "json", typeClass = JsonType.class)

实体映射将如下所示:

@Type(type = "json")
@Column(columnDefinition = "json")
private Location location;

如果您使用的是 Hibernate 5.2 或更高版本,则 JSON 类型将由 MySQL57Dialect 自动注册。

否则需要自己注册:

public class MySQLJsonDialect extends MySQL55Dialect {

    public MySQLJsonDialect() {
        super();
        this.registerColumnType(Types.JAVA_OBJECT, "json");
    }
}

并且,将hibernate.dialect Hibernate 属性设置为使用您刚刚创建的MySQLJsonDialect 类的完全限定类名。

【讨论】:

【解决方案3】:

如果需要在响应客户端(例如rest API响应)时将json类型属性映射为json格式,添加@JsonRawValue如下:

@Column(name = "params", columnDefinition = "json")
@JsonRawValue
private String params;

这可能不会为服务器端使用做 DTO 映射,但客户端会得到正确格式化为 json 的属性。

【讨论】:

  • 你需要columnDefinition = "json"@JsonRawValue注解吗?
  • 它非常适合获取数据.. 但是我怎样才能发送新数据?
【解决方案4】:

很简单

@Column(name = "json_input", columnDefinition = "json")
private String field;

在 mysql 数据库中你的列 'json_input' json 类型

【讨论】:

    【解决方案5】:

    对于那些不想写太多代码的人来说,有一个解决方法。

    前端 -> 在 POST 方法中将您的 JSON 对象编码为字符串 base64,在 GET 方法中将其解码为 json

    In POST Method
    data.components = btoa(JSON.stringify(data.components));
    
    In GET
    data.components = JSON.parse(atob(data.components))
    

    Backend -> 在您的 JPA 代码中,将列更改为 String 或 BLOB,无需转换。

    @Column(name = "components", columnDefinition = "json")
    private String components;
    

    【讨论】:

      【解决方案6】:

      我遇到了类似的问题,并通过使用 @Externalizer 注释和 Jackson 来序列化/反序列化数据来解决它(@Externalizer 是 OpenJPA 特定的注释,因此您必须检查您的 JPA 实现类似的可能性)。

      @Persistent
      @Column(name = "params")
      @Externalizer("toJSON")
      private Params params;
      

      Params 类实现:

      public class Params {
          private static final ObjectMapper mapper = new ObjectMapper();
      
          private Map<String, Object> map;
      
          public Params () {
              this.map = new HashMap<String, Object>();
          }
      
          public Params (Params another) {
              this.map = new HashMap<String, Object>();
              this.map.putAll(anotherHolder.map);
          }
      
          public Params(String string) {
              try {
                  TypeReference<Map<String, Object>> typeRef = new TypeReference<Map<String, Object>>() {
                  };
                  if (string == null) {
                      this.map = new HashMap<String, Object>();
                  } else {
                      this.map = mapper.readValue(string, typeRef);
                  }
              } catch (IOException e) {
                  throw new PersistenceException(e);
              }
          }
      
          public String toJSON() throws PersistenceException {
              try {
                  return mapper.writeValueAsString(this.map);
              } catch (IOException e) {
                  throw new PersistenceException(e);
              }
          }
      
          public boolean containsKey(String key) {
              return this.map.containsKey(key);
          }
      
          // Hash map methods
          public Object get(String key) {
              return this.map.get(key);
          }
      
          public Object put(String key, Object value) {
              return this.map.put(key, value);
          }
      
          public void remove(String key) {
              this.map.remove(key);
          }
      
          public Object size() {
              return map.size();
          }
      }
      

      HTH

      【讨论】:

      • 感谢您的回答。我们使用“spring-data-jpa”。正如 maven 所说,它依赖于 org.eclipse.persistence 和 JPA 的 org.hibernate。是你记下的吗?
      • 不,我们使用的是基于 OpenJPA 的 JPA,而且这个注解是 OpenJPA 特有的。我认为 Hibernate 作为 JPA 提供者不提供这样的功能,只有当您将它用作普通 Hibernate (HQL) 而不是 JPA 时。而Spring Data JPA,顾名思义,就是使用JPA...
      【解决方案7】:

      我们为什么真正使用 JSON / BLOB?

      1. 当有许多字段并且正在更新时,我们可能希望更新特定字段并让其他字段保持不变。在这种情况下,目前,我们需要读取现有值,全部到客户端,然后与新的更改合并,然后更新所有值。相反,现在,json 合并补丁操作将执行所需的更新,而无需对其他人了解实际值。
      2. 当存在太多字段并且这些字段可以更改并且我们不希望对代码进行太多更改时,我们可以使用 json / blob。 在这里,服务器端,我们存储为 json / blob 并仅将索引字段保留为表列中的非 json 字段,以便搜索条件不需要涉及太多 json 验证函数调用。现在,我们从没想过,剩余的非索引列会被划分回连接表。 作为 json 对象的一部分,我们正在妥协将不经常更改的值的多个副本。当必须更改这些值时,我们宁愿使用数据库函数或控制器/服务级别函数来更新所有 json/blob,而不是进行更快的更新。
      3. 这将确保,当用户登录时,可以在没有表连接的情况下作为单选操作获得所有必需的详细信息,并且所有必需的信息都本地化以用于单盘访问,从而以指数方式提高速度并将其余的 json 操作留给客户端,因为我们可以预期浏览器大部分时间都处于空闲状态,并且可以在没有太多开销/等待用户的情况下承担这项工作。
      4. 像 Angular 这样的应用程序具有数据服务,通过一次读取来存储这些类型的数据,并且这些数据根据需要在所有视图组件中使用
      5. 因此,理想情况下,我们必须期待这一点,
      6. 服务器端,我们将拥有更少的表并避免加入表 和客户端,许多 json 对象和详细的工作。

      相反,如果您在服务器端使用过多的连接并再次使用 blob,则意味着设计不佳

      【讨论】:

        【解决方案8】:

        在这个较新版本的 Spring Boot 和 MySQL 中,下面的代码就足够了

        @Column( columnDefinition = "json" )
        private String string;
        

        我遇到了引号问题,所以我在我的项目中评论了下面一行 #spring.jpa.properties.hibernate.globally_quoted_identifiers=true

        【讨论】:

          猜你喜欢
          • 2019-05-27
          • 1970-01-01
          • 2013-04-17
          • 2013-01-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多