Search This Blog

Wednesday 9 November 2011

Creating A Custom Id Generator

We have already seen the different identifier generators supported by Hibernate. However this does not constrain us to use these generators only. Hibernate very magnanimously allows us to create our own custom generator.
This could have been useful to us in one of the modules in our application. We had an entity wherein the Id was an auto-increment field. However later down the project we were given a requirement that in a certain scenario the id needed to be inserted through code and the auto-increment value was not to be used.
Needless to say we came up with a very complex solution and in hindsight today, I can say that it would have been a lot more simpler (and cleaner) to just implement our own custom id generator.
<?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.generator.custom">
    <class name="Entity" table="ENTITY">
        <id name="id" column="ID" type="long">
            <generator class="com.generator.custom.CrazyIdGenerator" />
        </id>
        <property name="name" type="string">
            <column name="NAME" />
        </property>
    </class>
</hibernate-mapping>
The above hbm file refers to a simple Entity class that is mapped to a two column table. There is nothing special about the mapping except the generator element. The class value points to our own custom implementation. The code for the generator class is as below:
public class CrazyIdGenerator implements IdentifierGenerator {

    /*
     * (non-Javadoc)
     * @see org.hibernate.id.IdentifierGenerator
            #generate(org.hibernate.engine.SessionImplementor, java.lang.Object)
     */
    @Override
    public Serializable generate(SessionImplementor session, Object object)
            throws HibernateException {
        final String CALL_PROC_QUERY = "call getId(?,?)";
        Long nextValue = null;
        try {
            final CallableStatement callableStmt = session.getBatcher()
                    .prepareCallableStatement(CALL_PROC_QUERY);
            callableStmt.registerOutParameter(1, java.sql.Types.BIGINT);
            callableStmt.setString(2, ((Entity) object).getName());
            callableStmt.executeQuery();

            nextValue = callableStmt.getLong(1);
            session.getBatcher().closeStatement(callableStmt);
        } catch (SQLException sqlException) {
            throw JDBCExceptionHelper.convert(session.getFactory()
                    .getSQLExceptionConverter(), sqlException,
                    "could not fetch identifier !! ", CALL_PROC_QUERY);
        }
        return nextValue;
    }

}
Our class implements the IdentifierGenerator interface. The interface includes one method that receives access to the current session and the new object to be saved. It returns the new Identifier. Hibernate lets us connect to the database if necessary to create the identifier. If not needed, we can put in any logic in the world to generate a suitable value to save as the identifier. in the above code, I execute a stored procedure that returns me a long value, which I then use to save my object.
delimiter |
 CREATE PROCEDURE getId(OUT newId BIGINT(20),IN Name VARCHAR(255)) 
 BEGIN        
 SET newId = RAND() * LENGTH(Name);
 END|
 delimiter ; 
 
CALL getId(@total,'in  process');
SELECT @total AS  total_in_process;
As seen from the above, the procedure does nothing smart. It just returns a random number between 0 and the length of the string. Which also means that there is a very good chance I shall be returning ids that already exist in the table and my save will fail. (I did say the procedure wasn't very smart :P )
However the main point is that now Hibernate uses our custom generator to create identifiers for objects. I executed a sample code to create a record in the ENTITY table.
public static void createEntity() {
    Entity entity = new Entity();
    entity.setName("sahndjka");
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    session.save(entity);
    transaction.commit();
    session.close();
    System.out.println("Entity saved with id " + entity.getId());
}
The logs indicate the following
643  [main] DEBUG org.hibernate.event.def.DefaultSaveOrUpdateEventListener 
- saving transient instance
643  [main] DEBUG org.hibernate.jdbc.AbstractBatcher  -
about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
644  [main] DEBUG org.hibernate.SQL  - 
    call getId(?,?)
Hibernate: 
    call getId(?,?)
644  [main] DEBUG org.hibernate.jdbc.AbstractBatcher  - preparing callable statement
670  [main] DEBUG org.hibernate.jdbc.AbstractBatcher 
- about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
671  [main] DEBUG org.hibernate.jdbc.AbstractBatcher  - closing statement
671  [main] DEBUG org.hibernate.event.def.AbstractSaveEventListener 
- generated identifier: 5, using strategy: com.generator.custom.CrazyIdGenerator
672  [main] DEBUG org.hibernate.event.def.AbstractSaveEventListener 
- saving [com.generator.custom.Entity#5]
680  [main] DEBUG org.hibernate.transaction.JDBCTransaction  - commit
...
683  [main] DEBUG org.hibernate.pretty.Printer 
- com.generator.custom.Entity{id=5, name=sahndjka}
...
685  [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        ENTITY
        (NAME, ID) 
    values
        (?, ?)
Entity saved with id 5
The above code had failed with a different version of mysql-connector jar. It did not work with mysql-connector-java-5.1.13-bin.jar, hence I switched to mysql-connector-java-5.1.22-bin.jar and it worked.
I also wanted to see where in the whole process is my method called.  Running the code with a break-point indicated the entire stack trace in Eclipse.
The break-point also shows the values received. as can be seen, the actual object passed to the SessionImplementor was an instance of the Hibernate session, possibly the same one on which I had called save.

3 comments: