The LazyInitializationException
Do you know HibernatesLazyInitializationException? It's one of the annoying parts of the object-relational mapper. So, when does Hibernate throw this exception? Think of an entity A with a one-to-many relation to an entity B. Per default this relation is marked as lazy. What does this mean? If you load objects of type A the relational objects of type B will not be loaded. Instead Hibernate uses his own Collection implementations (e.g. PersistentList). Internally there is a Hibernate session bound to these collections. This allows Hibernate to load the collection of B objects at the first time you're accessing the collection. This works perfectly well as long as the Hibernate session bound to the collection is open. Per default the session closes automatically on the transactions commit. As a consequence a LazyInitializationException will be thrown if you try to access the collection after the transaction has been commited. Hibernate objects not bound to an active session are called detached objects.The N+1 SELECT Problem
You may ask yourself: What if I never use detached objects? Indeed this is a possible solution to prevent LazyInitalizationExceptions to be thrown. But another problem arises: when you first access a non-initialized (lazy) collection Hibernate loads the objects by querying the database with additional SELECT statements. Think of a complex object graph where entity A knows many B entities which knows many C entities and so on. As you can imagine maaaaany SELECTS are fired while traversing this object graph (from A to C, back and forth). This leads to performance issues and is called the N+1 SELECT problem.The Preload Pattern
So what might be the simpliest solution to prevent both LazyInitializationExceptions on detached objects and the N+1 SELECT problem? You're right: we have to minimize the use of lazy initialized collections. And we want to do this on a per-usecase basis so we can individually decide for each usecase which data will be preloaded.Let me introduce to you the so called
CriteriaJoiner. The class allows you to easily specify which paths of an object graph you want to preload. Alternatively the class creates a Criteria or DetachedCriteria object automatically. Internally the created criteria uses LEFT JOINs to preload the demanded object graph with just one SELECT statement. It's up to you to modify the created criteria by adding additional restrictions.The Usage of CriteriaJoiner
The CriteriaJoiner will be instantiated for a given mapped hibernate class using the appropriate static methods. Then you can specify which part of the object graph you want to preload. This is done by adding additional paths based on the given root class. What does path mean in this context? A path is the concatenation of one or more collection member names separated by a slash, forming together a path through the graph of objects. So, adding the patha assumes that there is a property collection of name a in the specified root class. Adding the path a/b additionally assumes that the class for a has a property collection of name b and so on.After adding all paths you can create the criteria or detached criteria object for querying the database. Additionally you can use the
Preload enum (see below) to further restrict the preload depth. This allows you to re-use certain join-criterias with different fetching depths for different usecases.JoinCriteriaHelper joiner = JoinCriteriaHelper.forClass(SomeEntity.class);
joiner.addPath("a/b/c");
joiner.addPath("a/b/d");
joiner.addPath("a/e");
// this would fetch all properties a, b, c, d and e
Criteria c = joiner.getExecutableCriteria(session);
// this would only fetch properties a, b and e
DetachedCriteria dc = joiner.getDetachedCriteria(Preload.DEPTH_2);The Source Code
Additionally to the source code below you need to setup a project with Hibernate 3 on classpath:public class CriteriaJoiner {
private final static String ALIAS_SEPARATOR = "_";
private final static String ALIAS_PREFIX = "alias_";
private final Class<?> rootClass;
private final Set<Path> paths;
private Map<String, String> aliases;
private CriteriaJoiner(Class<?> rootClass) {
if (rootClass == null) {
throw new RuntimeException("Root class cannot be null.");
}
this.rootClass = rootClass;
this.paths = new HashSet<Path>();
}
public static CriteriaJoiner forClass(Class<?> rootClass) {
return new CriteriaJoiner(rootClass);
}
public static CriteriaJoiner forClass(Class<?> rootClass, String... paths) {
CriteriaJoiner helper = new CriteriaJoiner(rootClass);
for (String p : paths) {
if (p != null) {
helper.addPath(p);
}
}
return helper;
}
private String toAlias(String path) {
if (path == null || "".equals(path)) {
return "";
}
return ALIAS_PREFIX + path.replace(Path.PATH_SEPARATOR, ALIAS_SEPARATOR);
}
private String toPath(String alias) {
if (alias == null || "".equals(alias)) {
return "";
}
return alias.substring(ALIAS_PREFIX.length()).replace(ALIAS_SEPARATOR, Path.PATH_SEPARATOR);
}
private boolean existsAliasToPath(String path) {
return aliases.containsValue(path);
}
private void putAlias(String alias) {
aliases.put(alias, toPath(alias));
}
private DetachedCriteria createAliases(DetachedCriteria dc, Preload preload) {
aliases = new HashMap<String, String>(); // key=alias, value=property
for (Path p : paths) {
PathIterator i = p.iterator();
int count = 0;
while (i.hasNext() && count < preload.depth()) {
count++;
String property = i.next();
String path = i.getPath();
String previousPath = i.getPreviousPath();
if (existsAliasToPath(path)) {
continue;
}
if (!existsAliasToPath(previousPath)) {
String alias = toAlias(path);
putAlias(alias);
dc.createAlias(property, alias, Criteria.LEFT_JOIN);
} else {
String previousAlias = toAlias(previousPath);
String alias = toAlias(path);
putAlias(alias);
dc.createAlias(previousAlias + "." + property, alias, Criteria.LEFT_JOIN);
}
}
}
return dc;
}
public DetachedCriteria getDetachedCriteria() {
return getDetachedCriteria(Preload.ALL);
}
public DetachedCriteria getDetachedCriteria(Preload preload) {
DetachedCriteria dc = DetachedCriteria.forClass(rootClass);
dc.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return createAliases(dc, preload);
}
public Criteria getExecutableCriteria(Session session) {
return getDetachedCriteria().getExecutableCriteria(session);
}
public Criteria getExecutableCriteria(Session session, Preload preload) {
return getDetachedCriteria(preload).getExecutableCriteria(session);
}
public CriteriaJoiner addPath(final String path) {
Path joinPath = new Path(path);
paths.add(joinPath);
return this;
}
public CriteriaJoiner addPaths(String... joinPaths) {
for (String path : joinPaths) {
addPath(path);
}
return this;
}
}public enum Preload {
DEPTH_0(0),
DEPTH_1(1),
DEPTH_2(2),
DEPTH_3(3),
DEPTH_4(4),
ALL(Integer.MAX_VALUE);
private final int depth;
private Preload(int depth) {
this.depth = depth;
}
public int depth() {
return depth;
}
}public class Path implements Iterable<String> {
public final static String PATH_SEPARATOR = "/";
private final String path;
public Path(String path) {
this.path = path;
if (!isValid()) {
throw new RuntimeException("Path is not valid");
}
}
public boolean isValid() {
return path != null && !"".equals(path);
}
public String[] toArray() {
if (path == null || path.equals("")) {
return new String[0];
}
return path.split(PATH_SEPARATOR);
}
public PathIterator iterator() {
return new PathIterator(this);
}
}public class PathIterator implements Iterator<String> {
private final String[] properties;
private int index;
public PathIterator(Path path) {
this.properties = path.toArray();
this.index = -1;
}
public boolean hasNext() {
return index < properties.length - 1;
}
public String next() {
index++;
return properties[index];
}
public String getPreviousPath() {
return getPath(index - 1);
}
public String getPath() {
return getPath(index);
}
public String getPath(int pos) {
if (pos < 0 || pos > properties.length - 1) {
return "";
}
String alias = "";
for (int i = 0; i <= pos; i++) {
alias += properties[i];
if (i < pos) {
alias += Path.PATH_SEPARATOR;
}
}
return alias;
}
public void remove() {
// not implemented yet
}
}Conclusion
The introduced CriteriaJoiner is a convient solution to prevent LazyInitializationExceptions and the N+1 SELECT problem. Its flexibility allows you to decide for each usecase which data you want to be loaded by Hibernate. The class creates criteria or detached criteria objects which internally use LEFT JOINs to fetch all properties with just one SELECT statement.There're some known limitations on this approach. Because CriteriaJoiner creates aliases for each property given by the paths it's difficult to use these aliases in restrictions you might add to the criteria. This issue could be solved by introducing some kind of naming convention for the created aliases so you could re-use those aliases in the WHERE clause.There is another limitation while using this approach in combination with pagination. This is due to the fact that the result set of such FETCH JOIN statements contains multiple rows for the same entity. Therefor Hibernate cannot generate pageable SQL statements. In that case pagination would be done in-memory which can cause performance issues.


8 comments:
HQL fetch is easier to understand and more expressive. And it can do many things in database. I don't like Criteria - it's the wrong branch of hibernate growth.
Hi Alexander,
I totally agree that HQL in many cases is much cleaner and easier to maintain.
On the other side building dynamic queries is much easier by using the Criteria API. Think of a search form with massive filtering options - each filter can either be set or be ignored. This is much easier with Criteria because you cannot predefine a HQL string when certain restrictions are optional.
alexander, given an entity a, how do you left join fetch two or more collections b and c on that entity without getting the "multiple bags" exception?
benjamin, does this preload pattern work with two lists on an entity that you want to fetch? i didn't test it yet because i don't think it will work.
Hi timo,
it works fine with multiple collections. E.g. you can attach the following paths:
a/b/c
a/b/d
Entity B has 2 collections (c and d). The resulting SQL will correctly LEFT JOIN fetch all collections A, B, C and D.
The generated Criteria is similiar to the corresponding HQL statement:
SELECT DISTINCT root
FROM RootEntity root
LEFT JOIN FETCH root.a a
LEFT JOIN FETCH a.b b
LEFT JOIN FETCH b.c
LEFT JOIN FETCH b.d
Hi Benjamin, hi Timo!
Timo, i think multiple bag exception it's generally a mapping problem. If you have join 2 bags you'll get exception anyway then you'll trying to eagerly fetch it.
Benjamin, regarding dynamic queries, etc - i think database access code should be expreessed in "database language" and code complete goes out here. You lose only one if stament per parameter compare to Criteria. Although, Criteria by Example is a cool thing.
About HQL and dynamic queries. I completely agree with Benjamin here. criteria works much nicer here.
The 'only' one if statement per filter is really the exact argument, it is just the wrong way round.
In my last hibernate project this would account into about 1000 if statements. And the really bad thing is: this kind of code would actually reacreate the same stuff the criteria API tries to solve.
I agree though that the criteria api sucks in many ways, due to its many limitations.
On another note, I am not to happy about the claim of problems this approach claims to solve.
To me this is just a way to specify which parts of an object graph to load eagerly.
This can be done for performance reasons (n+1 select) or as a way to prevent LazyLoadExceptions.
But the real underlying problem is not adressed in the article: The data access layer must preload stuff. But what kind of stuff depends on what the business logic or presentation wants to do with the entities. So the question behind this is: How to convei this Information without leaking database information in the upper layers.
Hi Jens,
this is an interesting point you mentioned. It should be part of the persistence layer to decide, which data to load and how to load them. This is the single approach to encapsulate the whole persistence into one layer.
From my experience the only possibility to achive this is by letting your persistence layer only return DTOs which are totally decoupled from the Hibernate entities.
I for myself dont like DTOs because they bloat code by duplicating the business model.
Some time ago I implemented some kind of remote lazy loading for Hibernate entities. It's another solution to solve the problem but the approach has his own consequences, thus cannot be treated as a general purpose solution.
Im asking myself if fetch profiles in JPA 2.0 are some way to address these issues!? Cannot find any further information about that.
Post a Comment