piątek, 21 października 2011

Immutable value objects with Hibernate

Value objects are one the foundations of the Domain Driven Design. To take most of the benefits connected to them they should be immutable. This however causes some challenges when using them as an attributes of entities mapped using Hibernate.

Let's take a look at sample value object representing currency.

package com.acme;

public final class Currency implements Serializable {

 public static final Currency EUR = new Currency("EUR");  
 public static final Currency USD = new Currency("USD");  

 private final String code;
 
 public static Currency valueOf(String code) {
  if (EUR.getCode().equals(code) {
   return EUR;
  }
  if (USD.getCode().equals(code) {
   return EUR;
  }
  return new Currency(code);
 }

 private Currency(String code) {
  this.code = code;
 }
 
 public String getCode() {
  return code;
 }
 
 @Override
 public boolean equals(Object obj){
  if (!(obj instanceof Currency))
   return false;
  return code.equals(((Currency) obj).getCode()); 
 }
 
 @Override
 public int hashCode(){
  return code.hashCode();
 }
 
 @Override
 public String toString() {
  return code;
 }
}

Currency class is immutable which means that it is final, all of its fields are immutable and declared as final. Moreover it does not have any public constructor but instead it has a factory method which caches most commonly used values.

Now let's assume that we have an Order entity which has Currency as one of its attributes:

package com.acme;

import javax.persistence.Entity;

@Entity
public class Order {
 // ...
 private Currency currency;

 // ...
 public Currency getCurrency() {
  return currency;
 }

 public void setCurrency(Currency currency) {
  this.currency = currency;
 }
}

To map Order.currency attribute we need to create implementation of org.hibernate.usertype.UserType interface which will handle mapping of Currency class to VARCHAR database column.

package com.acme;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;

public class CurrencyType extends AbstractImmutableType {
 
 public static final String TYPE = "com.acme.CurrencyType"; 

 private static final int[] SQL_TYPES = { 
    Types.VARCHAR 
 };
    
 public CurrencyType() {
  super();
 }

 public Object nullSafeGet(ResultSet rs, String[] names, 
    Object owner) throws SQLException {
  String value = rs.getString(names[0]);
  if (rs.wasNull()) {
   return null;
  }
  return Currency.valueOf(value);
 }

 public void nullSafeSet(PreparedStatement st, Object value, 
    int index) throws SQLException {
  if (value != null) { 
   st.setString(index, ((Currency)value).getCode());
  } else {
   st.setNull(index, SQL_TYPES[0]);
  }
 }

 public Class<?> returnedClass() {
  return Currency.class;
 }

 public int[] sqlTypes() {
  return SQL_TYPES;
 }
}

It uses AbstractImmutableType class which is defined as follows:

package com.acme;

import java.io.Serializable;

import org.hibernate.usertype.UserType;

public abstract class AbstractImmutableType 
  implements UserType {

 public AbstractImmutableType() {
  super();
 }

 public boolean isMutable() {
  return false;
 }
 
 public Serializable disassemble(Object value) {
  return (Serializable) value;
 }
 
 public Object assemble(Serializable cached, Object owner) {
  return cached;
 }

 public Object deepCopy(Object value) {
  return value;
 }

 public Object replace(Object original, Object target, 
   Object owner) {
  return original;
 }
 
 public boolean equals(Object x, Object y) {
  if (x != null && y != null) {
   return x.equals(y);
  }
  // Two nulls are equal as well
  return x == null && y == null;
 }

 public int hashCode(Object x) {
  if (x != null) {
   return x.hashCode();
  }
  return 0;
 }
}

To use CurrencyType we have to annotate Order.currency attribute with Hibernate's @Type annotation:

package com.acme;

import javax.persistence.Entity;

import org.hibernate.annotations.Type;

@Entity
public class Order {
 // ...
 private Currency currency;

 // ...
 @Type(type = CurrencyType.TYPE)
 public Currency getCurrency() {
  return currency;
 }

 public void setCurrency(Currency currency) {
  this.currency = currency;
 }
}

Brak komentarzy:

Prześlij komentarz