Search This Blog

Wednesday 14 September 2011

Creating a Hibernate Custom Type - 2

As seen in the previous blog, the UserType interface does not provide the ability to use the properties of the new Type in Hibernate Queries. For this we need to implement the more powerful CompositeUserType.
Using the same AuditData object from the previous example,
I created a new custom type that would support the querying feature.
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.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.type.Type;
import org.hibernate.usertype.CompositeUserType;

import com.model.component.AuditData;

public class CompositeAuditType implements CompositeUserType {

    /**
     * This returns the Type of the SQL columns
     */
    @Override
    public String[] getPropertyNames() {
        return new String[] { "createdBy", "createdDate", "modifiedBy",
                "modifiedDate" };
    }

    /**
     * The name of the properties are exposed here
     */
    @Override
    public Type[] getPropertyTypes() {
        //createdBy, createdDate,modifiedBy,modifiedDate
        return new Type[] { Hibernate.INTEGER,
                Hibernate.TIMESTAMP, Hibernate.INTEGER,
                Hibernate.TIMESTAMP };
    }

    /**
     * This will return the individual value of the property
     */
    @Override
    public Object getPropertyValue(final Object component, final int property)
            throws HibernateException {
        Object returnValue = null;
        final AuditData auditData = (AuditData) component;
        if (0 == property) {
            returnValue = auditData.getCreatedBy();
        } else if (1 == property) {
            returnValue = auditData.getCreatedDate();
        } else if (2 == property) {
            returnValue = auditData.getModifiedBy();
        } else if (3 == property) {
            returnValue = auditData.getModifiedDate();
        }
        return returnValue; 
    }

    /**
     * This method is called to convert the sql column data into Java model object
     */
    @Override
    public void setPropertyValue(final Object component, final int property,
            final Object setValue) throws HibernateException {
        final AuditData auditData = (AuditData) component;
        if (0 == property) {
            final Integer createdBy = (Integer) setValue;
            auditData.setCreatedBy(createdBy);
        } else if (1 == property) {
            final Date createdDate = (Date) setValue;
            auditData.setCreatedDate(createdDate);
        } else if (2 == property) {
            final Integer modifiedBy = (Integer) setValue;
            auditData.setModifiedBy(modifiedBy);
        } else if (3 == property) {
            final Date modifiedDate = (Date) setValue;
            auditData.setModifiedDate(modifiedDate);
        }
        
    }

    /**
     * This is called so as to retrieve the values from the sql resultset
     */
    @Override
    public Object nullSafeGet(final ResultSet resultSet,
            final String[] names,
            final SessionImplementor paramSessionImplementor, final Object paramObject)
            throws HibernateException, SQLException {
        //owner here is of type TestUser or the actual owning Object
        AuditData auditData = null;
        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;
    }

    /**
     * Before executing the save call this method is called. It will set the
     * values in the prepared statement
     */
    @Override
    public void nullSafeSet(final PreparedStatement preparedStatement,
            final Object value, final int property,
            final SessionImplementor sessionImplementor)
            throws HibernateException, SQLException {
        if (null == value) {
            preparedStatement.setNull(property, Hibernate.INTEGER.sqlType());
            preparedStatement.setNull(property + 1, Hibernate.TIMESTAMP.sqlType());
            preparedStatement.setNull(property + 2, Hibernate.INTEGER.sqlType());
            preparedStatement.setNull(property + 3, Hibernate.TIMESTAMP.sqlType());
        } else {
            final AuditData auditData = (AuditData) value;
            preparedStatement.setInt(property, auditData.getCreatedBy());
            preparedStatement.setTimestamp(property + 1, new Timestamp(auditData.getCreatedDate().getTime()));
            preparedStatement.setInt(property + 2, auditData.getModifiedBy());
            preparedStatement.setTimestamp(property + 3, new Timestamp(auditData.getModifiedDate().getTime()));
        }
    }

    /**
     * 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,
            final SessionImplementor paramSessionImplementor)
            throws HibernateException {
        //Thus the data Types must implement serializable
        return (Serializable) value;
    }

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

    /**
     * Method is called when merging two objects.
     */
    @Override
    public Object replace(final Object original, final Object target,
            final SessionImplementor paramSessionImplementor, 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);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Class returnedClass() {
        return AuditData.class;
    }

    /**
     * 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 = false;
        }
        if (null == o1 || null == o2) {
            isEqual = false;
        } else {
            isEqual = o1.equals(o2);
        }
        return isEqual;
    }

    @Override
    public int hashCode(final Object value) throws HibernateException {        
        return value.hashCode();
    }

    /**
     * Helps hibernate apply certain optimizations for immutable objects
     */
    @Override
    public boolean isMutable() {
        return true;
    }
    
    /**
     * Used to create Snapshots of the object
     */
    @Override
    public Object deepCopy(final Object value) throws HibernateException {
//        return value; if object was immutable we could return the object as its is
        final AuditData recievedParam = (AuditData) value;
        final AuditData auditData = new AuditData(recievedParam);        
        return auditData;
    }
    
}
As can be seen in the above code additional methods are available with this interface. 
  1. The getPropertyNames() returns the properties present in this new type. 
  2. The getPropertyTypes() indicates the type of each property. 
  3. The getPropertyValue() and setPropertyValue() methods allows for access and modification of these individual properties.
The only change in the mapping file is the specification of the customType.
<?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"> -->
        <property name = "auditData" type ="com.customtype.CompositeAuditType"> 
            <column name="Created_By" />
            <column name="Created_Date" length="19" />
            <column name="Modified_By" />
            <column name="Modified_Date" length="19" />
        </property>
    </class>
</hibernate-mapping>
On start up the logs(cleaned) are as below:
1031 [main] INFO  org.hibernate.cfg.HbmBinder  - Mapping class: com.model.TypedU
ser -> USER
1047 [main] DEBUG org.hibernate.cfg.HbmBinder  - Mapped property: id -> ID
1063 [main] DEBUG org.hibernate.cfg.HbmBinder  - Mapped property: name -> NAME
1063 [main] DEBUG org.hibernate.cfg.HbmBinder  - Mapped property: auditData -> C
reated_By, Created_Date, Modified_By, Modified_Date
On executing an hql query to retrieve records with created_By value as 1:
static void testFetchByQuery() {
    Session session = SESSION_FACTORY.openSession();
    try {
        Query query = session.createQuery("from TypedUser user where user.auditData.createdBy = 1");
        TypedUser user = (TypedUser) query.uniqueResult();
        System.out.println("Name : " + user.getName() + " Audit details : " 
                + user.getAuditData() );
    } catch (HibernateException e) {
        e.printStackTrace();
    } finally {
        session.close();
    }
}
Logs:
6828 [main] DEBUG org.hibernate.engine.query.QueryPlanCache  - unable to locate 
HQL query plan in cache; generating (from TypedUser user where user.auditData.cr
eatedBy = 1)
7031 [main] DEBUG org.hibernate.hql.ast.QueryTranslatorImpl  - parse() - HQL: fr
om com.model.TypedUser user where user.auditData.createdBy = 1
7094 [main] DEBUG org.hibernate.hql.ast.AST  - --- HQL AST ---
 \-[QUERY] 'query'
    +-[SELECT_FROM] 'SELECT_FROM'
    |  \-[FROM] 'from'
    |     \-[RANGE] 'RANGE'
    |        +-[DOT] '.'
    |        |  +-[DOT] '.'
    |        |  |  +-[IDENT] 'com'
    |        |  |  \-[IDENT] 'model'
    |        |  \-[IDENT] 'TypedUser'
    |        \-[ALIAS] 'user'
    \-[WHERE] 'where'
       \-[EQ] '='
          +-[DOT] '.'
          |  +-[DOT] '.'
          |  |  +-[IDENT] 'user'
          |  |  \-[IDENT] 'auditData'
          |  \-[IDENT] 'createdBy'
          \-[NUM_INT] '1'

7281 [main] DEBUG org.hibernate.hql.ast.tree.FromElement  - handling property de
reference [com.model.TypedUser (user) -> auditData (class)]
7281 [main] DEBUG org.hibernate.hql.ast.tree.DotNode  - getDataType() : auditDat
a -> org.hibernate.type.CompositeCustomType@1388e5e
7281 [main] DEBUG org.hibernate.hql.ast.tree.DotNode  - Unresolved property path
 is now 'auditData.createdBy'
7281 [main] DEBUG org.hibernate.hql.ast.tree.FromReferenceNode  - Resolved :  us
er.auditData -> typeduser0_.Created_By
7297 [main] DEBUG org.hibernate.hql.ast.tree.DotNode  - getDataType() : auditDat
a.createdBy -> org.hibernate.type.IntegerType@ad483
7297 [main] DEBUG org.hibernate.hql.ast.tree.FromReferenceNode  - Resolved :  us
er.auditData.createdBy -> typeduser0_.Created_By
...
7359 [main] DEBUG org.hibernate.hql.ast.QueryTranslatorImpl  - SQL: select typed
user0_.ID as ID0_, typeduser0_.NAME as NAME0_, typeduser0_.Created_By as Created
3_0_, typeduser0_.Created_Date as Created4_0_, typeduser0_.Modified_By as Modifi
ed5_0_, typeduser0_.Modified_Date as Modified6_0_ from USER typeduser0_ where ty
peduser0_.Created_By=1
...
7391 [main] DEBUG org.hibernate.connection.DriverManagerConnectionProvider  - to
tal checked-out connections: 0
7391 [main] DEBUG org.hibernate.connection.DriverManagerConnectionProvider  - us
ing pooled JDBC connection, pool size: 0
7391 [main] DEBUG org.hibernate.SQL  - 
    select
        typeduser0_.ID as ID0_,
        typeduser0_.NAME as NAME0_,
        typeduser0_.Created_By as Created3_0_,
        typeduser0_.Created_Date as Created4_0_,
        typeduser0_.Modified_By as Modified5_0_,
        typeduser0_.Modified_Date as Modified6_0_ 
    from
        USER typeduser0_ 
    where
        typeduser0_.Created_By=1
...
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}]
7578 [main] DEBUG org.hibernate.impl.SessionImpl  - closing session
As can be seen from the logs, the query parser parsed the hql query, identified the attributes and executed the generated sql query to return the result. If the same query was executed using the UserType, the exception thrown is as below:
org.hibernate.QueryException: could not resolve property: createdBy of: com.mode
l.TypedUser [from com.model.TypedUser user where user.auditData.createdBy = 1]
    at org.hibernate.persister.entity.AbstractPropertyMapping.propertyException(Abs
tractPropertyMapping.java:44)
    at org.hibernate.persister.entity.AbstractPropertyMapping.toType(AbstractProper
tyMapping.java:38)
    at org.hibernate.persister.entity.AbstractEntityPersister.toType(AbstractEntity
Persister.java:1358)
    at org.hibernate.hql.ast.tree.FromElementType.getPropertyType(FromElementType.j
ava:279)
    at org.hibernate.hql.ast.tree.FromElement.getPropertyType(FromElement.java:386)

No comments:

Post a Comment