Here is a mechanism that lets you decouple the enum value names and ordinals from the data, while still making it easy to map enum fields to database columns.
The solution can be specific to an enum type, but it would be better to have all your persistent enum types implement the same interface to reuse the mechanism. Let's define the PersistentEnum interface:
public interface PersistentEnum { int getId(); }
and it will be implemented by enum types, for example:
public enum Gender implements PersistentEnum { MALE(0), FEMALE(1); private final int id; Gender(int id) { this.id = id } @Override public int getId() { return id; } }
Now we need to define the Hibernate User Type that will do the conversion both ways. Each enum requires its own user type, so first we will code an abstract superclass that works with the PersistentEnum interface and then subclass it for each enum:
public abstract class PersistentEnumUserType<T extends PersistentEnum> implements UserType { @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return cached; } @Override public Object deepCopy(Object value) throws HibernateException { return value; } @Override public Serializable disassemble(Object value) throws HibernateException { return (Serializable)value; } @Override public boolean equals(Object x, Object y) throws HibernateException { return x == y; } @Override public int hashCode(Object x) throws HibernateException { return x == null ? 0 : x.hashCode(); } @Override public boolean isMutable() { return false; } @Override public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException { int id = rs.getInt(names[0]); if(rs.wasNull()) { return null; } for(PersistentEnum value : returnedClass().getEnumConstants()) { if(id == value.getId()) { return value; } } throw new IllegalStateException("Unknown " + returnedClass().getSimpleName() + " id"); } @Override public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException { if (value == null) { st.setNull(index, Types.INTEGER); } else { st.setInt(index, ((PersistentEnum)value).getId()); } } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; } @Override public abstract Class<T> returnedClass(); @Override public int[] sqlTypes() { return new int[]{Types.INTEGER}; } }
The interesting methods are nullSafeGet() which is called when the resultset from the database is mapped to an object, and nullSafeSet() which is called when the fields of an object are mapped to SQL parameters of insert/update/delete statements.
The extension point is the abstract method returnedClass() - in every subclass we will override it so it returns the specific enum class. A User Type for the Gender enum defined above would like this:
public class GenderUserType extends PersistentEnumUserType<Gender> { @Override public Class<Gender> returnedClass() { return Gender.class; } }
The last thing to do is to configure fields of enum types to use the appropriate user types:
@Entity public class Person { @Type(type="it.recompile.GenderUserType") private Gender gender; // other fields and methods... }
Note that the type attribute should contain the fully qualified class of the user type, including the package.
That's it - now you can safely make changes to your enum classes without worrying about problems with existing data, as long as you don't change the id values in the constructors (which you have no reason to do).
well done!
ReplyDeletethanks
Hello,
ReplyDeleteJust as an addendum:
The code needs a slight update in order to support sql-enum-parsing like
SELECT COUNT(p) FROM ProfileData p WHERE p.state = de.arctis.demo.ProfileData$StateType.aktive ... "
I found the solution in an old discussion on https://forum.hibernate.org/viewtopic.php?t=930539
public abstract class PersistentEnumUserType implements EnhancedUserType {
...
public String objectToSQLString(Object obj) {
if(obj == null) return "null";
if (obj instanceof PersistentEnum) {
PersistentEnum enumObject = (PersistentEnum) obj;
return "" + enumObject.getId();
}
throw new IllegalArgumentException("Cannot convert " + obj.getClass());
}
public String toXMLString(Object obj) {
return objectToSQLString(obj);
}
public Object fromXMLString(String s) {
if(StringUtils.isEmpty(s) || s.trim().equalsIgnoreCase("null")) return null;
int id = Integer.parseInt(s);
for(PersistentEnum value : returnedClass().getEnumConstants()) {
if(id == value.getId()) {
return value;
}
}
throw new IllegalArgumentException("Cannot parse XML " + s + " to " + this.returnedClass().getName());
}
Not sure about the to/fromXMLString, but objectToSQLString is important.
(sorry for the bad formatting)
Thanks very much. I searched many websites and blogs for this solution without any progress. But, your tutorial helped me. Thanks!!
ReplyDeleteI need to create a custom type for each of the enum, right ? Is there any way to avoid that ?
ReplyDeleteBy definition a user type is specific to a type (specific enum in our case) because it needs to know how to instantiate it in nullSafeGet()
DeleteHmm. In my case, I have around 20 enums already and have scope to increase that number. So, to save the data using hibernate, I need to write 20+ custom types for enums, right ? Is that the way? Or am I using any wrong approach ?
DeleteYou have options - the approach above, Hibernates build in mapping by order or name, code generation of user types for each enum, or write a framework that integrates with the Hibernate metamodel (low level advanced stuff).
DeleteThanks for the reply. I can't use the ordinal/name mapping anyway. Let me check which one is better from other options.
DeleteHere is an improved version which only needs one user type for all your enums. The example uses a enum label instead of an id but this is easy to adapt.
Deletehttps://gist.github.com/jedvardsson/96af1b86f78762ac12ef
For memory saving, I recommend to use SMALLINT type instead of INTEGER. I suppose, that you have less than 32767 enums types ;)
ReplyDeletethank you
ReplyDeleteSoccer banker tip - vntopbet planet win 365 planet win 365 메리트카지노 메리트카지노 planet win 365 planet win 365 265No Deposit Casino Bonus 2020 - How to claim a bonus with free spins
ReplyDeletePerfect! I used it with Hibernate 3 and it worked like a charm!
ReplyDelete