Search code examples
javahibernatelazy-initializationspark-java

Hibernate incompatible with SparkJava?


I have an error using Hibernate with SparkJava in the lazy-loading mode.

It is working correctly without SparkJava, but when using SparkJava it is trying to force eager-loading for a OneToMany relationship.


- Model

@Entity
@Table(name = "KU_SUPPLIER")
public class Supplier {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @NotEmpty(message = "Please provide a name")
    private String name;

    @OneToMany(mappedBy = "supplier")
    private List<Item> items;  // Should be lazy-loaded

    // Constructor / Getters / Setters
}


- DAO

public class SupplierDao implements Dao<Supplier> {

    private final SessionFactory sessionFactory;

    public SupplierDao(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Supplier> findAll() {
        try (Session session = sessionFactory.openSession()) {
            return session.createQuery("FROM com.seafrigousa.model.Supplier").getResultList();
        }
    }
}


- Main

// Working perfectly and lazy-load Items as desired    
supplierDao.findAll();

// The method will be called when a web browser goes to "localhost/suppliers"
// It throws org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: model.Supplier.items, could not initialize proxy - no Session
get("/suppliers", "application/json", supplierDao::findAll);


I checked by not closing the session from the DAO and I saw that Hibernate was executing the query as if it was in EAGER loading mode, so it was executing two selects, one for Supplier and one for Item.

Is there a reason for this behavior ?

Thank you!


Solution

  • I guess that here: get("/suppliers", "application/json", supplierDao::findAll); you are serializing Supplier object into json. Items field is not marked as excluded from serialization, so getting its value cause lazy initialization out of session (or redundand second query for items, if session is not closed).

    If my guess is correct, make your serializer ignore items field or fetch them in your query

    session.createQuery("FROM com.seafrigousa.model.Supplier s join fetch s.items").getResultList();
    

    Using gson as serializer you have following options:

    1. @Expose annotation on fields which you want to serialize.

      @Entity
      @Table(name = "KU_SUPPLIER")
      public class Supplier {
      
          @Expose
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private int id;
      
          @Expose
          @NotEmpty(message = "Please provide a name")
          private String name;
      
          @OneToMany(mappedBy = "supplier")
          private List<Item> items;  // Should be lazy-loaded
      
          // Constructor / Getters / Setters
      }
      

      With following gson initiation

      Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
      
    2. ExclusionStrategy with custom annotation f.e.

      public class IgnoreFieldExclusionStrategy implements ExclusionStrategy {
      
          @Override
          public boolean shouldSkipField(FieldAttributes fieldAttributes) {
              return fieldAttributes.getAnnotation(GsonIgnore.class) != null;
          }
      
          @Override
          public boolean shouldSkipClass(Class<?> aClass) {
              return false;
          }
      }
      

      with custom annotation @GsonIgnore

      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.FIELD)
      public @interface GsonIgnore {}
      

      and gson initiation

      Gson gson = new GsonBuilder().addSerializationExclusionStrategy(new IgnoreFieldExclusionStrategy()).create();
      

      your class would look like this

      @Entity
      @Table(name = "KU_SUPPLIER")
      public class Supplier {
      
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private int id;
      
          @NotEmpty(message = "Please provide a name")
          private String name;
      
          @GsonIgnore
          @OneToMany(mappedBy = "supplier")
          private List<Item> items;  // Should be lazy-loaded
      
          // Constructor / Getters / Setters
      }
      

    If you would have need serialize Supplier with items in different api, you can create DTO object for Supplier and map it from results like this:

    package com.seafrigousa.dto
    
    public class SupplierDTO {
    
        private int id;
        private String name;
    
        public SupplierDTO(int id, String name) {
            this.id = id;
            this.name = name;
       }
    
        // Getters / Setters
    }
    

    and query:

    session.createQuery("select new com.seafrigousa.dto.SupplierDTO(s.id, s.name) FROM com.seafrigousa.model.Supplier s").getResultList();