30

I have a problem with EAGERs relationships in a big application. Some entities in this application have EAGER associations with other entities. This become "poison" in some functionalities.

Now my team needs to optimize this functionalities, but we cannot change the fetch type to LAZY, because we would need to refactor the whole application.

So, my question: Is there a way to do a specific query ignoring the EAGERs associations in my returned entity?

Example: when a I have this entity Person, I would like to not bring the address list when I do a query to find a Person.

@Entity
public class Person {

  @Column
  private String name;

  @OneToMany(fetch=FetchType.EAGER)
  private List<String> address;

}

Query query = EntityManager.createQuery("FROM Person person");
//list of person without the address list! But how???
List<Person> resultList = query.getResultList();

Thanks!

Updated

The only way I found is not returning the entity, returning only some fields of the entity. But I would like to find a solution that I can return the entity (in my example, the Person entity).

I'm thinking if is possible to map the same table twice in Hibernate. In this way, I can mapping the same table without the EAGER associations. This will help me in a few cases...

Dherik
  • 13,091
  • 10
  • 86
  • 132
  • http://stackoverflow.com/questions/10997321/how-to-override-fetchtype-eager-to-be-lazy-at-runtime?lq=1 – K.Nicholas Jan 04 '16 at 04:22
  • 1
    Maybe you are better off refactoring. Typically, EAGER would only be used for trivial relationships, if that. Better to have everything LAZY and then get the extra when you actually know you need it. – K.Nicholas Jan 04 '16 at 04:23
  • Hi @Nicholas! It's the better solution. Sadly, it is not possible refactor all the entities because there a lot of impact in the application. – Dherik Jan 04 '16 at 11:04

7 Answers7

13

If you are using JPA 2.1 (Hibernate 4.3+) you can achieve what you want with @NamedEntityGraph.

Basically, you would annotate your entity like this:

@Entity
@NamedEntityGraph(name = "Persons.noAddress")
public class Person {

  @Column
  private String name;

  @OneToMany(fetch=FetchType.EAGER)
  private List<String> address;

}

And then use the hints to fetch Person without address, like this:

EntityGraph graph = this.em.getEntityGraph("Persons.noAddress");

Map hints = new HashMap();
hints.put("javax.persistence.fetchgraph", graph);

return this.em.findAll(Person.class, hints);

More on the subject can be found here.

When you use the fetch graph only fields that you have put inside @NamedEntityGraph will be fetched eagerly.

All your existing queries that are executed without the hint will remain the same.

Milan
  • 1,272
  • 1
  • 10
  • 25
  • 5
    In practise this does not work, Hibernate will still fetch attributes marked as EAGER even if you don't include them in your EntityGraph, see: https://hibernate.atlassian.net/browse/HHH-8776 This answer can't have been tested because it doesn't work as intended. – Robert Hunt Oct 19 '17 at 09:46
  • Hibernate now correctly implements EntityGraphs in 5.4.22 (see https://hibernate.atlassian.net/browse/HHH-8776?focusedCommentId=106092 and https://hibernate.atlassian.net/browse/HHH-8776?focusedCommentId=107330). I have tested it succesfully with 5.4.27, it did not load fields that were statically marked as EAGER. – Matze Jan 21 '21 at 14:32
8

Update (09/06/2020):

The issue was resolved on the 5.4.11 version. I can't test right now, but is expected that the JPA entity-graphs attributes not included in the graph should stay unloaded, even if they are declared EAGER.

Original answer

After all this years, override the EAGER mapping is not yet possible on Hibernate. From the latest Hibernate documentation (5.3.10.Final):

Although the JPA standard specifies that you can override an EAGER fetching association at runtime using the javax.persistence.fetchgraph hint, currently, Hibernate does not implement this feature, so EAGER associations cannot be fetched lazily. For more info, check out the HHH-8776 Jira issue.

When executing a JPQL query, if an EAGER association is omitted, Hibernate will issue a secondary select for every association needed to be fetched eagerly, which can lead dto N+1 query issues.

For this reason, it’s better to use LAZY associations, and only fetch them eagerly on a per-query basis.

And:

The EAGER fetching strategy cannot be overwritten on a per query basis, so the association is always going to be retrieved even if you don’t need it. More, if you forget to JOIN FETCH an EAGER association in a JPQL query, Hibernate will initialize it with a secondary statement, which in turn can lead to N+1 query issues.

Dherik
  • 13,091
  • 10
  • 86
  • 132
3

By default, Hibernate's HQL, Criteria and NativeSQL gives us the flexibility to EAGERly load a collection if it is mapped as LAZY in the domain model.

Regarding the other way round, ie., mapping the collection as as EAGER in the domain model and try to do LAZY load using HQL, Criteria or NativeSQL, I couldn't find a straight forward or simpler way in which we can meet this with HQL/Criteria/NativeSQL.

Although we have FetchMode.LAZY that we can set on Criteria,it is deprecated and it is equivalent to FetchMode.SELECT. And effectively FetchMode.LAZY actually results in firing an extra SELECT query and still eagerly loads the collection.

But, if we want to LAZY load a collection that is mapped as EAGER, you can try this solution: Make the HQL/Criteria/NativeSQL to return the scalar values and use a ResultTransformer(Transformers.aliasToBean(..)) to return the entity object (or DTO) with fields populated from scalar values.

In my scenario, I have a Forest entity that has a collection of Tree entities with oneToMany mapping of FetchType.EAGER and FetchMode.JOIN. To load only the Forest entity without loading any trees, I have used the following HQL query with scalar values and Transformers.aliasToBean(...). This works with Criteria and Native SQL as well as long as scalars and aliasToBean Transformer is used.

Forest forest = (Forest) session.createQuery("select f.id as id, f.name as name, f.version as version from Forest as f where f.id=:forest").setParameter("forest", i).setResultTransformer(Transformers.aliasToBean(Forest.class)).uniqueResult();

I have tested for above simple query and it might be working checking if this works for complex cases as well and fits all your use cases.

Would be keen to know if there is a better or simpler way of doing it especially without scalars and Transformers.

2

Never actually tried this but might worth a shot... Assuming that the session factory is available at the DAO layer via injection or any other means you can implement something similar in a (probably new) DAO method:

List<Person> result = (List<Person>) sessionFactory.getCurrentSession()
        .createCriteria(Person.class)
        .setFetchMode("address", FetchMode.LAZY)
        .list();
return result;
Attila T
  • 404
  • 3
  • 11
  • 5
    As per hibernate docs, `FetchMode.LAZY` is deprecated and it is equivalent to `FetchMode.SELECT`. So `FetchMode.LAZY` actually results in firing an extra SELECT query to eagerly load the collection. This doesn't really load the collection LAZYily. – Madhusudana Reddy Sunnapu Dec 31 '15 at 10:33
2
  1. Yes, you can map two entity classes to the same table and that is a valid workaround. However, beware the situations in which instances of both types are simultaneously present in the same persistence context, because updates of an entity instance of one type are not reflected to the same instance of the other type. Also, second-level caching of such entities gets more complicated.
  2. Fetch profiles are also interesting, but are very limited at the time being and you can override the default fetch plan/strategy only with the join-style fetch profiles (you can make a lazy association eager, but not vice versa). However, you could use this trick to invert that behaviour: Make the association lazy by default and enable the profile by default for all sessions/transactions. Then disable the profile in transactions in which you want lazy loading.
Community
  • 1
  • 1
Dragan Bozanovic
  • 21,631
  • 4
  • 36
  • 100
1

You didn't say why you couldn't change from eager to lazy. I, however, got the impression that it was for performance reasons and so I want to question this assumption. If that is the case, please consider the following. I realize this answer does not strictly answer your question and it violate the condition of no lazy loads, but here is an alternative that reflects my dev team's approach to what I think is the same underlying issue.

Set the fetch type to lazy, but then set a @BatchSize annotation. Since hibernate generally uses a separate DB query to load a collection, this maintains that behavior but with a tuned BatchSize, you avoid 1 query per element (while in a loop, for example) - provided your session is still open.

The behavior for the backref of a OneToOne relationship gets a little funny (the referenced side of the relationship - the side without the foreign key). But for the other side of the OneToOne, for OneToMany and ManyToOne, this gives the end result that I think you probably want: you only query the tables if you actually need them but you avoid a lazy load per record, and you don't have to explicitly configure each use case. This means that your performance will remain comparable in the event that you execute the lazy load, but this load does not happen if you do not actually need it.

Nathan
  • 1,437
  • 8
  • 18
  • it's for perfomance! We have a lot of entity with EAGERs. Your solution not directly answer but is a very good alternative for resolve the performance problem in some cases where the EAGER is everywhere, like my case. – Dherik Jan 06 '16 at 19:49
0

Firstly you can not override FetchType.EAGER. But you can use a different way.

Create a new extra PersonVo class.

public class PersonVo {

private String name;

public PersonVo(String name){
   this.name = name;
}

// getter and setter

}

After you can write like the below query in your JPA repository interface. This query will return your PersonVo class without an address. com.bla.bla.PersonVo is your PersonVo class package path.

@Query("SELECT NEW com.bla.bla.PersonVo(p.name) from Person p where p.id in :idList")
List<PersonVo> findAllById(@Param("idList") List<Long> idList);

When I tried this way, it was worked.