在我的java spring应用程序中,我正在使用hibernate和jpa,并且我使用jackson在DB中填充数据。
这是User类:
@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"}
当我删除上面的行时,应用程序运行良好。
我认为,它抱怨因为它不能成为用户的实例。
所以, 我该怎么办呢?
帮自己一个忙,停止使用您的实体作为DTO!
JPA实体具有双向关系,JSON对象没有,我也相信实体的职责与DTO非常不同,虽然将这些职责加入单个Java类是可能的,但根据我的经验,这是一个非常糟糕的主意。
这有几个原因
- 您几乎总是需要在DTO层中具有更大的灵活性,因为它通常与UI相关。
- 您应该避免将主键从数据库暴露给外部,包括您自己的UI。我们总是为每个公开的实体生成一个额外的uniqueId(UUID),主键保留在DB中,仅用于连接。
- 您经常需要同一实体的多个视图。或者是多个实体的单个视图。
- 如果需要将新实体添加到与现有关系的关系中,则需要在数据库中找到现有实体,因此将新旧对象作为单个JSON结构发布没有任何优势。你只需要现有的uniqueId,然后是new。
开发人员对JPA的许多问题,特别是关于合并的问题来自于他们在json被反序列化后收到一个独立实体的事实。但是这个实体通常没有OneToMany关系(如果是这样,它是在JSON中与子关系有关系的父关系,但在JPA中,它是孩子对构成关系的父对象的引用)。在大多数情况下,您始终需要从数据库加载实体的现有版本,然后将更改从DTO复制到实体中。
自2009年以来,我与JPA进行了广泛的合作,我知道大多数分离和合并的角落情况,并且使用实体作为DTO没有问题,但我已经看到了将这些代码交给的时候出现的混乱和错误类型有些人并不熟悉JPA。 DTO所需的几行(特别是因为你已经使用了Lombok)非常简单,并且比试图保存一些文件和打破关注点分离更加灵活。
杰克逊提供 ObjectIdResolver 用于在反序列化期间从ID解析对象的接口。
在您的情况下,您想要根据JPA / hibernate解析id。因此,您需要通过调用JPA / hierbate实体管理器来实现自定义解析程序来解析id。
以下是高级别的步骤:
实现自定义 ObjectIdResolver
说 JPAEntityResolver
(你可以延伸 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;
}
}
通过使用注释JsonIdentityInfo告诉Jackson为类使用自定义id解析器(分解器 = JPAEntityResolver.class)。对于例如
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = User.class,
resolver = JPAObjectIdResolver.class)
public class User { ... }
JPAObjectIdResolver是一个自定义实现,将依赖于Jackson可能不知道的其他资源(JPA实体管理器)。所以杰克逊需要帮助来实例化解析器对象。为此,您需要提供自定义 HandlerInstantiator 至 ObjectMapper
实例。 (在我的情况下,我使用弹簧,所以我问春天创建的实例 JPAObjectIdResolver
通过使用自动装配)
- 现在反序列化应该按预期工作。
希望这可以帮助。
我已将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"
}
]
但我仍然认为,最好的方法是使用外键来记录用户。
欢迎任何解决方案
如果你的bean没有严格遵守JavaBeans格式,杰克逊就有困难。
最好为您的JSON模型bean创建一个显式的@JsonCreator构造函数,例如
class User {
...
@JsonCreator
public User(@JsonProperty("name") String name,
@JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
..
}
1-1字段映射效果很好,但是当涉及到复杂的对象映射时,最好使用一些API。
您可以使用Dozer Mapping或Mapstruct映射Object实例。
推土机也有弹簧整合。
您可以指定非默认构造函数,然后使用自定义反序列化器。
这样的东西应该工作(它还没有经过测试)。
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);
}
}
然后你需要注册你的新deserialiser(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);