IdentifierGeneratorHelper.getGeneratedIdentity() assumes that id column will always be at index 1

Description

If the database columns are created such that the serial id is not the first one, then the EntityManager.persist(Object) method sets the wrong value on the entity's @Id field (it sets it to the value of the first db column). For example:

Create the table:

CREATE TABLE info.rmbtest_course2
(
fee integer,
id bigserial NOT NULL,
starttime timestamp without time zone,
title character varying(100) NOT NULL,
CONSTRAINT rmbtest_course2_pkey PRIMARY KEY (id)
)

Note that the id column is the second column.

Create the entity:
package testhibernate.course;

import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import org.joda.time.DateTime;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;

@Entity(name = "course")
@Table(name = "rmbTest_course2", schema = "info")
@NamedQueries(@NamedQuery(name = "Course.findByTest", query = "from course"))
public class Course {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ID")
private final Long id;

@Column(name = "TITLE", length = 100, nullable = false)
private final String title;

@Column(name = "FEE")
private final int fee;

@Column(name = "startTime")
@Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
private final DateTime startTime;

public Course(final String title, final int fee, final DateTime startTime) {

this.id = null;
this.title = title;
this.fee = fee;
this.startTime = startTime;
}

/**

  • Required by JPA
    */
    public Course() {
    id = null;
    title = null;
    fee = 0;
    startTime = null;
    }

public CourseKey getKey() {

return new CourseKey(id);
}

public String getTitle() {

return title;
}

public int getFee() {

return fee;
}

public DateTime getStartTime() {

return startTime;
}

@Override
public String toString()
{
return "Course{" +
"id=" + id +
", title='" + title + '\'' +
", fee=" + fee +
", startTime=" + startTime +
'}';
}
}

Run this code:

Course course = new Course("Core Spring", 1000, new DateTime());

course = myRepository.save(course);

System.out.println("key = " + course.getKey());

In this case the returned course.getKey() should've been the auto allocated serial id, but it is 1000, i.e. the first column in the table.

The problem is that IdentifierGeneratorHelper.get(ResultSet rs, Type type) assumes that the id column is always the first column.

As a workaround I have set my entities @Id annotations to:

...
public class Course {

@Id
@GeneratedValue(generator = "myGenerator")
@GenericGenerator(name = "myGenerator", strategy = "testhibernate.MyGenerator")
@Column(name = "ID")
private final Long id;
...

and had the following Generator code:

package testhibernate;

import org.hibernate.HibernateException;
import org.hibernate.dialect.Dialect;
import org.hibernate.id.IdentifierGenerationException;
import org.hibernate.id.IdentifierGeneratorHelper;
import org.hibernate.id.IdentityGenerator;
import org.hibernate.id.PostInsertIdentityPersister;
import org.hibernate.id.ResultSetIdentifierConsumer;
import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate;
import org.hibernate.persister.entity.SingleTableEntityPersister;
import org.hibernate.type.CustomType;
import org.hibernate.type.Type;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class MyGenerator extends IdentityGenerator
{
@Override
public InsertGeneratedIdentifierDelegate getInsertGeneratedIdentifierDelegate(final PostInsertIdentityPersister persister,
final Dialect dialect,
final boolean isGetGeneratedKeysEnabled) throws HibernateException
{
final InsertGeneratedIdentifierDelegate result;

if(isGetGeneratedKeysEnabled)
{
result = new MyGetGeneratedKeysDelegate(persister, dialect);
}
else
{
result = super.getInsertGeneratedIdentifierDelegate(persister, dialect, isGetGeneratedKeysEnabled);
}

return result;
}

private static class MyGetGeneratedKeysDelegate extends GetGeneratedKeysDelegate
{
private final PostInsertIdentityPersister persister;

private MyGetGeneratedKeysDelegate(final PostInsertIdentityPersister persister, final Dialect dialect)
{
super(persister, dialect);

assert persister != null;

this.persister = persister;
}

private Serializable getGeneratedIdentityByColumnName(ResultSet rs,
Type type,
String columnName) throws SQLException, HibernateException {
if ( !rs.next() ) {
throw new HibernateException( "The database returned no natively generated identity value" );
}

final Serializable id = get(rs, type, columnName);
// todo log.debug( "Natively generated identity: " + id );
return id;
}

private Serializable get(ResultSet rs, Type type, String columnName) throws SQLException, IdentifierGenerationException
{
if ( ResultSetIdentifierConsumer.class.isInstance( type ) )
{
return ( ( ResultSetIdentifierConsumer ) type ).consumeIdentifier( rs );
}

if ( CustomType.class.isInstance( type ) )
{
final CustomType customType = (CustomType) type;
if ( ResultSetIdentifierConsumer.class.isInstance( customType.getUserType() ) ) {
return ( (ResultSetIdentifierConsumer) customType.getUserType() ).consumeIdentifier( rs );
}
}

Class<?> clazz = type.getReturnedClass();
if ( clazz == Long.class )
{
return rs.getLong(columnName);
}
else if ( clazz == Integer.class )
{
return rs.getInt(columnName);
}
else if ( clazz == Short.class )
{
return rs.getShort(columnName);
}
else if ( clazz == String.class )
{
return rs.getString( columnName );
}
else if ( clazz == BigInteger.class )
{
return rs.getBigDecimal( columnName ).setScale( 0, BigDecimal.ROUND_UNNECESSARY ).toBigInteger();
}
else if ( clazz == BigDecimal.class )
{
return rs.getBigDecimal( columnName ).setScale( 0, BigDecimal.ROUND_UNNECESSARY );
}
else
{
throw new IdentifierGenerationException("unrecognised id type : " + type.getName() + " -> " + clazz.getName());
}
}

@Override
public Serializable executeAndExtract(final PreparedStatement insert) throws SQLException
{
insert.executeUpdate();

ResultSet rs = insert.getGeneratedKeys();
try
{
final Type identifierType = persister.getIdentifierType();

Serializable result = null;
boolean useDefaultTechnique = true;

if(persister instanceof SingleTableEntityPersister)
{
final String[] idColumnNames = ((SingleTableEntityPersister)persister).getIdentifierColumnNames();
if(idColumnNames.length == 1)
{
// do it by column name
result = getGeneratedIdentityByColumnName(rs, identifierType, idColumnNames[0] );

useDefaultTechnique = false;
}
else
{
// todo - log
}
}
else
{
// todo - log
}

if(useDefaultTechnique)
{
result = IdentifierGeneratorHelper.getGeneratedIdentity(rs, identifierType);
}

return result;
}
finally
{
rs.close();
}
}
}

}

This seems to do the trick but is obviously an ugly hack.

Activity

Brett MeyerJuly 8, 2014 at 3:10 PM

Bulk rejecting stale issues. If this is still a legitimate issue on ORM 4, feel free to comment and attach a test case. I'll address responses case-by-case. Thanks!

Brett MeyerApril 7, 2014 at 5:46 PM

In an effort to clean up, in bulk, tickets that are most likely out of date, we're transitioning all ORM 3 tickets to an "Awaiting Test Case" state. Please see http://in.relation.to/Bloggers/HibernateORMJIRAPoliciesAndCleanUpTactics for more information.

If this is still a legitimate bug in ORM 4, please provide either a test case that reproduces it or enough detail (entities, mappings, snippets, etc.) to show that it still fails on 4. If nothing is received within 3 months or so, we'll be automatically closing them.

Thank you!

Tyler ColesFebruary 28, 2014 at 12:49 AM

I just got bit by what appears to be this issue. I didn't try the above workaround but instead re-ordered my table's columns to resolve the issue. This is a pretty nasty bug to be open for so long. We're using Hibernate 4.2.6.Final.

Former userApril 10, 2012 at 4:04 AM

This should be re-tested. I have just upgraded my hibernate version to 4.1.2 and the problem seems to have gone (i.e. I don't need my workaround anymore). Previous version (that was broken) was 4.0.1 Final, working version is:

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.1.2.Final</version>
</dependency>

Max ChebanDecember 27, 2011 at 3:03 PM

It's not neccesary to retrieve the value from ResultSet by column name, you just need to provide proper RETURNING column order.

Method GetGeneratedKeysDelegate.prepare(String, SessionImplementor) should be rewritten as following:

protected PreparedStatement prepare(String insertSQL, SessionImplementor session) throws SQLException {
return session.getBatcher().prepareStatement( insertSQL, persister.getRootTableKeyColumnNames() );
}

Rejected

Details

Assignee

Reporter

Components

Affects versions

Priority

Created August 15, 2011 at 6:17 AM
Updated July 8, 2014 at 3:10 PM
Resolved July 8, 2014 at 3:10 PM