【问题标题】:Jackson: referencing an object as a property杰克逊:将对象作为属性引用
【发布时间】:2017-06-18 19:08:48
【问题描述】:

在我的 java spring 应用程序中,我正在使用 hibernate 和 jpa,并且我使用 jackson 在 DB 中填充数据。

这是用户类:

@Data
@Entity
public class User{

    @Id
    @GeneratedValue
    Long id;

    String username;
    String password;
    boolean activated;

    public User(){}
}

第二类是:

@Entity
@Data
public class Roles {

    @Id
    @GeneratedValue
    Long id;

    @OneToOne
    User user;

    String role;

    public Roles(){}


}

在 Roles 类中,我有一个 User 属性 然后我制作了一个 json 文件来存储数据:

[ {"_class" : "com.example.domains.User", "id": 1, "username": "Admin", "password": "123Admin123","activated":true}
,
  {"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}]

不幸的是,当我运行它抱怨的应用程序时:

.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.example.domains.User: no int/Int-argument constructor/factory method to deserialize from Number value (1)
 at [Source: N/A; line: -1, column: -1] (through reference chain: com.example.domains.Roles["user"])

问题出在

{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}

当我删除上述行时,应用程序运行良好。

我认为,它抱怨是因为它无法创建用户实例。 那么,我该如何解决?

【问题讨论】:

  • 它直接说明了问题所在:它无法将整数 1 映射到具有“用户”类型的字段上。您应该在输入中使用用户对象而不是外键引用。或者做我喜欢的事情,编写一个包含您真正感兴趣的字段的 DTO,然后应用您需要的任何业务逻辑来使实体正常工作。
  • 我没听明白。你能写一个答案吗?
  • 从业务角度来看,一对一是否正确?
  • 我猜,问题出在您设置“id”的值,而它应该是自动生成的。

标签: java spring hibernate jpa jackson


【解决方案1】:

帮自己一个忙,停止使用您的实体作为 DTO!

JPA 实体具有双向关系,JSON 对象没有,我也相信实体的职责与 DTO 非常不同,虽然将这些职责合并到一个 Java 类中是可能的,但根据我的经验,它是一个非常糟糕的主意。

这里有几个原因

  • DTO 层几乎总是需要更大的灵活性,因为它通常与 UI 相关。
  • 您应该避免将数据库中的主键暴露给外部,包括您自己的 UI。我们总是为每个公开的实体生成一个额外的唯一 ID (UUID),主键保留在数据库中并且仅用于连接。
  • 您经常需要同一实体的多个视图。或多个实体的单一视图。
  • 如果您需要将新实体添加到与现有关系的关系中,则需要在数据库中找到现有实体,因此将新旧对象作为单个 JSON 结构发布没有任何优势。您只需要现有的 uniqueId,然后是新的。

开发人员在使用 JPA 时遇到的很多问题,特别是在合并方面,都来自于他们在 json 被反序列化后收到一个分离的实体这一事实。但是这个实体通常没有 OneToMany 关系(如果有的话,它是在 JSON 中与子级有关系的父级,但在 JPA 中,它是子级对构成关系的父级的引用)。在大多数情况下,您总是需要从数据库中加载实体的现有版本,然后将更改从 DTO 复制到实体中。

自 2009 年以来,我与 JPA 进行了广泛的合作,并且我知道分离和合并的大多数极端情况,并且使用实体作为 DTO 没有问题,但是我看到了当您处理此类时出现的混乱和错误类型代码交给不熟悉 JPA 的人。 DTO 所需的几行代码(尤其是因为您已经使用了 Lombok)非常简单,并且比尝试保存一些文件并破坏关注点分离更灵活。

【讨论】:

    【解决方案2】:

    Jackson 提供ObjectIdResolver 接口,用于在反序列化过程中从 id 中解析对象。

    在您的情况下,您希望根据 JPA/hibernate 解析 id。因此,您需要通过调用 JPA/hierbate 实体管理器来实现自定义解析器来解析 id。

    下面是高层次的步骤:

    1. 实现一个自定义ObjectIdResolverJPAEntityResolver(你可以从SimpleObjectIdResolver 扩展)。在解析对象期间,它将调用 JPA 实体管理器类以通过给定的 id 和范围查找实体(参见 ObjectIdResolver#resolveId java docs)

      //Example only;
      @Component
      @Scope("prototype") // must not be a singleton component as it has state
      public class JPAEntityResolver extends SimpleObjectIdResolver {
      //This would be JPA based object repository or you can EntityManager instance directly.
      private PersistentObjectRepository objectRepository;
      
      @Autowired
      public JPAEntityResolver (PersistentObjectRepository objectRepository) {
          this.objectRepository = objectRepository;
      }
      
      @Override
      public void bindItem(IdKey id, Object pojo) {
          super.bindItem(id, pojo);
      }
      
      @Override
      public Object resolveId(IdKey id) {
          Object resolved = super.resolveId(id);
          if (resolved == null) {
              resolved = _tryToLoadFromSource(id);
              bindItem(id, resolved);
          }
      
          return resolved;
      }
      
      private Object _tryToLoadFromSource(IdKey idKey) {
          requireNonNull(idKey.scope, "global scope does not supported");
      
          String id = (String) idKey.key;
          Class<?> poType = idKey.scope;
      
          return objectRepository.getById(id, poType);
      }
      
      @Override
      public ObjectIdResolver newForDeserialization(Object context) {
          return new JPAEntityResolver(objectRepository);
      }
      
      @Override
      public boolean canUseFor(ObjectIdResolver resolverType) {
          return resolverType.getClass() == JPAEntityResolver.class;
      }
      

      }

    2. 通过使用注释 JsonIdentityInfo(resolver = JPAEntityResolver.class) 告诉 Jackson 为类使用自定义 id 解析器。例如

      @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
                    property = "id",
                    scope = User.class,
                    resolver = JPAObjectIdResolver.class)
      public class User { ... }
      
    3. JPAObjectIdResolver 是一个自定义实现,将依赖于 Jackson 可能不知道的其他资源(JPA 实体管理器)。所以杰克逊需要帮助来实例化解析器对象。为此,您需要为ObjectMapper 实例提供自定义HandlerInstantiator。 (就我而言,我使用的是 spring,所以我要求 spring 通过使用自动装配来创建 JPAObjectIdResolver 的实例)

    4. 现在反序列化应该可以正常工作了。

    希望这会有所帮助。

    【讨论】:

    • 这很复杂,但确实有效。我发现步骤 3 需要 stackoverflow.com/a/44070717/204295 步骤 2 和 3 来实现,并且您还需要使用 @JsonProperty 使用您从 Web 表单提供的键来注释父对象与子对象的关系。
    【解决方案3】:

    我已将 json 文件更改为:

    [
      {"_class" : "com.example.domains.User",
       "id": 1,
       "username": "Admin",
       "password": "123Admin123",
       "activated":true
      },
      {
        "_class" : "com.example.domains.Roles",
        "id": 1,
        "user":{"_class" : "com.example.domains.User",
                "id": 1,
                "username": "Admin",
                "password": "123Admin123",
                "activated":true
              },
        "role": "Admin"
      }
    ]
    

    但我仍然认为,最好的方法是使用外键来记录用户。 欢迎任何解决方案

    【讨论】:

      【解决方案4】:

      如果您的 bean 没有严格遵守 JavaBeans 格式,Jackson 就会遇到困难。

      最好为您的 JSON 模型 bean 创建一个显式的 @JsonCreator 构造函数,例如

      class User {
          ...
      
         @JsonCreator
         public User(@JsonProperty("name") String name, 
                     @JsonProperty("age") int age) {
                  this.name = name;
                  this.age = age;
         }
      
         ..
      }
      

      【讨论】:

      • 你确定它应该在User,但Roles
      【解决方案5】:

      字段的1-1映射效果很好,但是当涉及到复杂的对象映射时,最好使用一些API。 您可以使用 Dozer Mapping 或 Mapstruct 来映射 Object 实例。 推土机也有弹簧集成。

      【讨论】:

        【解决方案6】:

        您可以指定非默认构造函数,然后使用自定义反序列化器。

        这样的东西应该可以工作(尚未经过测试)。

        public class RolesDeserializer extends StdDeserializer<Roles> { 
        
            public RolesDeserializer() { 
                this(null); 
            } 
        
            public RolesDeserializer(Class<?> c) { 
                super(c); 
            }
        
            @Override
            public Roles deserialize(JsonParser jp, DeserializationContext dsctxt) 
              throws IOException, JsonProcessingException {
                JsonNode node = jp.getCodec().readTree(jp);
                long id = ((LongNode) node.get("id")).longValue();
                String roleName = node.get("role").asText();
        
                long userId = ((LongNode) node.get("user")).longValue();
        
                //Based on the userId you need to search the user and build the user object properly
                User user = new User(userId, ....);
        
                return new Roles(id, roleName, user);
            }
        }
        

        然后你需要注册你的新反序列化器(1)或者使用@JsonDeserialize注解(2)

        (1)

        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Item.class, new RolesDeserializer());
        mapper.registerModule(module);
        
        Roles deserializedRol = mapper.readValue(yourjson, Roles.class);
        

        (2)

        @JsonDeserialize(using = RolesDeserializer.class)
        @Entity
        @Data
        public class Roles {
            ...
        }
        
        Roles deserializedRol = new ObjectMapper().readValue(yourjson, Roles.class);
        

        【讨论】:

          【解决方案7】:
          public class Roles {
          
              @Id
              @GeneratedValue
              Long id;
          
              @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
              @JsonIdentityReference(alwaysAsId = true)
              @OneToOne
              User user;
          
              String role;
          
              public Roles(){}
          
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-12-20
            • 1970-01-01
            • 2017-09-03
            • 1970-01-01
            • 1970-01-01
            • 2013-10-05
            • 2021-04-12
            • 2012-01-06
            相关资源
            最近更新 更多