Search This Blog

Monday 12 September 2011

Creating a Hibernate Custom Type - 1

As seen before Hibernate comes with a huge collection of mapping types. These cover a very wide range of types from single bit fields to blobs and clobs.
It is also possible that we would like to retrieve the data in one of our own pre-defined formats. Hibernate supports this by allowing us to define our own customized mapping types.
These make it possible to convert data in the tables directly to Java objects.
Consider the audit fields present in the tables. These can be represented using the available types in Hibernate:
<property name="createdBy" type="integer">
    <column name="Created_By" />
</property>
<property name="createdDate" type="timestamp">
    <column name="Created_Date" length="19" />
</property>
<property name="modifiedBy" type="integer">
    <column name="Modified_By" />
</property>
<property name="modifiedDate" type="timestamp">
    <column name="Modified_Date" length="19" />
</property>
The alternative option is to create a new type that will map all these columns to a Java Class just like string type maps a varchar column to a java.util.String class. Hibernate provides several interfaces to create custom mapping types. One way is to implement the org.hibernate.usertype.UserType interface. As per the comments in the source -code
"This interface should be implemented by user-defined "types". A "type" class is not the actual property type - it is a class that knows how to serialize instances of another class to and from JDBC."
The Java class used to represent the data is as below:
package com.model.component;

import java.io.Serializable;
import java.util.Date;

@SuppressWarnings("serial")
public class AuditData implements Serializable {
    private Integer createdBy;
    private Date createdDate;
    private Integer modifiedBy;
    private Date modifiedDate;
    
    public AuditData() {
    }
    
    /**
     * Copy constructor
     * @param other
     */
    public AuditData(AuditData other) {
        this.setCreatedBy(other.getCreatedBy());
        this.setCreatedDate(new Date(other.getCreatedDate().getTime()));
        this.setModifiedBy(other.getModifiedBy());
        this.setModifiedDate(new Date(other.getModifiedDate().getTime()));
    }

        //setter and getter methods

    @Override
    public boolean equals(Object obj) {
        boolean isEqual = false;
        if (obj instanceof AuditData) {
            AuditData auditData = (AuditData) obj;
            isEqual = auditData.getCreatedBy().equals(this.getCreatedBy())
                    && auditData.getModifiedBy().equals(this.getModifiedBy())
                    && auditData.getCreatedDate().equals(this.getCreatedDate())
                    && auditData.getModifiedDate().equals(
                            this.getModifiedDate());
        }
        return isEqual;
    }

    @Override
    public int hashCode() {
        int hash = this.getCreatedDate().hashCode();
        hash = hash * 17 + this.getCreatedBy().hashCode();
        hash = hash * 31 + this.getModifiedBy().hashCode();
        hash = hash * 13 + this.getModifiedDate().hashCode();
        return hash;

    }

    @Override
    public String toString() {
        return "[ " + this.getClass() + " { createdBy : " + createdBy
                + ", createdDate: " + createdDate + ", modifiedBy: "
                + modifiedBy + ", modifiedDate: " + modifiedDate + "}]";

    }
}
This is the class that will be serialized to and from JDBC. The class does not implement any Hibernate specific interface. It implements Serializable interface as well as the hashCode() and equals() methods
The mapping file needs to be modified to return the data in the audit columns to an instance of AuditData class. Consider a simple user class that includes the audit fields:
public class TypedUser {
    private Long id;
    private String name;
    private AuditData auditData;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public AuditData getAuditData() {
        return auditData;
    }

    public void setAuditData(AuditData auditData) {
        this.auditData = auditData;
    }
}
As can be seen instead of Date and int fields to represent the created by/modified by and time stamp, an instance of AuditData is used.The class looks similar to the component or entity classes that we create in our code.
We now need to create a mapping type that will be responsible for loading the data into the AuditData object and saving the data in this object to the appropriate columns in the table.
<?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="TypedUser" 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 = "auditData" type ="com.customtype.AuditType">
            <column name="Created_By" />
            <column name="Created_Date" length="19" />
            <column name="Modified_By" />
            <column name="Modified_Date" length="19" />
        </property>
    </class>
</hibernate-mapping>
As can be seen from the above mapping file, multiple columns have been mapped to the auditData property of TypedUser class. The AuditType class will be responsible for the conversion.
package com.customtype;

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

import org.apache.log4j.Logger;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;

import com.model.component.AuditData;

public class AuditType implements UserType {
    
    private static final Logger logger = Logger.getLogger(AuditType.class);

    /**
     * Returns the object from the 2 level cache
     */
    @Override
    public Object assemble(final Serializable cached, final Object owner)
            throws HibernateException {
        //would work as the AuditData.class is Serializable, 
        //and stored in cache as it is - see disassemble
        return cached;
    }

    /**
     * Used to create Snapshots of the object
     */
    @Override
    public Object deepCopy(Object value) throws HibernateException {
        //return value; -> if AuditData.class was immutable we could return the object as it is
        final AuditData recievedParam = (AuditData) value;
        final AuditData auditData = new AuditData(recievedParam);
        return auditData;
    }

    /**
     * method called when Hibernate puts the data in a second level cache. The data is stored 
     * in a serializable form
     */
    @Override
    public Serializable disassemble(final Object value) throws HibernateException {
        //For this purpose the AuditData.class must implement serializable
        return (Serializable) value;
    }

    /**
     * Used while dirty checking - control passed on to the {@link AuditData}
     */
    @Override
    public boolean equals(final Object o1, final 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;
        //for this to work correctly the equals() 
        //method must be implemented correctly by AuditData class
    }

    @Override
    public int hashCode(final Object value) throws HibernateException {        
        return value.hashCode();
        //for this to work correctly the hashCode() 
        //method must be implemented correctly by AuditData class

    }

    /**
     * Helps hibernate apply certain optimizations for immutable objects
     */
    @Override
    public boolean isMutable() {
        return true; //The audit fields can be modified
    }

    /**
     * This method retrieves the property value from the JDBC resultSet
     */
    @Override
    public Object nullSafeGet(final ResultSet resultSet, 
            final String[] names, final Object owner)
            throws HibernateException, SQLException {
        //owner here is class from where the call to retrieve data was made.
        //In this case the Test class

        AuditData auditData = null;
        //Order of columns is given by sqlTypes() method
        final Integer createdBy = resultSet.getInt(names[0]);
        //Deferred check after first read
        if (!resultSet.wasNull()) {
            auditData = new AuditData();
            auditData.setCreatedBy(createdBy);
            final Date createdDate = resultSet.getTimestamp(names[1]);
            final Integer modifiedBy = resultSet.getInt(names[2]);
            final Date modifiedDate = resultSet.getTimestamp(names[3]);
            auditData.setCreatedDate(createdDate);
            auditData.setModifiedBy(modifiedBy);
            auditData.setModifiedDate(modifiedDate);            
        }
        return auditData;
    }

    /**
     * The method writes the property value to the JDBC prepared Statement
     * 
     */
    @Override
    public void nullSafeSet(final PreparedStatement statement,
            final Object value, final int index) throws HibernateException,
            SQLException {
        if (null == value) {
            statement.setNull(index, Hibernate.INTEGER.sqlType());
            statement.setNull(index + 2, Hibernate.INTEGER.sqlType());
            statement.setNull(index + 1, Hibernate.TIMESTAMP.sqlType());
            statement.setNull(index + 3, Hibernate.TIMESTAMP.sqlType());
        } else {
            AuditData auditData = (AuditData) value;
            logger.debug("Saving object " + auditData + " for index " + index);
            statement.setInt(index, auditData.getCreatedBy());
            if (null != auditData.getCreatedDate()) {
                Timestamp createdTimestamp = new Timestamp(auditData
                        .getCreatedDate().getTime());
                statement.setTimestamp(index + 1, createdTimestamp);
            } else {
                statement.setNull(index + 1, Hibernate.TIMESTAMP.sqlType());
            }
            statement.setInt(index + 2, auditData.getModifiedBy());
            if (null != auditData.getModifiedDate()) {
                Timestamp modifiedTimestamp = new Timestamp(auditData
                        .getModifiedDate().getTime());
                statement.setTimestamp(index + 3, modifiedTimestamp);
            } else {
                statement.setNull(index + 3, Hibernate.TIMESTAMP.sqlType());
            }

        }
    }
/**      * Method used by Hibernate to handle merging of detached object.      */ @Override public Object replace(final Object original, final Object target, final Object owner) throws HibernateException { //return original; // if immutable use this //For mutable types at bare minimum return a deep copy of first argument return this.deepCopy(original); } /**      * Method tells Hibernate which Java class is mapped to this Hibernate Type      */ @SuppressWarnings("rawtypes") @Override public Class returnedClass() { return AuditData.class; } /**      * Method tells Hibernate what SQL columns to use for DDL schema generation.      * using the Hibernate Types leaves Hibernate free to choose actual SQl types      * based on database dialect.      * (Alternatively SQL types can also be used directly)      */ @Override public int[] sqlTypes() { //createdBy, createdDate,modifiedBy,modifiedDate return new int[] { Hibernate.INTEGER.sqlType(), Hibernate.TIMESTAMP.sqlType(), Hibernate.INTEGER.sqlType(), Hibernate.TIMESTAMP.sqlType() }; } }
On executing the code to load a TypedUser object from the database the logs can be seen below:
static void testLoad() {
    Session session = SESSION_FACTORY.openSession();
    try {
            TypedUser user = (TypedUser) session.load(TypedUser.class, 1L);
        System.out.println("Name : " + user.getName() + " Audit details : " 
            + user.getAuditData() );
    } catch (HibernateException e) {
        e.printStackTrace();
    } finally {
        session.close();
    }
}
Logs:
610  [main] INFO  org.hibernate.cfg.Configuration  - Reading mappings from resou
rce : com/model/TypedUser.hbm.xml
...
985  [main] INFO  org.hibernate.cfg.HbmBinder  - Mapping class: com.model.TypedU
ser -> USER
1000 [main] DEBUG org.hibernate.cfg.HbmBinder  - Mapped property: id -> ID
1031 [main] DEBUG org.hibernate.cfg.HbmBinder  - Mapped property: name -> NAME
1031 [main] DEBUG org.hibernate.cfg.HbmBinder  - Mapped property: auditData -> C
reated_By, Created_Date, Modified_By, Modified_Date
...
2953 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  - Stat
ic SQL for entity: com.model.TypedUser
2953 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ver
sion select: select ID from USER where ID =?
2953 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Sna
pshot select: select typeduser_.ID, typeduser_.NAME as NAME0_, typeduser_.Create
d_By as Created3_0_, typeduser_.Created_Date as Created4_0_, typeduser_.Modified
_By as Modified5_0_, typeduser_.Modified_Date as Modified6_0_ from USER typeduse
r_ where typeduser_.ID=?
2953 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ins
ert 0: insert into USER (NAME, Created_By, Created_Date, Modified_By, Modified_D
ate, ID) values (?, ?, ?, ?, ?, ?)
2953 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Upd
ate 0: update USER set NAME=?, Created_By=?, Created_Date=?, Modified_By=?, Modi
fied_Date=? where ID=?
...
...
352969 [main] DEBUG org.hibernate.SQL  - 
    select
        typeduser0_.ID as ID0_0_,
        typeduser0_.NAME as NAME0_0_,
        typeduser0_.Created_By as Created3_0_0_,
        typeduser0_.Created_Date as Created4_0_0_,
        typeduser0_.Modified_By as Modified5_0_0_,
        typeduser0_.Modified_Date as Modified6_0_0_ 
    from
        USER typeduser0_ 
    where
        typeduser0_.ID=?
...
353156 [main] DEBUG org.hibernate.loader.Loader  - done processing result set (1
 rows)
353172 [main] DEBUG org.hibernate.loader.Loader  - done entity load
Name : New User Audit details : [ class com.model.component.AuditData { createdB
y : 1, createdDate: 2011-08-21 12:06:51.0, modifiedBy: 1, modifiedDate: 2011-08-
21 12:06:51.0}]...
353188 [main] DEBUG org.hibernate.jdbc.JDBCContext  - after transaction completi
on
Thus the data was retrieved using the custom mapping type. These custom types are useful if there is a need to perform certain validations before database write or certain conversion operations on the retrieved data before allowing it to be used by the application business layer.
However the UserType interface does not expose the individual properties such as created_by to Hibernate.Thus you cannot write a HQL query or a Criteria specifying properties of the UserType.
For this the mapping class needs to implement a different interface.

1 comment:

  1. Hi can someone help me on the issue

    http://stackoverflow.com/questions/35227986/implementing-custom-hibernate-type

    ReplyDelete