Saturday, May 3, 2014

Annotating Custom Types in Hibernate

Two new classes are needed. The first is the class you want to use for the column.  In my case, I created a class Phone to store data in database.  Here's my class:

Phone.java


package com.pkm.utils.types;

public class Phone implements java.io.Serializable {
    private String areaCode;
    public String getAreaCode() {
        return areaCode;
    }
    public void setAreaCode(String areaCode) {
        this.areaCode = areaCode;
    }
    public void clearAreaCode() {
        this.areaCode = null;
    }
    
    private String phoneNum;
    public String getPhoneNum() {
        return phoneNum;
    }
    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }
    
    public Phone() {
        
    }
    
    public Phone(String phoneNum, String areaCode) {
        this.phoneNum = phoneNum;
        this.areaCode = areaCode;
    }
    
    public Phone(String phoneNum) {
        this.phoneNum = phoneNum;
        this.areaCode = null;
    }
    
    @Override
    public String toString(){
        return areaCode + "-" + phoneNum;
   }
    
    @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;
    }
    
    @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;
    }
}

The factory class is produced by implementing (in the simplest case) org.hibernate.usertype.UserType, you can also see for composite user type. Documentation in this interface is pretty thin, but there are good examples available in the Hibernate distribution. Here's my implementation.

PhoneSimpleType.java


package com.pkm.utils.types;

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.engine.spi.SessionImplementor;
import org.hibernate.usertype.UserType;

public class PhoneSimpleType implements UserType {
    @Override
    public int[] sqlTypes() {
        return new int[] {Types.VARCHAR};
    }

    @Override
    public Class returnedClass() {
        return Phone.class;
    }

    @Override
    public boolean equals(Object o, Object o1) throws HibernateException {
        if(o == null && o1 == null) {
            return true;
        }
        if(o == null || o1 == null) {
            return false;
        }
        return o.equals(o1);
    }

    @Override
    public int hashCode(Object o) throws HibernateException {
        if(o != null) {
            return o.hashCode();
        }
        return 0;
    }

    @Override
    public Object nullSafeGet(ResultSet rs, String[] strings, SessionImplementor si, Object o) throws HibernateException, SQLException {
        String stringValue = rs.getString(strings[0]);
        if(stringValue == null) {
            return new Phone();
        }
        StringTokenizer tokenizer = new StringTokenizer(stringValue, "-");
        Phone phone = new Phone();
        phone.setAreaCode(tokenizer.nextToken());
        phone.setPhoneNum(tokenizer.nextToken());
        return phone;
    }

    @Override
    public void nullSafeSet(PreparedStatement ps, Object o, int i, SessionImplementor si) throws HibernateException, SQLException {
        if(o == null) {
            ps.setNull(i, Types.VARCHAR);
            return;
        }
        ps.setString(i, ((Phone) o).toString());
    }

    @Override
    public Object deepCopy(Object o) throws HibernateException {
        if(o == null || !(o instanceof Phone)) {
            return null;
        }
        Phone e = (Phone) o;
        Phone n = new Phone(e.getPhoneNum(), e.getAreaCode());
        return n;
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    @Override
    public Serializable disassemble(Object o) throws HibernateException {
        Object object = deepCopy(o);
        if(object == null || !(object instanceof Phone)) {
            return null;
        }
        return (Phone) object;
    }

    @Override
    public Object assemble(Serializable srlzbl, Object o) throws HibernateException {
        return deepCopy(srlzbl);
    }

    @Override
    public Object replace(Object o, Object o1, Object o2) throws HibernateException {
        return deepCopy(o);
    }    
}

The core of this class is the two methods which get and set values associated with my new type: nullSafeSet and nullSafeGet.  One key thing to note is that nullSafeGet is supplied with a list of all the column names mapped to the custom datatype in the current query.  In my case, there's only one, but in complex cases, you can map multiple columns to one object (there are examples in the Hibernate documentation).
The final piece of the puzzle is the annotation which tells Hibernate to use the new "Type" class to generate objects of your custom type by adding a new @Type annotation to the column:

Use new type as follows:


@Type(type = "com.pkm.utils.types.PhoneSimpleType")
@Column(name = "phone_details", nullable = true)
private Phone phone = new Phone();
public void setPhone(Phone phone) {
    this.phone = phone;
}
public Phone getPhone() {
    return phone;
}

Use as follows


User user = (User) session.get(User.class, 1);
user.getPhone().setAreaCode("+880");
user.getPhone().setPhoneNum("01727499452");

Output in my database as like (After save/update reading values from database):


Phone area: +880
Phone number: 01727499452

No comments:

Post a Comment