#

Deeper into Hibernate – 2

Und schon die nächste Erfahrung: Enumerations sind eine feine Sache, auch wenn man diese in Java 1.4 noch per Hand machen muss.
Hibernate unterstützt Enumerations, so dass man seine Enumeration auch schön konsisten in der Datenbank ablegen kann.

Doch so einfach, wie es auf den ersten Blick erscheint, ist das ganze nicht, zumindest, wenn man es wiederverwertbar lösen möchte.

Meine erste Suche brachte mich auf den Artikel „UserType for persisting Typesafe Enumerations“ in den Hibernate Foren. Dort wurde die Grundlage an einem einfachen Beispiel erläutert:
Zuerst benötigt man eine Kalsse, welche die Funktionalität einer Enumeration bereitstellt (public abstract class Enum). Anschließend erbt man von dieser Basisklasse und implementiert seine eigene Enumeration, wie z. B. die Ratings.
Zu guter letzt benötigt man die Klasse EnumUserType, welche das Interface UserType (,und evtl. ParameterizedType, da in diesem Beispiel ein Parameter aus der Hibernate-Konfigurationsdatei an diesen UserType übergeben werden soll,) implementiert.
Der Haken: Die Klasse, welche das Interface UserType implementiert, und damit die Konvertierung in einen SQL Wert und zurück in ein Objekt durchführt, muss die Class-Klasse des Enumeration in der Methode public Class returnedClass() zurückgeben.
Im Beispiel wird dies über einen zusätzlichen Parameter in der Konfigurationsdatei gelöst:

<property name="result" >
<column name="RESULT"/>
<type name="util.usertype.EnumUserType">
<param name="targetClass">util.enum.Result</param>
</type>
</property>

Sehr unschön. Dieses Problem wird in der Fortführung des Artikels unter „UserType for persisting a Typesafe Enumeration with a VARCHAR column“ einfach dadurch gelöst, dass es nun eine eigene UserType für Ratings gibt, in der die Klasse hartkodiert steht. (An dieser Stelle fällt mir auf, dass die Artikel hier von mir in der falschen Rehenfolge gelesen und präsentiert wurden. Einen dritten Artikel zu diesem Thema gibt es auch: UserType for persisting Typesafe Enumerations with a single class“. Das ändert aber an den Problemen nichts…)

Also habe ich eine entsprechende Lösung erstellt, bei der ich von einer Basisimplementation von UserType erbe und ihr die Klasse im Konstruktor übergebe. (In meinem Fall geht es darum, eine Enumeration als ein Character in der DB zu speichern, was einfacher zu interpretieren ist als ein Integerwert.)

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
 
import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;
 
public abstract class EnumCharUserType implements UserType {
private Class returnedClass;
 
protected EnumCharUserType(Class returnedClass) {
this.returnedClass = returnedClass;
}
 
private static final int[] SQL_TYPES = new int[]
 { Types.VARCHAR };
 
public int[] sqlTypes() {
return SQL_TYPES;
}
 
public boolean equals(Object x, Object y) {
if (x != null) return x.equals(y);
else if (y != null) return y.equals(x);
else return true;
}
 
public int hashCode(Object x) throws 
HibernateException {
return x.hashCode();
}
 
public Object deepCopy(Object value) {
return value;
}
 
public boolean isMutable() {
return false;
}
 
public void nullSafeSet(PreparedStatement statement,
Object value, int index) throws HibernateException,
SQLException {
String val = (String) value;
if (StringUtils.isEmpty(val)) {
statement.setNull(index, Types.VARCHAR);
}
else {
statement.setString(index, val.substring(0, 1));
}
}
 
public Serializable disassemble(Object value) 
throws HibernateException {
return (Serializable) value;
}
 
public Object assemble(Serializable cached, 
Object owner) throws HibernateException {
return cached;
}
 
public Object replace(Object original, Object target, 
Object owner) throws HibernateException {
return original;
}
 
public Class returnedClass() {
return returnedClass;
}
 
public Object nullSafeGet(ResultSet rs, String[] names, 
Object owner) throws HibernateException, SQLException {
String value = rs.getString(names[0]);
if (!StringUtils.isEmpty(value)) { 
return rs.wasNull() ? null : 
EnumChar.getByValue(returnedClass, value
.charAt(0)); 
}
return null;
}
}

Benutzt werden kann die Klasse über eine Implementierung:

// Die eigentliche Enumeration
import singularity.blog.PostState;
 
public class PostStateUserType 
extends EnumCharUserType {
 
public PostStateUserType(){
super(PostState.class);
}
}

So einfach.

Da ich diese Enumeration-Adaption auch an anderer Stelle benutze, gibt es auch einen State für Comments:

import singularity.blog.CommentState;
 
public class CommentStateUserType 
extends EnumCharUserType {
 
public CommentStateUserType(){
super(CommentState.class);
}
}

Dabei erscheint auch schon das nächste Problem: Die Basisklasse für die Enumerations (Enum) im Beispiel benutzt static HashMaps um die Enumerationwerte zu speichern. Wenn nun sowohl PostState als auch CommentState von dieser Klasse erben und ihre Werte dort ablegen, kollidieren deren Werte. So kann der PostState ‚a‘-„aaaaaa“ von dem CommentState ‚a‘-„bbbbbb“ überschrieben werden.

Meine Lösung bestand nun darin, für jede Enumeration-Klasse eine eigene Instanz der internen Maps anzulegen. Hierzu muss beim Abrufen der Werte auch die Klasse übergeben werden:

import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
 
/**
 * Provides basic funtionality for enumerations.
 * Since the enumerations are cached in static hashmaps,
 * this class needs the class of the implementation to prevent
 * enumeration value collisions.
 *
 */
public abstract class EnumChar implements Serializable {
private static final long serialVersionUID = 1L;
 
protected static Map classToNameMap = new HashMap(5);
 
protected static Map classToValueMap = new HashMap(5);
 
protected static Map classToAllValuesMap = new HashMap(5);
 
protected char value;
 
protected transient String name;
 
protected transient String classname;
 
protected EnumChar(String name, char value, Class clazz) {
this.value = value;
this.name = name;
this.classname = clazz.getName().intern();
add();
}
 
protected void add() {
Map namemap = (Map) classToNameMap.get(classname);
if (namemap == null) {
namemap = new HashMap(15);
classToNameMap.put(classname, namemap);
}
namemap.put(this.name, this);
 
Map valuemap = (Map) classToValueMap.get(classname);
if (valuemap == null) {
valuemap = new HashMap(15);
classToValueMap.put(classname, valuemap);
}
valuemap.put(new Character(this.value), this);
 
Set allvalues = (Set) classToAllValuesMap.get(classname);
if (allvalues == null) {
allvalues = new LinkedHashSet();
classToAllValuesMap.put(classname, allvalues);
}
allvalues.add(this);
}
 
/**
 * Returns all values of the enumeration defined by the class 
identified by the given object.
 *  
 * @param object
 * @return
 */
public static Set getAllValues(Object object) {
return getAllValues(object.getClass());
}
 
/**
 * Returns all values of the enumeration defined by the given class.
 * 
 * @param clazz
 * @return
 */
public static Set getAllValues(Class clazz) {
return (Set) classToAllValuesMap.get(clazz.getName());
}
 
public static EnumChar getByName(Object object, 
String name) {
return getByName(object.getClass(), name);
}
 
public static EnumChar getByName(Class clazz, String name) {
Map namemap = (Map) classToNameMap
.get(clazz.getName());
if (namemap == null) return null;
return (EnumChar) namemap.get(name);
}
 
public static EnumChar getByValue(Object object, char value) {
return getByValue(object.getClass(), value);
}
 
public static EnumChar getByValue(Class clazz, char value) {
Map valuemap = (Map) classToValueMap
.get(clazz.getName());
if (valuemap == null) return null;
return (EnumChar) valuemap.get(new Character(value));
}
 
public char getValue() {
return value;
}
 
public String getName() {
return name;
}
 
public String getClassName() {
return classname;
}
 
public boolean equals(Object o) {
if (o == this) return true;
if (o == null || !(o instanceof EnumChar)) return false;
final EnumChar other = (EnumChar) o;
if (other.getClassName().equals(
getClassName()) && other
.getValue() == this.getValue()
) return true;
return false;
}
 
public int hashCode() {
return getClassName().hashCode() * 23 + 
new Character(value).hashCode();
}
 
protected Object readResolve() throws 
ObjectStreamException {
return getByValue(this, this.value);
}
}

Damit geht das dann. In der EnumCharUserType war der Vorreiter auch schon zu erkennen. In der Methode nullSafeGet steht:

String value = rs.getString(names[0]);
if (!StringUtils.isEmpty(value)) { 
return rs.wasNull() ? null : 
EnumChar.getByValue(returnedClass, value
.charAt(0)); 
}
return null;

Dort wird also auch explizit die Klasse der konkreten Enumeration Implementierung mit übergeben.

Die konkreten Implementierungen sehen dann in etwa wie folgt aus:

public class PostState extends EnumChar {
private static final long serialVersionUID = 1L;
 
// 'publish' ,'draft'
public static final PostState PUBLISH = new PostState("publish", 'p');
public static final PostState DRAFT = new PostState("draft", 'd');
 
protected PostState(String name, char value) {
super(name, value, PostState.class);
}
}
Tags:,

Leave a Reply »»

Note: All comments are manually approved to avoid spam. So if your comment doesn't appear immediately, that's ok. Have patience, it can take some days until I have the time to approve my comments.