Wednesday 20 February 2008

UUID and Hibernate

Before I begin, apologies for the poor formatting of this post. It is late and my few attempts to edit the HTML directly have not been too good, so I figured I would work out how to format correctly on subsequent posts, if I have the time.

I have been playing around with a postgresql DB, running v8.3 with the new uuid data type. This now means that most of the commonly used DB's support a uuid data type in various forms (SQL Server - uniqueidentifier, Oracle, MySQL, and PostgreSql - uuid, with DB2 yet to do so. Not sure on the support for the other DB engines).

Additionally, since Java 1.5, Java has supported the generation of UUID's with the java.util.UUID class.

However, the implementation (storage and interface) of UUID's differs from DB to DB, and the java.sql.Types has no GUID/UUID data type. When using JDBC, UUID strings work with no problems at all with the JDBC 4 postgresql driver and Java 6.

Unfortunately, Hibernate attempts to be too clever, and as the PostgreSql UUID datatype is unknown to it, its reverse engineering tools treats it as a Serializable oject. So on interaction with the database, it fails in three ways.

  1. There is a datatype mismatch, which requires you to create a custom hibernate data type.
  2. Hibernate treats the data as a bytea datatype, as it is Serializable, so you need to change the java datatype in the Entity object to String and the sql-type in the *.hbm.xml to "uuid".
  3. The default uuid.hex generator in hibernate does not generate a correctly formatted uuid string. The default is with no hyphens. You can instruct the uuid generator to use hyphens, but when it does, it is in an incorrect custom built 8-8-4-8-4 format, instead of the strict 8-4-4-4-12 that postgresql requires. So you also need to create your own custom UUID generator.
Essentially, in the *.hbm.xml you would see something similar to the following:

<id name="nameOfField" type="FullyQualifiedPath.CustomUUIDType">
<column length="36" name="name_of_column" type="uuid"/>
<generator class="FullyQualifiedPath.CustomUUIDGenerator"/>


And in the Entity class, something similar to:

private String NameOfField;

public String get
NameOfField() {
return this.
NameOfField;
}

public void set
NameOfField(String NameOfField) {
this.parent =
NameOfField;
}


And naturally, a custom UUID Type that implements org.hibernate.usertype.UserType. The following is my implementation (which still requires a little work):

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.UUID;

import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;

public class CustomUUIDType implements UserType {

private static final int SQL_TYPE = Types.OTHER;
private static final String DB_OBJECT_TYPE = "uuid";

@Override
public Object assemble(Serializable cached, Object owner)
throws HibernateException {
//I don't fully understand what I am meant to do to honour this contract.
//So at present, just returning the cloned cached object as is.
//It is optional anyway.
return deepCopy(cached);
}

@Override
public Object deepCopy(Object value) throws HibernateException {
Object retValue = null;

if (value != null && value instanceof String) {
try {
//make sure that it is a correctly formatted value before returning it
UUID.fromString((String) value);
//as Strings are immutable, returning a new reference to the value is effectively cloning it
retValue = value;
} catch (IllegalArgumentException e) {
//return null if value is not correctly formatted,
//but potentially should return an Exception.
}
}
return retValue;
}

@Override
public Serializable disassemble(Object value) throws HibernateException {
//I don't fully understand what I am meant to do to honour this contract.
//So at present, just returning the cloned value object as is.
//It is optional anyway.
return (Serializable) deepCopy(value);
}

@Override
public boolean equals(Object x, Object y) throws HibernateException {
boolean eq = false;

if (x != null && y != null
&& x instanceof String && y instanceof String) {
try {
UUID lhs = UUID.fromString((String) x);
UUID rhs = UUID.fromString((String) y);

eq = lhs.equals(rhs);
} catch (IllegalArgumentException e) {}
}

return eq;
}

@Override
public int hashCode(Object x) throws HibernateException {
return (x != null ? x.hashCode() * 33 : 1234567890);
}

@Override
public boolean isMutable() {
return true;
}

@Override
public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
throws HibernateException, SQLException {
Object retValue = null;

assert names.length == 1;

String uuidValue = rs.getString(names[0]);
if (rs.wasNull()) {
retValue = null;
} else {
try {
//make sure that it is a correctly formatted value before returning it
UUID.fromString(uuidValue);
retValue = uuidValue;
} catch (IllegalArgumentException e) {
//return null if data retrieved is not correctly formatted,
//but potentially should return an Exception.
}
}

return retValue;
}

@Override
public void nullSafeSet(PreparedStatement st, Object value, int index)
throws HibernateException, SQLException {
if (value == null) {
st.setNull(index, SQL_TYPE, DB_OBJECT_TYPE);
} else {
try {
//make sure that it is a correctly formatted value before returning it
UUID.fromString((String) value);
st.setObject(index, value, SQL_TYPE);
} catch (IllegalArgumentException e) {
//set null if data being set is not correctly formatted,
//but potentially should return an Exception.
st.setNull(index, SQL_TYPE, DB_OBJECT_TYPE);
}
}
}

@Override
public Object replace(Object original, Object target, Object owner)
throws HibernateException {
return original;
}

@Override
public Class returnedClass() {
return String.class;
}

@Override
public int[] sqlTypes() {
return new int[] {SQL_TYPE};
}
}

And of course the custom UUID Generator which implements the org.hibernate.id.IdentifierGenerator:

import java.io.Serializable;
import java.util.UUID;

import org.hibernate.HibernateException;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.id.IdentifierGenerator;

public class CustomUUIDGenerator implements IdentifierGenerator {

@Override
public Serializable generate(SessionImplementor arg0, Object arg1)
throws HibernateException {
return UUID.randomUUID().toString();
}
}

And yes, I will discuss the whole issue of using surrogate keys, UUID's and collisions, etc, in another post.

So I have worked through all of this and got it working a treat with *hbm.xml files. But do you think I can do the same with annotations? It is just not working for me. I have everything working, but the custom UUID generator.

The following are the annotations that I'm using:

@Id
// @GeneratedValue(generator="system-uuid")
// @GenericGenerator(name="system-uuid", strategy = "uuid")
@GeneratedValue(generator="
FullyQualifiedPath.CustomUUIDGenerator")
@Column(name = "id", unique = true, nullable = false, length = 36, columnDefinition="uuid")
@Type(type="
FullyQualifiedPath.CustomUUIDType")
@NotNull
public String getId() {
return this.id;
}

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

The custom type seems to be working fine, as I can retrieve the data from the DB, however, the UUID generator does not appear to be working at all. Hibernate is not even returning an error on an attempted insert. So that is as far as I have got at this stage in getting the generator working using annotations. Hopefully my next post will have the solution, but if any of my fantasised legions of readers were to have the solution or an idea of what I'm missing, feedback would be hugely appreciated. But my guess is that I will need to ask on the forums to find a solution...

1 comment:

Unknown said...

I've been trying to figure this out for about 4 days, and the only thing I haven't tried is to upgrade to the latest java and jdbc... you would think that postgres would post this info with the release notes! I'm tired now, so I will try your method tomorrow.