Donnerstag, 15. März 2012

Java Enumerations mit Hibernate

In meinem derzeitigen Kundenprojekt finden Enumerations im Domänen-Modell Verwendung. Die im Modell festgehaltenen Werte sollen auch in der Datenbank abgespeichert werden. Bei einfachen Enumerations stellt das auch kein Problem dar:

public enum TitleEnum {
    HERR, FRAU;
}

import javax.persistence.EnumType;
import javax.persistence.Enumerated;

public class Person {
    
    @Enumerated(EnumType.ORDINAL)
    TitleEnum title;
    
    @Enumerated(EnumType.STRING)
    TitleEnum title2;
} 
In diesem kleinen Beispiel sieht man, wie mittels der Annotation @Enumerated entweder der String oder die Ordnungszahl der Enumeration in der Datenbank absgespeichert werden können.
So weit, so gut, so einfach. Nun existieren allerdings auch komplexere Enumerations, welche zusätzliche Werte definieren:
public enum TitleEnum {
    HERR("Herr"), FRAU("Frau");

    private final String title;

    private TitleEnum(String title) {
        this.title = title;
    }

    public static TitleEnum getEnum(String title) {
        if(title.equals(HERR.title)){
            return HERR;
        } else if(title.equals(FRAU.title)){
            return FRAU;
        }
        return null;
    }

    public String getTitle() {
        return title;
    }
}
Für diese Art von Enumeration bietet Hibernate die Möglichkeit mittels der Annotation @Type einen eigenen UserType zu definieren, welcher lediglich das Interface org.hibernate.usertype.UserType implementieren muss. Möchte man der Annotation noch zusätzliche Properties übergeben, ist auch noch das Interface org.hibernate.usertype.ParameterizedType zu implementieren. Im folgenden Beispiel ist der UserType so implementiert, dass Strings oder Integer-Werte in die Datenbank gespeichert werden können. Möchte man andere Datentypen abspeichern, ist in der Regel lediglich die Methode sqlTypes anzupassen.

Die Klasse sieht folgendermaßen aus:

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Properties;

import org.hibernate.HibernateException;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;
import org.springframework.util.ObjectUtils;

public class EnumUserType implements UserType, ParameterizedType {

    private Method recreateEnumMethod;
    private Method recreateValueMethod;
    private Class enumClass;

    public void setParameterValues(Properties parameters) {
        if(parameters != null){
            String enumMethod = parameters.getProperty("getEnumMethod");
            String stringMethod = parameters.getProperty("valueMethod");
            String className = parameters.getProperty("enumClassName");
            Class returnType = null;
            try{
                enumClass = Class.forName(className).asSubclass(Enum.class);
                recreateValueMethod = enumClass.getMethod(stringMethod, new Class[] {});
                returnType = recreateValueMethod.getReturnType();
                recreateEnumMethod = enumClass.getMethod(enumMethod, new Class[] {returnType});
            } catch (ClassNotFoundException e){
                e.printStackTrace();
            } catch (SecurityException e){
                e.printStackTrace();
            } catch (NoSuchMethodException e){
                e.printStackTrace();
            }
        }
    }

    public int[] sqlTypes() {
        Class type = recreateValueMethod.getReturnType();
        if(type.getName().endsWith("String")){
            return new int[] {Types.CHAR};
        } else{
            return new int[] {Types.INTEGER};
        }
    }

    public Class returnedClass() {
        return enumClass;
    }

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

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

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {

        Object value = rs.getObject(names[0]);
        Object returnVal = null;

        if(value == null)
            return null;
        else{
            try{
                returnVal = recreateEnumMethod.invoke(enumClass, new Object[] {value});
            } catch (IllegalArgumentException e){
                e.printStackTrace();
            } catch (IllegalAccessException e){
                e.printStackTrace();
            } catch (InvocationTargetException e){
                e.printStackTrace();
            }
        }
        return returnVal;
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
        Object prepStmtVal = null;

        if(value == null){
            st.setObject(index, null);
        } else{
            try{
                prepStmtVal = recreateValueMethod.invoke(value, new Object[] {});
                st.setObject(index, prepStmtVal);
            } catch (IllegalArgumentException e){
                e.printStackTrace();
            } catch (IllegalAccessException e){
                e.printStackTrace();
            } catch (InvocationTargetException e){
                e.printStackTrace();
            }
        }
    }

    public Object deepCopy(Object value) throws HibernateException {
        if(value == null)
            return null;
        else{
            Object obj=null;
            try{
                obj = recreateEnumMethod.invoke(value, new Object[] {recreateValueMethod.invoke(value, new Object[] {})});
            } catch (IllegalArgumentException e){
                e.printStackTrace();
            } catch (IllegalAccessException e){
                e.printStackTrace();
            } catch (InvocationTargetException e){
                e.printStackTrace();
            }
            return obj;
        }
    }

    public boolean isMutable() {
        return false;
    }

    public Serializable disassemble(Object value) throws HibernateException {
        Object deepCopy = deepCopy(value);

        if(!(deepCopy instanceof Serializable)) return (Serializable) deepCopy;

        return null;
    }

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

    public Object replace(Object original, Object target, Object owner) throws HibernateException {
        return deepCopy(original);
    }
}
Was passiert genau in der Klasse?
In den Zeilen 17-19 werden Variablen für die aufzurufenden Methoden und die dazugehörige Klasse definiert.
In den Zeilen 21-31 werden zuerst die übergebenen Properties in Strings ausgelesen, anhand derer dann die Methoden und die Klasse der Enumeration per Reflection erzeugt werden.
In den Zeilen 42-49 wird per Reflection anhand des Rückgabetyps der recreateValueMethod-Methode der SQL-Datentyp bestimmt. Wie weiter oben beschrieben ist dies im Beispiel exemplarisch für String und Integer implementiert worden.
 In den Zeilen 63-72 wird versucht, den aus dem Resultset ausgelesenen Wert wieder in die zugehörige Enumeration  umzuwandeln. Ist der Wert im Resultset null, wird null zurückgegeben, ansonsten wird per Reflection die recreateEnumMethod aufgerufen.
In den Zeilen 84-92 wird aus der enum per Reflection die Methode aufgerufen, die den abzuspeichernden Wert zurückgibt. Dieser wird dann in das Prepared Statement eingefügt. Um den Code möglichst generisch zu halten, erfolgt der Aufruf mit einem Object, kann aber bei Bedarf durch den konkret verwendeten Datentypen ersetzt werden.

Jetzt muss der EnumUserType nur noch in der Annotation verwendet werden:

@Type(type = "foo.bar.EnumUserType", parameters = {
            @Parameter(name = "enumClassName", value = "foo.bar.TitleEnum"),
            @Parameter(name = "getEnumMethod", value = "getEnum"),
            @Parameter(name = "valueMethod", value = "getTitle")})
    private TitleEnum TitleEnum;
Statt foo.bar ist natürlich der vollqualifizierende Name der Klassen anzugeben.

Keine Kommentare:

Kommentar veröffentlichen