Jackson + Hibernate = many problems
So here is my situation: I want to create a simple CRUD webservice using Jackson and Hibernate. Seems like a perfect job for Spring Boot. So, we have the following:
(Note that I am compacting the code so it doesn't compile)
class Doctor {
@Id
long id;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "doctor_service", joinColumns = { @JoinColumn(name = "doctor_id", nullable = false) }, inverseJoinColumns = { @JoinColumn(name = "service_id", nullable = false) })
Set<Service> services;
}
class Service {
@Id
long id;
@ManyToMany(fetch = FetchType.EAGER, mappedBy = "services")
Set<Doctor> doctors;
}
Simple data model. And we have a simple requirement: on the web service, when we get the service objects, we must get the associated Doctors. And when we get Doctors, we must get related Services. We use lazy because [insert excuse here].
So now let's serve it up:
@Path("/list")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public JsonResponse<List<Doctor>> list() {
return JsonResponse.success(doctorCrudRepo.findAll());
}
Glossing over the JsonResponse object (now just a handy black box) and suggesting that doctorCrudRepo is a valid CrudRepository instance.
And a firestorm begins:
failed to lazily initialize a collection of role: Doctor.services, could not initialize proxy - no Session (through reference chain: ...)
Ok, so Lazy doesn't work. Simple enough. Just look forward to it.
Caused by: java.lang.StackOverflowError: null
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:655)
... 1011 common frames omitted
So let's see what other people said:
Contestant # 1 : The solutions are not relevant because they apply to one-to-many, not many, so I still get a StackOverflowError.
Member # 2 : Same as before, still one-to-many, still StackOverflow.
Participant # 3 : Same (no one ever used many-to-many ???)
Member # 4 : I cannot use @JsonIgnore because that means it will never be serialized. Thus, it does not meet the requirements.
Participant # 5 : At first glance, it seems to be working fine! However, only the doctor's endpoint works - she receives services. Service endpoint is down - Doctors are not getting it (empty set). It probably depends on which link defines the join table. This again does not fit the bill.
Participant # 6 : No.
Some other solutions that are wrong but deserve a mention:
-
Create a new set of json serialization objects that are not wrapped in hibernate and then copy the properties in the controller. This is a lot of extra work. Forcing this pattern will hurt the purpose of using Hibernate.
-
After loading the Doctor, loop each Service and set service.desctors to null to prevent further lazy loading. I'm trying to establish a set of best practices, not come up with hacky workarounds.
So ... what is the RIGHT solution? What kind of picture can I keep track of that looks clean and proud that I am using Hibernate and Jackson? Or is this combination of technologies so incompatible with proposing a new paradigm?
source to share
I found a solution that seems elegant.
-
Use OpenEntityManagerInViewFilter . It seems to be frowned upon (perhaps for security reasons, but I saw no compelling reason not to use it). It's easy to use, just define a bean:
@Component public class ViewSessionFilter extends OpenEntityManagerInViewFilter { }
-
Use LAZY for all links. This is what I wanted to start, and this is especially important since my data has many links and my services are small.
-
Use @JsonView . See this helpful article.
First find out what the views will be (one for doctors, one for patients)
public interface Views {
public static interface Public {}
public static interface Doctors extends Public {}
public static interface Services extends Public {}
}
In the Doctor view, you will see services.
@Entity
@Table(name = "doctor")
public class Doctor {
@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinTable(name = "doctor_service", joinColumns = { @JoinColumn(name = "doctor_id", nullable = false) },
inverseJoinColumns = { @JoinColumn(name = "service_id", nullable = false) })
@JsonView(Views.Doctors.class)
private Set<Service> services;
}
And when you look out of the Services window, you will see the doctors.
@Entity
@Table(name = "service")
public class Service {
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "services")
@JsonView(Views.Services.class)
private Set<Doctor> doctors;
}
Then assign the views to the service endpoints.
@Component
@Path("/doctor")
public class DoctorController {
@Autowired
DoctorCrudRepo doctorCrudRepo;
@Path("/list")
@POST
@Produces(MediaType.APPLICATION_JSON)
@JsonView(Views.Doctors.class)
public JsonResponse<List<Doctor>> list() {
return JsonResponse.success(OpsidUtils.iterableToList(doctorCrudRepo.findAll()));
}
}
Works great for a simple CRUD app. I even think it will scale well for larger and more complex applications. But it must be supported carefully.
source to share
First, with regard to your statement "... copy properties in the controller. That's a lot of overhead. Forcing this pattern universally defeats the purpose of using Hibernate.":
This does not defeat the purpose of using Hibernate. ORMs were created to eliminate the need to convert database strings received from JDBC to POJOs. The challenge of Hibernate lazy loading is to eliminate the redundant work of writing custom queries against the RDBMS when you don't need high performance or you can cache objects.
The problem is not with Hibernate & Jackson, but with the fact that you are trying to use the tool for any purpose, it was never meant to be.
I am assuming that your project is trending upward (they usually do everything). If this is true, you will have to separate the layers sometime and better sooner rather than later. Therefore, I suggest you stick to "Wrong Solution # 1" (create a DTO). You can use something like ModelMapper to prevent you from manually writing Entity transformation logic to DTO.
Also consider that without a DTO, your project can be difficult to maintain:
- The data model will evolve and you will always have to update your interface with changes.
- The data model can contain some fields that you can omit when submitting to your users (for example, the user's password field). You can always create additional entities, but they will require additional DAOs, etc.
- Someday you may need to return data to the user, which is the composition of some objects. You could write a new JPQL, for example
SELECT new ComplexObject(entity1, entity2, entity3) ...
, but that would be much more complicated than calling multiple service methods and composing the result in a DTO.
source to share