So here's my situation: I want to build a simple CRUD webservice using Jackson and Hibernate. Seems like a perfect job for Spring Boot. So we have the following:
(Please note that I am condensing the code so its not compile-able)
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;
}
A simple data model. And we have a simple requirement: on the webservice, when we get Service objects we should get the associated Doctors. And when we get the Doctors, we should get the associated Services. We are using lazy because [insert justification here].
So now lets serve it:
@Path("/list")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public JsonResponse<List<Doctor>> list() {
return JsonResponse.success(doctorCrudRepo.findAll());
}
Gloss over the JsonResponse object (just a convenience blackbox for now) and lets assume the doctorCrudRepo is a valid instance of CrudRepository.
And the firestorm begins:
failed to lazily initialize a collection of role: Doctor.services, could not initialize proxy - no Session (through reference chain: ...)
Ok so Lazy doesnt work then. Simple enough. Just make it eager.
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 have said:
Contestant #1: Solutions are not relevant because they apply to one-to-many, not many-to-many, so I still get the StackOverflowError.
Contestant #2: Same as before, still a one-to-many, still StackOverflow.
Contestant #3: Same (has nobody ever used many-to-many???)
Contestant #4: I can't use @JsonIgnore because that means it will never be serialized. So it doesn't fit the requirements.
Contestant #5: At first glance, it appears to work fine! However, only the Doctor endpoint works - it is getting the services. The Services endpoint doesn't work - its not getting the Doctors (empty set). It's probably based on which reference defines the join table. This again doesn't fit the bill.
Contestant #6: Nope.
Some other solutions which are wrong but worth mentioning:
Create a new set of objects for json serialization that are not wrapped by hibernate, and then copy the properties over in the controller. This is a lot of extra work. Forcing this pattern everywhere defeats the purpose of using Hibernate.
After loading the Doctor, loop over each Service and set the service.doctors to null, to prevent further lazy loading. I'm trying to establish a set of best practices, not to come up with hackish workarounds.
So... what's the RIGHT solution? What pattern can I follow that looks clean and makes me proud to use Hibernate and Jackson? Or is this combination of technology so incompatible to suggest a new paradigm?