Friday, November 21, 2008

On Hibernate UserTypes

Hibernate UserTypes are a great way for persisting complex classes which may require more than one column in a table in order to be properly represented in a database. Many people have used them successfully in order to be able to simplify storage of complex objects and thus simplify their applications at the same time. One simpler use case of them was to create Enum types before support for Java 5+ enums came along.
Now, you'd think that once you've got Java 5 support with Hibernate, you'd never have to use such a simple use case again because you've got the Enums themselves, along with the @Enumerated annotation you can place on fields and properties, right ? Wrong. I've recently started migrating my projects to Spring Security from Acegi Security, and I've found that in the process, they've made the GrantedAuthority interface extend Comparable. This conflicts with the fact that Java 5+ Enums already implemented the Comparable interface with a generic parameter, and as such your code won't compile if you have an Enum that also implements GrantedAuthority, such as the following :


public enum Role implements GrantedAuthority, ConfigAttribute
{
ROLE_ADMINISTRATOR;

public String getAuthority() {
return name();
}

public String getAttribute() {
return name();
}

public String getMessageKey() {
return "enum.Role.".concat(name());
}
}


In order to remedy the conflict, I've had to convert my nice, pretty, simple Enum into a class, and simulate Java 5+'s enum behaviour, like so :


public final class Role implements GrantedAuthority, ConfigAttribute, Serializable, Cloneable {

private static final long serialVersionUID = 1L;

public static final Role ROLE_ADMINISTRATOR = new Role(0, "ROLE_ADMINISTRATOR");

private static final Role[] VALUES = new Role[] {
ROLE_ADMINISTRATOR
};

private static final Map NAME_MAPPINGS = new HashMap();

static {
Arrays.sort(VALUES, new Comparator() {
public int compare(Role o1, Role o2) {
return o1.ordinal - o2.ordinal;
}
});

for (Role r : VALUES) {
NAME_MAPPINGS.put (r.getName(), r);
}
}

private int ordinal;
private String name;

//Constructors

/*
* DO NOT EVER USE THIS! It exists only for serialization purposes.
*/
public Role() {
super();
}

private Role(Role r) {
this(r.ordinal, r.name);
}

private Role(int ordinal, String name) {
super();
this.ordinal = ordinal;
this.name = name;
}

//Behaviour Methods

public int compareTo(Object o) {

if (o == null || o.getClass() != Role.class) {
throw new IllegalArgumentException(
"Comparison object may not be null, and must be a Role");
}

return this.ordinal - ((Role) o).ordinal;
}

public String getAuthority() {
return getName();
}

public String getAttribute() {
return getName();
}

//Pseudo-properties

public String getMessageKey() {
return "enum.Role.".concat(getName());
}

//Property Accessors

public final int getOrdinal() {
return this.ordinal;
}

public final String getName() {
return this.name;
}

//Helper Methods

public static final int hashCode(Role r) {
return r == null ? 0 : r.hashCode();
}

public static final boolean equals(Role x, Role y) {
return x == null ? (y == null) : x.equals(y);
}

public static final Role clone(Role r) {
return r == null ? null : r.clone();
}

public static final Role[] values() {
return VALUES;
}

public static final Role valueOf(String s) throws IllegalArgumentException {

String key = defaultString(s).toUpperCase();

if (NAME_MAPPINGS.containsKey(key)) {
return NAME_MAPPINGS.get(key);
} else {
throw new IllegalArgumentException("No role by the name ["+s+"] exists");
}
}

//Object Overrides

@Override
public String toString() {
return new StringBuffer().append(this.name)
.append("(").append(this.ordinal).append(")")
.toString();
}

@Override
public Role clone() {
return new Role(this);
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.name == null) ? 0 : this.name.hashCode());
result = prime * result + this.ordinal;
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Role other = (Role) obj;
if (this.name == null) {
if (other.name != null)
return false;
} else if (!this.name.equals(other.name))
return false;
if (this.ordinal != other.ordinal)
return false;
return true;
}

}


Once this was done, I created a Hibernate UserType implementation for Role, so that Hibernate could persist Roles as Integers, just as was already being done for the Enum version of Role:


public class RoleUserType implements UserType {

private static final int[] SQL_TYPES = new int[] { Types.INTEGER };

public static final String HIBERNATE_TYPE_NAME = "RoleUserType";

public Object deepCopy(Object value) throws HibernateException {
return value;
}

public Object assemble(Serializable cached, Object owner) throws HibernateException {
return (Role)cached;
}

public Serializable disassemble(Object value) throws HibernateException {
return (Role)value;
}

public boolean equals(Object x, Object y) throws HibernateException {
return Role.equals((Role)x, (Role)y);
}

public int hashCode(Object x) throws HibernateException {
return Role.hashCode((Role)x);
}

public boolean isMutable() {
return false;
}

public Object nullSafeGet(ResultSet resultSet, String[] names, Object owner) throws HibernateException, SQLException {
int roleOrdinal = resultSet.getInt(names[0]);

return resultSet.wasNull() ? null : Role.values()[roleOrdinal];
}

public void nullSafeSet(PreparedStatement statement, Object value, int index)
throws HibernateException, SQLException {
if (value == null) {
statement.setNull(index, Types.INTEGER);
} else {
statement.setInt(index, ((Role)value).getOrdinal());
}
}

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

@SuppressWarnings("unchecked")
public Class returnedClass() {
return Role.class;
}

public int[] sqlTypes() {
return SQL_TYPES;
}

}


Once the Hibernate UserType implementation was written, it was just a matter of adding my model package to Hibernate's list of annotated packages (using Java 5 after all) :


@TypeDef(name = RoleUserType.HIBERNATE_TYPE_NAME, typeClass = RoleUserType.class)
package com.mypackage.model;

import org.hibernate.annotations.TypeDef;
import eu.alenislimited.acshelper.support.hibernate.RoleUserType;


After defining the Hibernate UserType type, it was then just a matter of going around to all my fields that used Role, and replacing ...


@Enumerated(EnumType.ORDINAL)


... with ...


@Type(type = RoleUserType.HIBERNATE_TYPE_NAME)


Fortunately, doing the conversion was just extra work, and didn't require any special changes to any of my Spring configuration, or even my Hibernate configuration (beyond adding the @TypeDef to my model package and adding it to my Hibernate SessionFactory's list of annotatedPackages).

1 comment:

Jaime Hablutzel said...

Hi, here is a couple of good examples, too :)

http://i-proving.ca/space/Technologies/Hibernate/User+Types+in+Hibernate