Saturday, February 5, 2011

Hibernate hurdles

I've been using Hibernate for the better half of 10 years now (since Hibernate 2), and while I really like Hibernate it still surprises me from time to time how complex things can get. This post highlights a few of these hurdles. I'm pretty sure other ORMs have the same or similar issues, but I'll use Hibernate here because that's what I know best. To demonstrate things I will use the classic Order/LineItem example:
@Entity
@Table(name = "T_ORDER")
public class Order {

 @Id
 @GeneratedValue
 private Long id;

 @ElementCollection
 @CollectionTable(name = "T_LINE_ITEM")
 private Set<LineItem> lineItems = new HashSet<LineItem>();

 public Long getId() {
  return id;
 }

 public Set<LineItem> getLineItems() {
  return lineItems;
 }
 
 public int getTotalPrice() {
  int total = 0;
  for (LineItem lineItem : lineItems) {
   total += lineItem.getPrice();
  }
  return total;
 }
}

@Embeddable
public class LineItem {

 @ManyToOne(fetch = FetchType.LAZY)
 private Product product;
 
 private int price;
 
 @SuppressWarnings("unused")
 private LineItem() {
 }

 public LineItem(Product product, int price) {
  this.product = product;
  this.price = price;
 }
 
 public Product getProduct() {
  return product;
 }

 public int getPrice() {
  return price;
 }
}

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(name = "T_PRODUCT")
public abstract class Product {

 @Id
 @GeneratedValue
 private Long id;
 
 private String name;
 
 protected Product() {
 }
 
 protected Product(String name) {
  this.name = name;
 }
 
 public Long getId() {
  return id;
 }

 public String getName() {
  return name;
 }
}

@Entity
public class PcProduct extends Product {
 
 @SuppressWarnings("unused")
 private PcProduct() {
 }

 public PcProduct(String name) {
  super(name);
 }
}

@Entity
public class MacProduct extends Product {
 
 @SuppressWarnings("unused")
 private MacProduct() {
 }

 public MacProduct(String name) {
  super(name);
 }
}
That's pretty straightforward stuff. Assume we have an order stored in the database:
Product pc = new PcProduct("Dell XPS M1330");
session.save(pc);
Product mac = new MacProduct("Apple MacBook Pro");
session.save(mac);

Order order = new Order();
order.getLineItems().add(new LineItem(pc, 1000));
order.getLineItems().add(new LineItem(mac, 2000));
session.save(order);
Now let's look at some surprising pieces of code.

Beware of fetch joins

Suppose you have a business transaction processing orders involving Dell XPS M1330s:
List<Order> results = session.createQuery(
  "from Order as o join fetch o.lineItems as li where li.product.name = 'Dell XPS M1330'").list();
for (Order result : results) {
 System.out.println(result.getTotalPrice());
}
What do you think this prints out for our sample order containing both a Dell XPS M1330 and an Apple MacBook Pro? If you guessed 1000 you would be right... oopsie! The fetch in the query combined with criteria on the joined relation causes the lineItems collection of the resulting Order entities to be incomplete! Unfortunately, there is no way to tell that those Order objects are crippled. Worse still, they will end up in the Hibernate session so you might inadvertently end up using them later on in the same transaction, unaware that they are damaged goods. This can lead to very hard to diagnose bugs, because the cause of the problem (the join fetch) and the effects (incorrect results) might be far apart in the code. It's interesting to note that normal joins (so not using fetch) do not have this adverse side effect. So my advice is:
Be very wary of fetch joins. Only use them if you are sure the resulting objects will not be reused later on in the same session for different purposes.

Beware of inheritance mapping

Let's look at another order processing transaction:
long orderId = ...;
Order order = (Order) session.get(Order.class, orderId);
Product product = order.getLineItems().iterator().next().getProduct();

PcProduct pcProduct = (PcProduct) session.get(PcProduct.class, product.getId());

System.out.println(pcProduct.getId().equals(product.getId()));
System.out.println(pcProduct == product);
If you run this piece of code, Hibernate will spit out a warning:
1463 [main] WARN org.hibernate.engine.StatefulPersistenceContext.ProxyWarnLog
 Narrowing proxy to class com.ervacon.order.PcProduct - this operation breaks ==
And indeed, the output is true / false, confirming that the session can now no longer guarantee that primary key equality is equivalent with reference equality (==). This is a dangarous situation that could again lead to hard to diagnose bugs, especially if some parts of the code (e.g. the lineItem.getProduct() call) are only executed is certain situations. There are other problems with mapping inheritance hierarchies (e.g. the fact that instanceof in unreliable), so my advice here is:
When using Hibernate, avoid mapping inheritance hierarchies. There are often better ways to factor your code that do not require mapping an inheritance hierarchy to the database, for instance by introducing an common interface to facilitate polymorphism.

There are other non-obvious head-scratchers you run into when using an object-relational mapper like Hibernate. If you know about an interesting one, I'd love to hear about it!

6 comments:

  1. Very good post. I had no idea about == breaking under these circumstances!
    However, I fail to see the problem in the first example... If you explicitly stated in the WHERE part of the query that you only want Dell (btw, I had no clue one can filter fetched lists like that), what's so strange about only getting Dell?

    ReplyDelete
  2. The issue with fetch joins where you have conditions on the joined relation is that you get back incomplete entities. The intent of the query is, or at least seems to be "give me orders involving Dell XPS M1330s, together with their line items". However, the query really comes down to "give me orders involving Dell XPS M1330s, together with their Dell XPS M1330 line items". That is a bit of a surprise, especially if you look at it from the domain model point of view: why would you ever have an Order entity in the domain model with an incomplete set of line items!? If you are unaware of this, you might end up reusing the Order entity thinking it contains all its line items (why shouldn't it, right?), which can of course give obscure bugs.

    ReplyDelete
  3. @veggen:

    Basically it comes down to the fact that hibernate gives you a free cache (the session), which sometimes yields to unexpected results.

    Once you load an entity in hibernate, you will always get that same version back (unless you evict or do a force reload or something).

    The problem with this is illustrated by Erwin: if you manage to load a 'filtered' version of an entity relation. In this case by of join fetch + restriction on the join.

    So first you loaded an entity and eager fetched a (filtered) part of its relation. Now, a bit further down in that same business transaction, you are doing another query which loads the entity of that same type as previously, however, at this time you don't have any restriction on the join.

    Normally you think getting back the entity with all elements in the relation. But think again: you will get the entity back as initially loaded, so only with a part of the relation, as resulted by the first query.

    ReplyDelete
  4. @klr8

    Spoiled by Hibernate? It helps to keep in mind that HQL is only a *thin* layer on top of SQL.

    ReplyDelete
  5. Indeed, if you look at the HQL queries as 'SQL-like', the results come as less of a surprise. It's only if you thinking in terms of entities and aggregate roots that this behavior seems bizarre.

    ReplyDelete