Search This Blog

Sunday, 11 September 2011

Id Generators - 3

Let us look at at the native and increment types in this post.

native

Consider the simple POJO person that represents a Person table
public class Person {
    private Integer id;
    private String name;
    private Integer age;
    //setter getters
}
The mapping file for the same would be
<?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="Person" table="PERSON">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="native" />
        </id>
        <property name="name" type="string">
            <column name="NAME" />
        </property>
        <property name="age" type="integer">
            <column name="AGE" />
        </property>
    </class>
</hibernate-mapping> 
When the native generator is specified,Hibernate picks identity, sequence or hilo depending upon the capabilities of the underlying database. With MySQL, the native generator will default to an identity strategy i.e. it will use identity columns, which are per table.
The sql script for table (same as that used for identity)
create table PERSON  (
  ID int(11) not null auto_increment,
  NAME varchar(100) not null,
  AGE int(11) not null,
  PRIMARY KEY (ID)
  );
On executing a test function to create a record in the table the logs give the details
3391 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ver
sion select: select ID from PERSON where ID =?
3391 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Sna
pshot select: select person_.ID, person_.NAME as NAME0_, person_.AGE as AGE0_ fr
om PERSON person_ where person_.ID=?
3391 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ins
ert 0: insert into PERSON (NAME, AGE, ID) values (?, ?, ?)
3391 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Upd
ate 0: update PERSON set NAME=?, AGE=? where ID=?
3391 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Del
ete 0: delete from PERSON where ID=?
3391 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  -  Ide
ntity insert: insert into PERSON (NAME, AGE) values (?, ?) 
...

272032 [main] DEBUG org.hibernate.event.def.AbstractSaveEventListener  - executi
ng identity-insert immediately
272032 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  - In
serting entity: com.model.Person (native id)
272047 [main] DEBUG org.hibernate.jdbc.AbstractBatcher  - about to open Prepared
Statement (open PreparedStatements: 0, globally: 0)
272047 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        PERSON
        (NAME, AGE) 
    values
        (?, ?)
...
272125 [main] DEBUG org.hibernate.id.IdentifierGeneratorFactory  - Natively gene
rated identity: 1
As can be seen , Hibernate identified the generator strategy as native and based on the dialect used (), it opted to use the identity strategy. The sql generated for insert is same as identity- it does not include the id field. The id value is returned by jdbc code executed to create the record.

increment

This type is capable of generating identifiers of type long, int or short. Let us start with a simple POJO class City
public class City {
    private Long id;
    private String name;
    private String code;
    //setter getters
}
The mapping file is as follows:
<?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="City" table="CITY">
        <id name="id" type="long">
            <column name="ID" />
              <generator class="increment" />
        </id>
        <property name="name" type="string">
            <column name="NAME" />
        </property>
        <property name="code" type="string">
            <column name="CODE" />
        </property>
    </class>
</hibernate-mapping> 

The sql script for the table is as below:

create table CITY  (
  ID int(11) not null,
  NAME varchar(100) not null,
  CODE varchar(100) not null
);
alter table CITY add constraint CITY_PK primary key (ID);


To create a single record in the database the code would be as follows
public static void testCreateCities() {
    City city = new City();//No need to assign. Hibernate will manage the ids
    city.setName("NewCity");
    city.setCode("Code-1");
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    session.save(city);
    transaction.commit();
}
The above code does not set any value for the id field. On executing this function on start up, the logs give us the following details:

149922 [main] DEBUG org.hibernate.jdbc.JDBCContext  - after transaction begin
150656 [main] DEBUG org.hibernate.event.def.DefaultSaveOrUpdateEventListener  - 
saving transient instance
150656 [main] DEBUG org.hibernate.id.IncrementGenerator  - fetching initial valu
e: select max(ID) from CITY
150656 [main] DEBUG org.hibernate.jdbc.AbstractBatcher  - about to open Prepared
Statement (open PreparedStatements: 0, globally: 0)
150656 [main] DEBUG org.hibernate.SQL  - 
    select
        max(ID) 
    from
        CITY
150656 [main] DEBUG org.hibernate.jdbc.AbstractBatcher  - preparing statement
150734 [main] DEBUG org.hibernate.id.IncrementGenerator  - first free id: 1
...
152375 [main] DEBUG org.hibernate.pretty.Printer  - com.model.City{id=1, name=Ne
wCity, code=Code-1}
...
152375 [main] DEBUG org.hibernate.persister.entity.AbstractEntityPersister  - In
serting entity: [com.model.City#1]
152375 [main] DEBUG org.hibernate.jdbc.AbstractBatcher  - about to open Prepared
Statement (open PreparedStatements: 0, globally: 0)
152375 [main] DEBUG org.hibernate.SQL  - 
    insert 
    into
        CITY
        (NAME, CODE, ID) 
    values
        (?, ?, ?)
As can be seen at the beginning of the transaction, Hibernate retrieves the highest value of id from the table. It then uses the next higher value to insert records in the database. The id field is passed from java to the database as can be seen from the logs.
The query to fetch the maximum id is not executed at the beginning of every transaction. For the very first create transaction, Hibernate checks if the current maximum id is available with it. If not it fetches this value from the database. It also manages this value locally from here onwards. For any other create operation that occurs in the code (within same or different transaction) logs similar to this can be observed:
7937 [main] DEBUG org.hibernate.jdbc.JDBCContext  - after transaction begin
7937 [main] DEBUG org.hibernate.event.def.DefaultSaveOrUpdateEventListener  - sa
ving transient instance
7937 [main] DEBUG org.hibernate.event.def.AbstractSaveEventListener  - generated
 identifier: 5, using strategy: org.hibernate.id.IncrementGenerator
This makes perfect sense as the query to fetch id will not be necessary since it is hibernate alone that is doing the inserts. 
This is also the exact reason why the generator cannot be used in  a cluster environment. If multiple sources are performing inserts on the same table, there is a very good chance that the hibernate generated id already exists in the database. This will lead to a duplicate key exception. I managed to reproduce the above scenario and here is a look at the failure logs:
2485 [main] DEBUG org.hibernate.util.JDBCExceptionReporter  - Could not execute
 JDBC batch update [insert into CITY (NAME, CODE, ID) values (?, ?, ?)]
java.sql.BatchUpdateException: Duplicate entry '8' for key 'PRIMARY'
    at com.mysql.jdbc.PreparedStatement.executeBatchSerially(PreparedStatement.java
:2020)
    at com.mysql.jdbc.PreparedStatement.executeBatch(PreparedStatement.java:1451)
    at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:48)
    at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:246)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:237)
    at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:141)
    at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(Abst
ractFlushingEventListener.java:298)
    at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventL
istener.java:27)
    at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1000)
    at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:338)
    at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
    at test.model.TestCity.testCreateCities(TestCity.java:37)
    at test.model.TestCity.main(TestCity.java:18)
22485 [main] WARN  org.hibernate.util.JDBCExceptionReporter  - SQL Error: 1062, 
SQLState: 23000
22485 [main] ERROR org.hibernate.util.JDBCExceptionReporter  - Duplicate entry '
8' for key 'PRIMARY'
22485 [main] ERROR org.hibernate.event.def.AbstractFlushingEventListener  - Coul
d not synchronize database state with session
This is one reason I feel this generator should never be used anywhere other than in tutorials. However much we convince ourselves that a project is a simple project and will not run into needs for multiple server deployments, we can never be sure. If the need did ever arise for deployment on multiple nodes, then there is going to be a lot of unnecessary code change.

No comments:

Post a Comment