In my java spring application, I am working with hibernate and jpa, and i use jackson to populate data in DB.
Here is the User class:
@Data @Entity public class User{ @Id @GeneratedValue Long id; String username; String password; boolean activated; public User(){} }
and the second class is:
@Entity @Data public class Roles { @Id @GeneratedValue Long id; @OneToOne User user; String role; public Roles(){} }
In the class Roles i have a property of User and then i made a json file to store the data:
[ {"_class" : "com.example.domains.User", "id": 1, "username": "Admin", "password": "123Admin123","activated":true} , {"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}]
Unfortunately, when i run the app it complains with:
.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"])
The problem comes from
{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}
and when i remove the above line the app works well.
I think, it complains because it cannot make an instance of user. So, how can i fix it?
6 Answers
Answers 1
Do yourself a favor and stop using your Entities as DTOs!
JPA entities have bidirectional relations, JSON objects don't, I also believe that the responsibilities of an Entity is very different from a DTO, and although joining these responsibilities into a single Java class is possible, in my experience it is a very bad idea.
Here are a couple of reasons
- You almost always need more flexibility in the DTO layer, because it is often related to a UI.
- You should avoid exposing primary keys from your database to the outside, including your own UI. We always generate an additional uniqueId (UUID) for every publicly exposed Entity, the primary key stays in the DB and is only used for joins.
- You often need multiple views of the same Entity. Or a single view of multiple entities.
- If you need to add a new entity to a relation with an existing, you will need find the existing one in the database, so posting the new and old object as a single JSON structure has no advantage. You just need the uniqueId of the existing, and then new.
A lot of the problems developers have with JPA, specifically with regards to merging comes from the fact that they receive a detached entity after their json has been deserialized. But this entity typically doesn't have the OneToMany relations (and if it does, it's the parent which has a relation to the child in JSON, but in JPA it is the child's reference to the parent which constitutes the relationship). In most cases you will always need to load the existing version of the entity from the database, and then copy the changes from your DTO into the entity.
I have worked extensively with JPA since 2009, and I know most corner cases of detachment and merging, and have no problem using an Entity as a DTO, but I have seen the confusion and types of errors that occur when you hand such code over to some one who is not intimately familiar with JPA. The few lines you need for a DTO (especially since you already use Lombok), are so simple and allows you much more flexibility, than trying to save a few files and breaking the separation of concerns.
Answers 2
I have changed the json file to :
[ {"_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" } ]
But i still think, the best ways is using a foreign key to user record. Any solution is welcomed
Answers 3
If your bean doesn't strictly adhere to the JavaBeans format, Jackson has difficulties.
It's best to create an explicit @JsonCreator constructor for your JSON model bean, e.g.
class User { ... @JsonCreator public User(@JsonProperty("name") String name, @JsonProperty("age") int age) { this.name = name; this.age = age; } .. }
Answers 4
1-1 mapping of fields works well , but when it comes to complex object mapping , better to use some API. You can use Dozer Mapping or Mapstruct to map Object instances. Dozer has spring integration also.
Answers 5
You could specify non default constructors and then use a custom deserialiser.
Something like this should work (it has not been tested).
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); } }
Then you need to register your new deserialiser (1) or use the @JsonDeserialize annotation (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);
Answers 6
Jackson provide ObjectIdResolver interface for resolving the objects from ids during de-serialization.
In your case you want to resolve the id based from the JPA/hibernate. So you need to implement a custom resolver to resolve id by calling the JPA/hierbate entity manager.
At high level below are the steps:
Implement a custom
ObjectIdResolver
sayJPAEntityResolver
(you may extends fromSimpleObjectIdResolver
). During resolving object it will call JPA entity manager class to find entity by given id and scope(see. 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; }
}
Tell Jackson to use a custom id resolver for a class, by using annotation JsonIdentityInfo(resolver = JPAEntityResolver.class). For e.g.
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", scope = User.class, resolver = JPAObjectIdResolver.class) public class User { ... }
JPAObjectIdResolver is a custom implementation and will have dependency on other resources( JPA Entity Manager) which might not be known to Jackson. So Jackson need help to instantiate resolver object. For this purpose, you need to supply a custom HandlerInstantiator to
ObjectMapper
instance. (In my case I was using spring so I asked spring to create instance ofJPAObjectIdResolver
by using autowiring)- Now de-serialization should work as expected.
Hope this helps.
0 comments:
Post a Comment