4

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:

  1. 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.

  2. 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?

Community
  • 1
  • 1
James Watkins
  • 4,806
  • 5
  • 32
  • 42
  • Have you tried Hibernate.initialize(service.getDoctors()) ? – Sarit Adhikari Jun 07 '15 at 03:41
  • Yes. That's one of the proposed solutions for Contestant #1. But each Doctor again has references to Services, so I get a slightly different error message but same root cause. – James Watkins Jun 07 '15 at 03:51
  • Post the actual code of your entities, not some derived part of it. Generally you must specify the `mappedBy` if you don't hibernate might get confused. Also you mention a stack overflow please add the stack trace to see where it goes wrong. In theory when you have your setup correctly eager loading should work, I suspect that your `@Transactional` is pretty much useless due to that fact it it a JAX-RS and not a spring bean. – M. Deinum Jun 07 '15 at 12:08
  • See my edits. If it's not a spring bean then why does @Autowired work? – James Watkins Jun 07 '15 at 12:17
  • Maybe [this](http://stackoverflow.com/a/4126846/2587435) – Paul Samsotha Jun 07 '15 at 12:27
  • That doesn't work either. The bi-directional references works by disabling serialization on one of the properties, which doesn't fit my requirements. The jackson-module-hibernate documentation is overall very lacking, and seems like magic. I'm not going to use anything if I don't understand how it helps me. – James Watkins Jun 07 '15 at 12:36

2 Answers2

2

Firstly, regarding your statement "...copy the properties over in the controller. This is a lot of extra work. Forcing this pattern everywhere defeats the purpose of using Hibernate.":

It doesn't defeat the purpose of using Hibernate. ORMs were created in order to eliminate necessity of converting database rows received from JDBC to POJOs. Hibernate's lazy-loading purpose is to eliminate redundant work on writing custom queries to RDBMS when you don't need great performance, or you are able to cache the entities.

The issue is not with Hibernate&Jackson, but with the fact that you are trying to use the instrument for a purpose, it was never designed for.

I guess that your project tends to grow (usually they all do). If that's true then you will have to separate layers someday, and better sooner than later. So I would suggest you to stick to the "wrong solution #1" (create a DTO). You can use something like ModelMapper to prevent hand-writing of Entity to DTO conversion logic.

Also consider that without DTOs your project may become hard to maintain:

  • Data model will evolve, and you will always have to update your front-end with changes.
  • Data model may contain some fields, that you may want to omit from sending to your users (like user's password field). You can always create additional entities, but they will require additional DAOs, etc.
  • Someday you may need to return user a data that is a composition of some entities. You can write a new JPQL, like SELECT new ComplexObject(entity1, entity2, entity3) ..., but that would be much harder than to call few service's methods and compose the result into DTO.
user3707125
  • 3,394
  • 14
  • 23
  • I don't like this solution because of the excess engineering involved. Now I have 2 different mapping logic, one for doctor with services, another for service with doctors, but there is common code shared in these. I will need to keep this mapping logic dry. Automatic mapping "based on conventions" sounds like a terrible idea, and would probably break with infinite loops of lazy-loaded references anyway. – James Watkins Jun 07 '15 at 11:56
2

I found a solution that seems to be elegant.

  1. Use OpenEntityManagerInViewFilter. Seems to be frowned upon (probably for security reasons, but I haven't seen any convincing reason to not use it). It's simple to use, just define a bean:

    @Component
    public class ViewSessionFilter extends OpenEntityManagerInViewFilter {
    }
    
  2. Use LAZY on all references. This is what I wanted to begin with, and it's especially important since my data has many references and my services are small.

  3. Use @JsonView. See this helpful article.

First, figure 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 {}
}

Looking from the Doctors view, you will see the 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 looking from the Services view, 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 perfectly for a simple CRUD app. I even think it will scale well to bigger, more complex apps. But it would need to be maintained carefully.

James Watkins
  • 4,806
  • 5
  • 32
  • 42