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!