| « Stripping line feeds from dos files | How can I do lazy loading with a one-to-one relationship in hibernate? » |
java.util.UUID primary keys in hibernate
November 1st, 2008Surprisingly Hibernate 3 does not support UUID’s. I think the main reason for this is that UUID is not yet an ANSI database type. Each database manufacturer has implemented it in a different way, stored it as a slightly different type and has a driver (or set of drivers) that handle it differently!
For example, MySQL has a UUID() function that returns a 36 character string in the form aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee and expects you to store it in a VARCHAR(36) where as the .NET driver for MySQL will spot that a database column is a BINARY(16) and automatically convert it for you.
There follows a post explaining how to plug a java.util.UUID key into Hibernate with a couple of the pitfalls that I fell into.
There are several supporting classes that you need to write, and a bit of configuration:
- Serialiser and de-serialiser for the java.util.UUID class
- java.util.UUID generator
- Hibernate type
The UUIDUserType class below was mostly lifted from the Springframework forum site. Thanks bessette for the starting point. I have added the corrections he discovered.
This class takes care of converting a java.util.UUID into something that the database column can coup with and converting the contents of a database column into a normal java.util.UUID. More information can be found in the JavaDoc for UserType.
Code:
package uk.co.bigsoft.project.db.types ; | |
| |
| |
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 UUIDUserType implements UserType | |
{ | |
| |
private static final String CAST_EXCEPTION_TEXT = " cannot be cast to a java.util.UUID." ; | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#assemble(java.io.Serializable, | |
* java.lang.Object) | |
*/ | |
public Object assemble (Serializable cached, Object owner) throws HibernateException | |
{ | |
| |
if (!String.class.isAssignableFrom (cached.getClass ())) | |
{ | |
return null ; | |
} | |
| |
return UUID.fromString ((String) cached) ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#deepCopy(java.lang.Object) | |
*/ | |
public Object deepCopy (Object value) throws HibernateException | |
{ | |
| |
if (!UUID.class.isAssignableFrom (value.getClass ())) | |
{ | |
throw new HibernateException (value.getClass ().toString () + CAST_EXCEPTION_TEXT) ; | |
} | |
| |
UUID other = (UUID) value ; | |
| |
return UUID.fromString (other.toString ()) ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#disassemble(java.lang.Object) | |
*/ | |
public Serializable disassemble (Object value) throws HibernateException | |
{ | |
| |
return value.toString () ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#equals(java.lang.Object, | |
* java.lang.Object) | |
*/ | |
public boolean equals (Object x, Object y) throws HibernateException | |
{ | |
if (x == y) | |
return true ; | |
| |
if (!UUID.class.isAssignableFrom (x.getClass ())) | |
{ | |
throw new HibernateException (x.getClass ().toString () + CAST_EXCEPTION_TEXT) ; | |
} | |
else if (!UUID.class.isAssignableFrom (y.getClass ())) | |
{ | |
| |
throw new HibernateException (y.getClass ().toString () + CAST_EXCEPTION_TEXT) ; | |
} | |
| |
UUID a = (UUID) x ; | |
UUID b = (UUID) y ; | |
| |
return a.equals (b) ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#hashCode(java.lang.Object) | |
*/ | |
public int hashCode (Object x) throws HibernateException | |
{ | |
if (!UUID.class.isAssignableFrom (x.getClass ())) | |
{ | |
throw new HibernateException (x.getClass ().toString () + CAST_EXCEPTION_TEXT) ; | |
} | |
| |
return x.hashCode () ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#isMutable() | |
*/ | |
public boolean isMutable () | |
{ | |
| |
return false ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#nullSafeGet(java.sql.ResultSet, | |
* java.lang.String[], java.lang.Object) | |
*/ | |
public Object nullSafeGet (ResultSet rs, String[] names, Object owner) throws HibernateException, | |
SQLException | |
{ | |
| |
String value = rs.getString (names[0]) ; | |
if (value == null) | |
{ | |
return null ; | |
} | |
else | |
{ | |
return UUID.fromString (value) ; | |
} | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see | |
* org.hibernate.usertype.UserType#nullSafeSet(java.sql.PreparedStatement, | |
* java.lang.Object, int) | |
*/ | |
public void nullSafeSet (PreparedStatement st, Object value, int index) | |
throws HibernateException, SQLException | |
{ | |
| |
if (value == null) | |
{ | |
st.setNull (index, Types.VARCHAR) ; | |
return ; | |
} | |
| |
if (!UUID.class.isAssignableFrom (value.getClass ())) | |
{ | |
throw new HibernateException (value.getClass ().toString () + CAST_EXCEPTION_TEXT) ; | |
} | |
| |
st.setString (index, value.toString ()) ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#replace(java.lang.Object, | |
* java.lang.Object, java.lang.Object) | |
*/ | |
public Object replace (Object original, Object target, Object owner) throws HibernateException | |
{ | |
| |
if (!UUID.class.isAssignableFrom (original.getClass ())) | |
{ | |
throw new HibernateException (original.getClass ().toString () + CAST_EXCEPTION_TEXT) ; | |
} | |
| |
return UUID.fromString (original.toString ()) ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#returnedClass() | |
*/ | |
@SuppressWarnings("unchecked") | |
public Class returnedClass () | |
{ | |
| |
return UUID.class ; | |
} | |
| |
/* | |
* (non-Javadoc) | |
* @see org.hibernate.usertype.UserType#sqlTypes() | |
*/ | |
public int[] sqlTypes () | |
{ | |
| |
return new int[] { Types.CHAR } ; | |
} | |
} |
We are probably going to be using this UUID type for a lot of things, for example if it is a key field, there will be a corresponding column in another table.
I like one hibernate definition per file (which has so many advantages I could get another blog out of it!), so I need a new file to hold UUIDUserType’s indirection name so that we can reuse it in our other hibernate mapping files.
It would be sensible to call it uuid or guid but this will clash with existing (built-in) indirection names so we must pick another.
XML:
<?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> | |
| |
<typedef name="bsuuid" | |
class="uk.co.bigsoft.project.types.UUIDUserType"> | |
</typedef> | |
| |
</hibernate-mapping> |
The main reason for using UUID’s is that they are unique! So now we need a generator that generates them and returns them in the form we want. Hibernate has 2 uid generators; none of which fit the bill.
The first, uuid, returns a uid in the form “aaaaaaaabbbbccccddddeeeeeeeeeeee” which is generated by hibernate and the second, guid, returns it in the form “aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee” which is generated from a call to the database.
The problem with both of these is that they return the object as a String, which means that your POJO must have the id as a String with the method signature of setId(String id).
Hibernate infers that because the setter for id is a String then id must be a String. We want our id to be a UUID. We don’t want to duplicate getters and setters depending on what situation we are in.
The UuidGenerator below generates a UUID which implements the IdentifierGenerator hibernate interface and allows us to return a real UUID which travels through hibernate as a Serializable object, until it passes through UUIDUserType (above) and ends up in our POJO’s setter as a real UUID.
Code:
package uk.co.bigsoft.project.db.types ; | |
| |
| |
import java.io.Serializable; | |
import java.util.UUID; | |
| |
import org.hibernate.HibernateException; | |
import org.hibernate.engine.SessionImplementor; | |
import org.hibernate.id.IdentifierGenerator; | |
| |
| |
public class UuidGenerator implements IdentifierGenerator | |
{ | |
| |
@Override | |
public Serializable generate (SessionImplementor session, Object parent) | |
throws HibernateException | |
{ | |
UUID u = UUID.randomUUID () ; | |
return u ; | |
} | |
| |
} |
Below is an example instance of a POJO class with a UUID for an id:
Code:
package uk.co.bigsoft.project ; | |
| |
| |
import java.util.UUID; | |
| |
| |
| |
public class MyClass | |
{ | |
| |
private UUID id ; | |
private String stuff ; | |
| |
public void setId (UUID id) | |
{ | |
this.id = id ; | |
} | |
| |
public UUID getId () | |
{ | |
return id ; | |
} | |
| |
public String getStuff () | |
{ | |
return stuff; | |
} | |
| |
public void setStuff (String stuff) | |
{ | |
this.stuff = stuff; | |
} | |
} |
Finally we need to bring it all together,
XML:
<?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> | |
| |
<class name="uk.co.bigsoft.project.MyClass" table="my_class"> | |
| |
<id name="id" column="id" type="bsuuid" unsaved-value="null"> | |
<generator class="uk.co.bigsoft.db.types.UuidGenerator" /> | |
</id> | |
<property name="stuff" column="stuff" type="java.lang.String"/> | |
</class> | |
| |
</hibernate-mapping> |
This structure will work with any type of class not just UUID, the only caveat is that is must implement the Serializable interface.
7 comments
La solution que tu donnes correspond parfaitement à mon problème. Je l'ai donc suivie scrupuleusement. Mais j'ai une erreur qui est générée suite à l'utilisation de l'UUID:
[MrN: The solution you give fits to my problem. So I followed scrupulously. But I get an error that is generated as a result of using the UUID]
org.springframework.dao.InvalidDataAccessResourceUsageException: Could not execute JDBC batch update; nested exception is org.hibernate.exception.DataException: Could not execute JDBC batch update
Quelqu'un peut-il m'aider ?
[MrN: Can anyone help me?]
Merci d'avance
You are probably getting the exception because hibernate is trying to insert a record with no id, then trying to update it afterwards with a newly generated key.
Without seeing your code it's difficult to tell.
I remember getting this exception and used the Eclipse debugger to figure it out, then changed my configuration accordingly.
I followed your instructions exactly and am getting the following error:
Initial SessionFactory creation failed.org.hibernate.MappingException: Could not determine type for: bsuuid, for columns: [org.hibernate.mapping.Column(id)]
Any ideas? Thanks.
In deepCopy, since the UUID is immutable, I don't believe you need to do any work - just return the value passed in.
For assemble and disassemble and replace, since UUIDs are serializable, is there any need to convert to String?
Under equals(), I don't believe you need to cast before calling equals. You should just be able to check == and then call .equals().
Hashcode can probably just return hashcode(); I think the casting check is somewhat superfluous.
If your understanding is different from mine, please let me know. I have some UserType experience, but not a ton.
jason
serializable - UUID has a readObject() but no writeObject(). If you don't explicitly assemble them then you get an exception when talking to the database because the (automatic) serialisation done by Java becomes the actual bytes in memory of the UUID which are invalid unicode characters to put into the varchar column in the database. Possibly you could change your schema to be binary but then it makes debugging and reading the database more difficult.
equals - I agree.
hashcode - I agree.
There is a UserType.isMutable() call on the interface so I suspect that a lot of the things you queried might be handled higher up in the hibernate code without calling into this UserType implementation.
My implementation is as much an example as anything else so from it you can work out all the things you need to do (and the way you need to do them) as if it was really a user defined class.
Anyone have a solution ?
Thanks
UUID.fromString (other.toString ()) ;
to
newUUID(other.getMostSignificantBits(), other.getLeastSignificantBits());

