Search This Blog

Tuesday 20 September 2011

Creating a Hibernate Custom Type - 4

Let us look at the EnhancedUserType.This interface extends the UserType interface and provides additional methods to allow usage in XML representation. I shall implement a type that will allow saving of Enums directly to the database.
Before this if we needed to map a column in the database table with an enum, we might have saved a string or integer representation of the enum to the database and written code to manually convert the SQL value into an enum entry and vice verse.
Consider our Enum and the Java Model Class :
public class SkilledUser {
    private Long id;
    private String name;
    private Skill skill;

    public Skill getSkill() {
        return skill;
    }

    public void setSkill(Skill skill) {
        this.skill = skill;
    }
        //Other setter getter methods
}
package com.model.enumtype;

public enum Skill {
    GOOD("good"),
    BETTER("better"),
    BEST("best");
    
    private String id;
    
    private Skill(final String id) {
        this.id = id;
    }
    
    public String getId() {
        return id;
    }
    
    public static Skill getSkill(String id) {
        Skill targetSkill = null;
        for (Skill skill : Skill.values()) {
            if (skill.getId().equals(id)) {
                targetSkill = skill;
            }
        }
        return targetSkill;
    }
    
}
The hibernate mapping for the SkilledUser class is as below:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="com.model">
    <class name="SkilledUser" table="USER">
        <id name="id" type="long">
            <column name="ID" />
            <generator class="native" />
        </id>
        <property name="name" type="string">
            <column name="NAME" />
        </property>
        <property name="skill" type="com.customtype.EnhancedUserTypeForSkill">
            <column name="SKILL" />
        </property>
    </class>
</hibernate-mapping>

The skill property has been specified with a new customized type. The code for this type is as below:
package com.customtype;

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

import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.usertype.EnhancedUserType;

import com.model.enumtype.Skill;

public class EnhancedUserTypeForSkill implements EnhancedUserType {

    /**
     * This tells Hibernate to represent the column with the data-type specified here
     */
    @Override
    public int[] sqlTypes() {
        return new int[] {Hibernate.STRING.sqlType()};
    }

    @SuppressWarnings("rawtypes")
    @Override
    /**
     * method tells Hiberante that data recived from sql row should be mapped to Skill enum
     */
    public Class returnedClass() {
        return Skill.class;
    }

    @Override
    //Used for dirty checking
    public boolean equals(Object o1, Object o2) throws HibernateException {
        boolean isEqual = false;
        if (o1 == o2) {
            isEqual = true;
        }
        if (null == o1 || null == o2) {
            isEqual = false;
        } else {
            isEqual = o1.equals(o2);
        }
        return isEqual;
    }

    @Override
    //Used for dirty checking
    public int hashCode(Object x) throws HibernateException {        
        Skill skill = (Skill) x;
        return skill.hashCode();
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        return value; //as Enum is immutable its returns the same reference
    }

    @Override
    public boolean isMutable() {
        return false;//As Enum is immutable
    }

    @Override
    //called when a Skill object is stored in Hibernate second level cache
    public Serializable disassemble(Object value) throws HibernateException {
        return (Serializable) value;
    }

    @Override
    //called when a Skill object is retrieved from Hibernate second level cache
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException {
        return cached; //returning the same object as is simply a reference to an Enum constant
    }

    //used to merge detached objects
    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException {
        return original; // if immutable use this
    }
    
    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
            throws HibernateException, SQLException {
        Skill skill = null;
        String name = rs.getString(names[0]);
        if (null != name) {//The actual conversion from string to Enum
            skill = Skill.getSkill(name);
        }
        return skill;
    }

    @Override
    public void nullSafeSet(PreparedStatement st, Object value, int index)
            throws HibernateException, SQLException {
        if (null == value) {
            st.setNull(index, Hibernate.STRING.sqlType());
        } else {//the conversion from Enum to string
            Skill skill = (Skill) value;
            st.setString(index, skill.getId());
        }
    }

    //used for XML Marshalling
    @Override
    public String objectToSQLString(Object value) {
        Skill skill = (Skill) value;
        return '\'' + skill.name() +'\'';
    }

    //used for XML Marshalling
    @Override
    public String toXMLString(Object value) {
        Skill skill = (Skill) value;
        return skill.getId();
    }

    //used for XML Marshalling
    @Override
    public Object fromXMLString(String xmlValue) {
        return Skill.valueOf(xmlValue);
    }

}
I have added the comments in the java class itself. The start up logs are as below:
438 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  - Stat
ic SQL for entity: com.model.SkilledUser
2438 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ver
sion select: select ID from USER where ID =?
2438 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Sna
pshot select: select skilleduse_.ID, skilleduse_.NAME as NAME0_, skilleduse_.SKI
LL as SKILL0_ from USER skilleduse_ where skilleduse_.ID=?
2438 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ins
ert 0: insert into USER (NAME, SKILL, ID) values (?, ?, ?)
2438 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Upd
ate 0: update USER set NAME=?, SKILL=? where ID=?
2438 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Del
ete 0: delete from USER where ID=?
2438 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ide
ntity insert: insert into USER (NAME, SKILL) values (?, ?)
...
2641 [main] DEBUG org.hibernate.tool.hbm2ddl.SchemaExport  - 
    drop table if exists USER
2704 [main] DEBUG org.hibernate.tool.hbm2ddl.SchemaExport  - 
    create table USER (
        ID bigint not null auto_increment,
        NAME varchar(255),
        SKILL varchar(255),
        primary key (ID)
    )

On executing code to create a row of User in the table
static void testCreate() {
    Session session = SESSION_FACTORY.openSession();
    Transaction transaction = null;
    try {
        final SkilledUser user = new SkilledUser();
        user.setName("Lowest Skilled user");
        user.setSkill(Skill.GOOD);
        transaction = session.beginTransaction();
        session.save(user);
        transaction.commit();
    } catch (HibernateException e) {
        e.printStackTrace();
        transaction.rollback();
    } finally {
        session.close();
    }
}
4031 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        USER
        (NAME, SKILL) 
    values
        (?, ?)
The record created in the table holds the string value for the skill 'Good'. I fetched the same record now created using a simple hql query
static void testFetchByQuery() {
    Session session = SESSION_FACTORY.openSession();
    try {
        Query query = session
                .createQuery("from SkilledUser user where user.skill = com.model.enumtype.Skill.GOOD");
        SkilledUser user = (SkilledUser) query.list().get(0);
        System.out.println("Name : " + user.getName() + " Id : "
                + user.getId());
    } catch (HibernateException e) {
        e.printStackTrace();
    } finally {
        session.close();
    }
}

The query fired does a varchar based comparism in the database to return the actual record

3063 [main] DEBUG org.hibernate.SQL  - 
    select
        skilleduse0_.ID as ID0_,
        skilleduse0_.NAME as NAME0_,
        skilleduse0_.SKILL as SKILL0_ 
    from
        USER skilleduse0_ 
    where
        skilleduse0_.SKILL='GOOD'
How did the above query work ? More specifically how did Hibernate convert the enum instance of GOOD to the literal string 'Good' ?
The EnhancedUserType used the objectToSQLString() method to convert the object in the HQL to a string representation. The method was invoked by the HQL parser.
Thus now we can directly use the enum constants/names in our HQL queries. Instead of hard-coding the value as in the HQL query, we could pass it as a value to the method.
Also by combining the ParametrizedType Interface with the above code it is possible to create a single generic Type class to handle all the different enum mappings in our code. 

No comments:

Post a Comment