Grim
Grim

Reputation: 2040

How to fetch a field lazily with Hibernate Criteria

Each row of the table Person (having name, firstname and age) shall be read.

EntityManager em = emf.createEntityManager();
Session s = (Session) em.getDelegate();
Criteria criteria = s.createCriteria(Person.class);
criteria.setFetchMode("age", FetchMode.SELECT);

But the SQL shows

Hibernate:
    select
        person0_.name,
        person0_.firstname,
        person0_.age
    from 
        SCOPE.PERSON person0_

How to let the age be lazy ONLY for the Criteria??

Upvotes: 15

Views: 10039

Answers (6)

Rusli Usman
Rusli Usman

Reputation: 117

I would like to add (or maybe clarify) the followings. Given the main class (Settlement) with an attribute class (Customer):

@Entity
@Table(name = "settlement") 
public class Settlement extends IdBasedObject {

...

    @OneToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "customer_id_fk")
    private Customer customer;
}

@Entity
@Table(name = "customer", schema = SchemaUtil.SCHEMA_COMMON)
public class Customer extends IdBasedObject {
    @Column(name = "organization_type")
    @Enumerated(EnumType.ORDINAL)
    private CompanyType companyType;

    @Column(name = "organization_legal_name")
    private String companyLegalName;

    ...
}

If you would like to get all the distinct customers from the Settlement, you would use the Projections distinct on the 'customer' property and followed by creating an alias from the Settlement class:

    public List<Customer> findUniqueCustomer() throws Exception {
        Session session = super.getSessionFactory().openSession();

        ProjectionList projectionList = Projections.projectionList();
        projectionList.add(Projections.distinct(Projections.property("customer")));

        Criteria criteria = session.createCriteria(Settlement.class);
        criteria.setProjection(projectionList);
        criteria.createAlias("customer", "customer");

return criteria.list();
}

Now, if you do that, you will get back a list of non-proxy error 'could not initialize proxy - no Session' for each of the customer object.

Fortunately, criteria provides the setResultTransformer function that will 're-shape' the return.

        criteria.setResultTransformer(new ResultTransformer() {
            
            @Override
            public Object transformTuple(Object[] tuple, String[] aliases) {
                Customer customerObject = (Customer) tuple[0];
                
                Customer customer = new Customer();
                customer.setId(customerObject.getId());
                customer.setVersion(customerObject.getVersion());
                customer.setCompanyType(customerObject.getCompanyType());
                 
             customer.setCompanyLegalName(customerObject.getCompanyLegalName());
             return customer;
             ...
          }

            @SuppressWarnings("rawtypes")
            @Override
            public List<Customer> transformList(List collection) {

                return collection;
            }
});

The tuple[0] essentially contains the customer object value, since the customer object is not proxied, you will get the error. In the transformTuple function, you have a chance to 're-create' each of the customer object thereby avoiding the 'non-proxied' error.

Please give a try.

Upvotes: 0

Vlad Mihalcea
Vlad Mihalcea

Reputation: 154090

You can simply define a new entity SimplePerson mapped to the same persons database table which contains only the following attributes:

  • id
  • name
  • firstName

This way, when selecting a SimplePerson with both Criteria and HQL, the age column will not be retrieved.

Another alternative is to use lazy loading for basic attributes, but mapping multiple subentities to the same database table is much more flexible.

Upvotes: 4

Dragan Bozanovic
Dragan Bozanovic

Reputation: 23562

Your reasoning is valid (in general; we can however argue about the specific example of the age field), but unfortunately there is no straight-forward solution for this. Actually, Hibernate has the concept of fetch profiles, but it is currently very limited (you can override the default fetch plan/strategy only with the join-style fetch profiles).

So, the possible workaround to your issue could be as follows.

1) Move age to a separate entity and associate the Person entity with it with a lazy one-to-one relationship:

@Entity
class PersonAge {
   private Integer age;
}

@Entity
class Person {
   @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, optional = false)
   @JoinColumn(name = "PERSON_AGE_ID")
   private PersonAge personAge;

   public Integer getAge() {
      return personAge.getAge();
   }

   public void setAge(Integer age) {
      personAge.setAge(age);
   }
}

2) Define a fetch profile which overrides the default one:

@FetchProfile(name = "person-with-age", fetchOverrides = {
   @FetchProfile.FetchOverride(entity = Person.class, association = "personAge", mode = FetchMode.JOIN)
})

3) Enable this profile for each session in the application:

session.enableFetchProfile("person-with-age");

Depending on the framework you use, there should be an easy hook/interceptor which you will use to enable the profile for each session (transaction) that is craeted. For example, an approach in Spring could be to override AbstractPlatformTransactionManager.doBegin of the transaction manager in use.

This way the personAge will be eagerly loaded in all the sessions in the application, unless the fetch profile is explicitly disabled.

4) Disable the fetch profile in the session in which you use the desired Criteria query:

session.disableFetchProfile("person-with-age");

This way the default fetch plan/strategy is used (specified in the entity mappings), which is the lazy loading of the PersonAge.

Upvotes: 4

Mathias Begert
Mathias Begert

Reputation: 2480

Setting the FetchMode of the "age" property on a criteria has no effect because the fetching strategy at this point is for associated objects only but not for properties. See section 20.1. Fetching strategies of the hibernate docs.

Hibernate uses a fetching strategy to retrieve associated objects if the application needs to navigate the association. Fetch strategies can be declared in the O/R mapping metadata, or over-ridden by a particular HQL or Criteria query.

The only way for lazy loading of a property is the @Basic annotation set to FetchType.LAZY. See here, or if you use .hbm.xml files for mapping use lazy=true, see this section of the hibernate docs.

The @Basic annotation allows you to declare the fetching strategy for a property. If set to LAZY, specifies that this property should be fetched lazily when the instance variable is first accessed. It requires build-time bytecode instrumentation, if your classes are not instrumented, property level lazy loading is silently ignored.

Lazy loading of properties also use buildtime bytecode instumentation (hibernate is changing the entity classes after compilation to allow lazy loading of properties). Read 20.1.8. Using lazy property fetching

An other possible solution (except for all the other solutions) to your problem is to make a simpler Person class and use a constructor query like:

public class PersonDTO {
    private String name;
    private String firstname;

    private Person(String name, String firstname) {
        this.name = name;
        this.firstname = firstname;
    }
    // getters & setters
}

Query q = session.createQuery("select new your.package.name.PersonDTO("
    + "p.name, p.firstname) from Person p");
q.list();

You could even use your existing Person class, just extend it with an appropriate constructor, but I would prefer explicitness.

But all the solutions presented here do not implement a lazy loading of the age attribute. The only way to do this is the @Basicannotation, or you have to implement your own lazy loading.

Upvotes: 9

gabrielgiussi
gabrielgiussi

Reputation: 9585

If your age is an object like the PersonAge of @Dragan you could associate the fecth mode with the criteria rather than the entity like you do.

So, I think you have three options:

  1. age as primitive and projection like @Paco says (Person.age will be null and not a Proxy, you lose the lazyness that you want)
  2. age as primitive without projection (more bytes in the wire)
  3. age as PersonAge + criteria.setFetchMode (you will get the lazyness that you want at the cost of an extra object/table/mapping)

For Projection you could use ResultTransformer to

Criteria crit = session.createCriteria(Person.class);
ProjectionList projList = Projections.projectionList();
projList.add(Projections.property("name"));
projList.add(Projections.property("firstname"));
crit.setProjection(projList);
crit.setResultTransformer(new ResultTransformer() {

      @Override
      public Object transformTuple(Object[] tuple, String[] aliases) {
        String name = (Long) tuple[0];
        String firstName = (String) tuple[1];
        return new Person(name , firstName);
      }

      @Override
      public List<Reference> transformList(List collection) {
        return collection;
      }
    });

I think you could create a PersonProxy on your own that triggers a query for retrieve the age but this is kind of awful.

  @Override
  public Object transformTuple(Object[] tuple, String[] aliases) {
    String name = (Long) tuple[0];
    String firstName = (String) tuple[1];
    return new PersonProxy(name , firstName);
  }

  class PersonProxy {
    Person realPerson;

    public getAge(){
       // create a query with realPerson.id for retrieve the age. 
    }
  }

Upvotes: 4

Paco Abato
Paco Abato

Reputation: 4065

I think that lazy mode only makes sense with associations. If you are accessing a plain table it will load all the fields.

If you want the age field not to appear in the SQL and so not being loaded into memory then use projections:

Criteria crit = session.createCriteria(Person.class);
ProjectionList projList = Projections.projectionList();
projList.add(Projections.property("name"));
projList.add(Projections.property("firstname"));
crit.setProjection(projList);

Upvotes: 11

Related Questions