Tuesday, July 26, 2011

Hibernate and User Types


What is user Type?

There are many cases where we persist/update the information differently and show it differently in the UI.

Lets take the case of gender information,in database we are persisting 'M' for 'Male' and 'F' for 'Female'.So we are persisting and showing the information differently.

Another use case could be Phone Number.We are showing area code and number differently in UI.However in database we are persisting area code and phone number together.
Hibernate helps in resolving these cases by providing user types which helps in this conversion.

Simple UserType

UseCase:- In the UI we are showing area code and phone number seperately.However in  the database,we are persisting the area code and phone number together in one column as a String datatype.

Phone.java containing seperate place holders for area code and phone number.

package com.kunaal.model.userType;

/**
 * 
 * User type class converting area code and phone number into one column in database
 * and vice versa.
 * In the UI area code and phone number have seperate place holders 
 * But in database it wraps into one column.
 * 
 * @author Kunaal A Trehan
 *
 */
public class Phone {
 
 private String areaCode;
 
 private String phoneNum;

 /**
  * @return the areaCode
  */
 public String getAreaCode() {
  return areaCode;
 }

 /**
  * @param areaCode the areaCode to set
  */
 public void setAreaCode(String areaCode) {
  this.areaCode = areaCode;
 }

 /**
  * @return the phoneNum
  */
 public String getPhoneNum() {
  return phoneNum;
 }

 /**
  * @param phoneNum the phoneNum to set
  */
 public void setPhoneNum(String phoneNum) {
  this.phoneNum = phoneNum;
 }
 
 public String toString(){
  return areaCode + "-" + phoneNum;
 }

 /* (non-Javadoc)
  * @see java.lang.Object#hashCode()
  */
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result
    + ((areaCode == null) ? 0 : areaCode.hashCode());
  result = prime * result
    + ((phoneNum == null) ? 0 : phoneNum.hashCode());
  return result;
 }

 /* (non-Javadoc)
  * @see java.lang.Object#equals(java.lang.Object)
  */
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  Phone other = (Phone) obj;
  if (areaCode == null) {
   if (other.areaCode != null)
    return false;
  } else if (!areaCode.equals(other.areaCode))
   return false;
  if (phoneNum == null) {
   if (other.phoneNum != null)
    return false;
  } else if (!phoneNum.equals(other.phoneNum))
   return false;
  return true;
 }

}


PhoneUserType.java which implements UserType interface provided by hibernate.UserType implementation provides developer to hook the custom implementation and acts as a adapter between relational database and UI view.Important methods are nullSafeGet and nullSafeSet where all the conversion logic exists

package com.kunaal.model.userType;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.StringTokenizer;

import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;
import org.springframework.util.ObjectUtils;

/**
 * @author Kunaal A Trehan
 *
 */
public class PhoneUserType implements UserType {

 /**
  * What column types to map,data type of the column
  */
 public int[] sqlTypes() {
  return new int[]{Types.VARCHAR};
 }

 /**
  * Class  details of object which is going to be used
  */
 public Class returnedClass() {
  return Phone.class;
 }

 public boolean equals(Object x, Object y) throws HibernateException {
  return ObjectUtils.nullSafeEquals(x, y);
 }

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

 /**
  * Creates the custom object from the data returned by resultset
  */
 public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
   throws HibernateException, SQLException {
  Phone phone=null;
  
  String nameVal = rs.getString(names[0]);
  if(nameVal !=null){
   phone=new Phone();
   
   StringTokenizer tokenizer=new StringTokenizer(nameVal,"-");
   phone.setAreaCode(tokenizer.nextToken());
   phone.setPhoneNum(tokenizer.nextToken());
   
  }
  return phone;
 }

 /**
  * Converts custom object into value which needs to be passed to prepared statement
  */
 public void nullSafeSet(PreparedStatement st, Object value, int index)
   throws HibernateException, SQLException {
  
  if(value==null){
   st.setNull(index, Types.VARCHAR);
  }else{
   st.setString(index, ((Phone)value).toString());
  }
  
 }

 /**
  * Returns deep copy of object
  */
 public Object deepCopy(Object value) throws HibernateException {
  if(value==null)
   return null;
  else{
   Phone newObj=new Phone();
   Phone existObj=(Phone)value;
   
   newObj.setAreaCode(existObj.getAreaCode());
   newObj.setPhoneNum(existObj.getPhoneNum());
   
   return newObj;
  }
   
 }

 public boolean isMutable() {
  return false;
 }

 /**
  * 
  */
 public Serializable disassemble(Object value) throws HibernateException {
  Object  deepCopy=deepCopy(value);
  
  if(!(deepCopy instanceof Serializable))
   return (Serializable)deepCopy;
  
  return null;
 }

 public Object assemble(Serializable cached, Object owner)
   throws HibernateException {
  return deepCopy(cached);
  //return cached;
 }

 public Object replace(Object original, Object target, Object owner)
   throws HibernateException {
  return deepCopy(original);
 }

}


Inorder to use PhoneUserType ,we have to add @Type annotation defining the implementation class

@Type (type="com.kunaal.model.userType.PhoneUserType")
private Phone phoneNumber;

Composite UserType

Plain usertypes are used when we are storing the information in one column in database.However there are cases when we have multiple columns corresponding to one java object.In that case,hibernate provides composite user type to do the conversion.

Use Case:- Lets take the case of monetary value.We have a money class containing currency code and monetary amount.In  database we have different columns for persisting this information.However when we show the information in the UI,we are clubbing the property values and showing it.

e.g. In the UI,100 US dollars are shown as USD 100.However in database USD and 100 are stored seperately in different columns.

Money.java having seperate properties for currency code and amount.


package com.kunaal.model.compositeUserType;

import java.util.Currency;

/**
 * Custom user type for money
 * In UI there will be one placeholder for entering monetary value
 * including currency code and amount.
 * However in db there are two different column for that 
 *
 * @author Kunaal A Trehan
 *
 */
public class Money {

 private Currency currCode;
 
 private Double amount;

 /**
  * @return the currCode
  */
 public Currency getCurrCode() {
  return currCode;
 }

 /**
  * @param currCode the currCode to set
  */
 public void setCurrCode(Currency currCode) {
  this.currCode = currCode;
 }

 /**
  * 
  */
 public Money() {
  super();
 }

 /**
  * @param currCode
  * @param amount
  */
 public Money(Currency currCode, Double amount) {
  super();
  this.currCode = currCode;
  this.amount = amount;
 }

 /**
  * @return the amount
  */
 public Double getAmount() {
  return amount;
 }

 /**
  * @param amount the amount to set
  */
 public void setAmount(Double amount) {
  this.amount = amount;
 }

 /* (non-Javadoc)
  * @see java.lang.Object#hashCode()
  */
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((amount == null) ? 0 : amount.hashCode());
  result = prime * result
    + ((currCode == null) ? 0 : currCode.hashCode());
  return result;
 }

 /* (non-Javadoc)
  * @see java.lang.Object#equals(java.lang.Object)
  */
 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  if (obj == null)
   return false;
  if (getClass() != obj.getClass())
   return false;
  Money other = (Money) obj;
  if (amount == null) {
   if (other.amount != null)
    return false;
  } else if (!amount.equals(other.amount))
   return false;
  if (currCode == null) {
   if (other.currCode != null)
    return false;
  } else if (!currCode.equals(other.currCode))
   return false;
  return true;
 }
 
}


MoneyUserType.java containing implementation for conversion from Object to relational database and vice versa.Important methods are getPropertyNames(),getPropertyTypes(),getPropertyValue(),setPropertyValue(),nullSafeGet(),nullSafeSet() 
package com.kunaal.model.compositeUserType;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Currency;

import org.hibernate.HibernateException;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.type.Type;
import org.hibernate.usertype.CompositeUserType;
import org.springframework.util.ObjectUtils;

/**
 * Composite user type mapping money object to two columns in database
 * 
 * @author Kunaal A Trehan
 *
 */
public class MoneyUserType implements CompositeUserType{
 /**
  * This refers to java object property names which are mapped 
  */
 public String[] getPropertyNames() {
  return new String[]{"currCode","amount"};
 }

 /**
  * This refers to java object property types
  */
 public Type[] getPropertyTypes() {
  return new Type[]{org.hibernate.type.StandardBasicTypes.CURRENCY,
    org.hibernate.type.StandardBasicTypes.DOUBLE};
 }

 /**
  * This method fetches the property from the user type depending upon 
  * the index.It should follow getPropertyNames()s
  */
 public Object getPropertyValue(Object component, int property)
   throws HibernateException {
  if(component ==null)
   return null;
  else{
   if(property==0 )
    return ((Money)component).getCurrCode();
   else if(property==1)
    return ((Money)component).getAmount();
  }
  
  return null;
 }

 /**
  * This method sets the individual property in the custom user type
  */
 public void setPropertyValue(Object component, int property, Object value)
   throws HibernateException {
  if(value!=null){
   if (property ==0){
    ((Money)component).setCurrCode((Currency)value);
   }else if(property ==1)
    ((Money)component).setAmount((Double)value);
  }
 }

 /** 
  * This method returns the custom user type class
  */
 public Class returnedClass() {
  return Money.class;
 }

 public boolean equals(Object x, Object y) throws HibernateException {
  return ObjectUtils.nullSafeEquals(x, y);
 }

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

 /**
  * This method constructs the custom user type from the resultset
  */
 public Object nullSafeGet(ResultSet rs, String[] names,
   SessionImplementor session, Object owner)
   throws HibernateException, SQLException {
  String currCode = rs.getString(names[0]);
  Double balanceAmt=rs.getDouble(names[1]);
  
  if(currCode !=null && balanceAmt !=null){
   return new Money(Currency.getInstance(currCode),balanceAmt);
  }else{
   return null;
  }
 }

 /**
  * This method sets the value from the user type into prepared statement
  */
 public void nullSafeSet(PreparedStatement st, Object value, int index,
   SessionImplementor session) throws HibernateException, SQLException {
  if(value !=null){
   st.setString(index,((Money)value).getCurrCode().getCurrencyCode());
   st.setDouble(index+1, ((Money)value).getAmount());
  }else{
   st.setObject(index,null);
   st.setObject(index +1, null);
  }
 }

 /**
  * Deep copy
  */
 public Object deepCopy(Object value) throws HibernateException {
  Money returnVal=new Money();
  Money currVal=(Money)value;
  Double amount = currVal.getAmount();
  Currency currCode = currVal.getCurrCode();
  
  returnVal.setCurrCode(Currency.getInstance(currCode.getCurrencyCode()));
  returnVal.setAmount(new Double(amount.doubleValue()));
  
  return returnVal;
 }

 public boolean isMutable() {
  return false;
 }

 public Serializable disassemble(Object value, SessionImplementor session)
   throws HibernateException {
  Object  deepCopy=deepCopy(value);
  
  if(!(deepCopy instanceof Serializable))
   return (Serializable)deepCopy;
  
  return null;
 }

 public Object assemble(Serializable cached, SessionImplementor session,
   Object owner) throws HibernateException {
  return deepCopy(cached);
 }

 public Object replace(Object original, Object target,
   SessionImplementor session, Object owner) throws HibernateException {
  return deepCopy(original);
 }

}


Inorder to use MoneyUserType ,we have to add @Type annotation defining the implementation class and column mapping

@Type(type="com.kunaal.model.compositeUserType.MoneyUserType")
                     @Columns(columns = { @Column(name="CurrencyCode"),
                                                           @Column(name="Balance")
                                                         }
                                        )
private Money acctBalance;

8 comments:

  1. It is really a great work and the way in which u r sharing the knowledge is excellent.Thanks a lot! You made a new blog entry to answer my question; I really appreciate your time and effort.
    java training institutes in chennai | java j2ee training institutes in velachery

    ReplyDelete
  2. Thanks for great tutorial, Is there a way to pass a parameter in the @Type annotation and then read that parameter in one of the methods of UserType interface?

    ReplyDelete
  3. Thanks man you saved my day. :) God Bless

    ReplyDelete