This article introduces an approach to preload certain relations in complex object graphs with Hibernate on a per-usecase basis. The intention is to prevent LazyInitializationException
during runtime and to reduce the N+1 SELECT problem while working with lazy relations. What does per-usecase mean in this context? The approach affords to easily decide which parts of an object graph are directly loaded by Hibernate for each and every usecase . If you’re familiar with these problems you can skip the next section and dive directly into the proposed pattern.
The LazyInitializationException™
Do you know Hibernates LazyInitializationException
? 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 path a 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 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.