-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 
Author Message
 Post subject: Mapping complex enums
PostPosted: Mon Jan 29, 2007 10:48 pm 
Regular
Regular

Joined: Fri Mar 26, 2004 11:45 am
Posts: 75
Hi,

I have a model with multiple entities, each of which is associated with a status. Statuses for each type of entity need to be ordered and it is possible that new statuses will be inserted in the future or the order of existing statuses will change. This indicates that I cannot just map my statuses to an enum.

What I want to do is create a table called Status with the following columns: "StatusName", "StatusKind", and "SortOrder". Here is a sample content for this table:

Started, Task, 1
Completed, Task, 2
Scheduled, Event, 1
Cancelled, Event, 2

I can map StatusKind as an enum, but I am not sure how to model and map Status entity itself. I want to prevent the user of my DAO layer from being able to persist a status with conflicting StatusName and StatusKind. For example, suppose that only events can be scheduled, but tasks cannot be scheduled, so I need to disallow an entry such as [Scheduled, Task, 1].

I have a lot of types of entities with statuses, so I would like to use one table to store all the statuses, differentiate them by StatusKind and order by SortOrder.

What would be the best mapping for this kind of situation?

Thanks.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 30, 2007 1:05 am 
Newbie

Joined: Wed Jan 17, 2007 9:48 pm
Posts: 9
I've got a similar scenario and so far am just storing a defined ID (as opposed to the ordinal or value). I took org.hibernate.type.EnumType as a starting point and so far have (not too clean I know)
Code:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.usertype.EnhancedUserType;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.util.ReflectHelper;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
* Enum type mapper (customised from org.hibernate.type.EnumType)
* Try and find the appropriate SQL type depending on column metadata
*
* @author Emmanuel Bernard
* @author Peter Johnson
*/
//TODO implements readobject/writeobject to recalculate the enumclasses
public class EnumType implements EnhancedUserType, ParameterizedType, Serializable {

    private static final Log log = LogFactory.getLog(EnumType.class);

    public static final String ENUM = "enumClass";
    public static final String TYPE = "type";

    private static Map<Class, Map<String, Enum>> enumValues = new HashMap<Class, Map<String, Enum>>();

    private Class<? extends Enum> enumClass;
    private int sqlType = Types.INTEGER;
    private boolean hasOwnId = false;

    public int[] sqlTypes() {
        return new int[]{sqlType};
    }

    public Class returnedClass() {
        return enumClass;
    }

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

    public int hashCode(Object x) throws HibernateException {
        return x == null ? 0 : x.hashCode();
    }

    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws HibernateException, SQLException {
        if (rs.wasNull()) {
            /*log.debug("Returning null as column " + names[0]);*/
            return null;
        }
        //log.debug("Column: " + names[0]);
        if (rs.getObject(names[0]) == null) {
            //log.info("Value was null");
            return null;
        }
        //log.debug("Value: " + rs.getObject(names[0]).toString());
        return fromXMLString(rs.getObject(names[0]).toString());
    }

    public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException {
        if (value == null) {
            /*log.debug("nullSafeSet: Binding null to parameter: " + index);*/
            st.setNull(index, sqlType);
        } else {
            if (isOrdinal(sqlType)) {
                int ordinal = hasOwnId ? ((HibernateEnum) value).getId() : ((Enum) value).ordinal();
                /*log.debug("nullSafeSet: Binding '" + ordinal + "' to parameter: " + index);*/
                st.setObject(index, Integer.valueOf(ordinal), sqlType);
            } else {
                String enumString = ((Enum) value).name();
                /*log.debug("nullSafeSet: Binding '" + enumString + "' to parameter: " + index);*/
                st.setObject(index, enumString, sqlType);
            }
        }
    }

    private boolean isOrdinal(int paramType) {
        switch (paramType) {
            case Types.INTEGER:
            case Types.NUMERIC:
            case Types.SMALLINT:
            case Types.TINYINT:
            case Types.BIGINT:
            case Types.DECIMAL: //for Oracle Driver
            case Types.DOUBLE:  //for Oracle Driver
            case Types.FLOAT:   //for Oracle Driver
                return true;
            case Types.CHAR:
            case Types.LONGVARCHAR:
            case Types.VARCHAR:
                return false;
            default:
                throw new HibernateException("Unable to persist an Enum in a column of SQL Type: " + paramType);
        }
    }

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

    public boolean isMutable() {
        return false;
    }

    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 void setParameterValues(Properties parameters) {
        /*log.info("Initialising EnumType parameters for " + parameters.getProperty(ENUM));*/
        String enumClassName = parameters.getProperty(ENUM);
        try {
            enumClass = ReflectHelper.classForName(enumClassName, this.getClass()).asSubclass(Enum.class);
            for (Class iface : enumClass.getInterfaces()) {
                if (iface.isAssignableFrom(HibernateEnum.class)) {
                    hasOwnId = true;       // determines whether ordinal or Id (via getId()) should be used
                    break;
                }
            }

        } catch (ClassNotFoundException exception) {
            throw new HibernateException("Enum class not found", exception);
        }
        //this is threadsafe to do it here, setParameterValues() is called sequencially
        if (!enumValues.containsKey(enumClass)) {
            initEnumValue();
            /*for (String key : enumValues.get(enumClass).keySet())
                log.debug("Key: " + key + " : " + enumValues.get(enumClass).get(key));*/
        }
        String type = parameters.getProperty(TYPE);
        if (type != null) {
            sqlType = Integer.decode(type);
        }
    }

    private void initEnumValue() {
        /*log.info("Initialising enumValues for " + enumClass.getName());*/
        try {
            Method method = enumClass.getDeclaredMethod("values", new Class[0]);
            Enum[] values = (Enum[]) method.invoke(null, new Object[0]);
            HashMap<String, Enum> enumValuesMap = new HashMap<String, Enum>();
            for (Enum value : values) {
                enumValuesMap.put(value.toString(), value);
                enumValuesMap.put(Integer.toString(hasOwnId ? ((HibernateEnum) value).getId() : value.ordinal()),
                                  value);
            }
            enumValues.put(enumClass, enumValuesMap);
        } catch (Exception e) {
            throw new HibernateException("Error while accessing enum.values(): " + enumClass, e);
        }
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        //FIXME Hum, I think I break the thread safety here
        ois.defaultReadObject();
        initEnumValue();
    }

    public String objectToSQLString(Object value) {
        if (isOrdinal(sqlType)) {
            if (hasOwnId) {
                return ((HibernateEnum) value).getId().toString();
            } else {
                return Integer.toString(((Enum) value).ordinal());
            }
        } else {
            return '\'' + ((Enum) value).name() + '\'';
        }
    }

    public String toXMLString(Object value) {
        if (isOrdinal(sqlType)) {
            if (hasOwnId) {
                return ((HibernateEnum) value).getId().toString();
            } else {
                return Integer.toString(((Enum) value).ordinal());
            }
        } else {
            return ((Enum) value).name();
        }
    }

    public Object fromXMLString(String xmlValue) {
        /*log.info("fromXMLString: Looking for class " + enumClass + " key " + xmlValue);*/
        if (!enumValues.containsKey(enumClass)) {
            throw new AssertionFailure("enumValues not preprocessed: " + enumClass);
        }
        if (enumValues.get(enumClass).containsKey(xmlValue)) {
            return enumValues.get(enumClass).get(xmlValue);
        } else {
            throw new IllegalArgumentException(
                    "fromXMLString: Unknown key value for enum " + enumClass + ": " + xmlValue);
        }
    }
}



Any Enums I want to have the ID persist instead of the ordinal or value implement

Code:
public interface HibernateEnum {

    public Integer getId();
}


So to use
Code:
    <!-- Define type here when using as a discriminator -->
    <typedef name="AddressType" class="EnumType">
        <param name="enumClass">AddressType</param>
        <param name="type">4</param>
        <!-- Defines the column type: 4 = INTEGER, 12 = VARCHAR -->
    </typedef>

then as a discriminator
Code:
<discriminator column="Address_Type" type="AddressType" not-null="true"/>

and / or normal property
Code:
<property name="type" column="Address_Type" type="AddressType" not-null="true" insert="false" update="false"/>


With the Enum looking like

Code:
public enum AddressType implements HibernateEnum {

    POSTAL( 1,"Postal"),
    PHYSICAL( 2,"Physical");


    AddressType(Integer id, String value) {
        this.id = id;
        this.value = value;
    }

    private Integer id;
    private String value;

    public Integer getId() {
        return id;
    }

    public String getValue() {
        return value;
    }

    // Alternative approach is to use a switch statement
    public static AddressType fromId(Integer key) {
        for (AddressType addressType : values()) {
            if (addressType.getId().equals(key)) {
                return addressType;
            }
        }
        return null;
    }
}


Hope this helps.

If / when I get time I was going to try to implement CompositeUserType in a hope to persist / maintain the values of the Enum in the DB but I'm not sure if that will work or if it is even the right approach to take to achieve it.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 30, 2007 1:19 pm 
Regular
Regular

Joined: Fri Mar 26, 2004 11:45 am
Posts: 75
Thanks for your response. It seems like you've been struggling with this too.

I am not sure if your problem is identical to mine, so I will ask the question agains.

How do I write Hibernate mappings for the following enum so that enum values are stored in a separate Constant table?

public enum ConstantEnum
{
TASK_STARTED("Started", CONSTANTKIND.TASK_KIND, 1),
TASK_COMPLETED("Completed", CONSTANTKIND.TASK_KIND, 2),
EVENT_SCHEDULED("Scheduled", CONSTANTKIND.EVENT_KIND, 1),
EVENT_CANCELLED("Cancelled", CONSTANTKIND.EVENT_KIND, 2);

private enum CONSTANTKIND{
TASK_KIND, EVENT_KIND
};

private final String constName;
private final CONSTANTKIND constKind;
private final int sortOrder;
ConstantEnum(String constName, CONSTANTKIND constKind, int sortOrder) {
this.constName = constName;
this.constKind = constKind;
this.sortOrder = sortOrder;
}
public String constName() { return constName; }
public CONSTANTKIND constKind() { return constKind; }
public int sortOrder() { return sortOrder; }
}


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 30, 2007 3:04 pm 
Expert
Expert

Joined: Tue Nov 23, 2004 7:00 pm
Posts: 570
Location: mostly Frankfurt Germany
With annotations you can define how an enum is mapped.

One option is String another ordinal. With the following enum

Code:
public enum Status { OPEN, CLOSED}

my table content looks like
id, status
1, OPEN
2, CLOSED

I like it this way because I can understand the status in the table as well.[/code]

_________________
Best Regards
Sebastian
---
Training for Hibernate and Java Persistence
Tutorials for Hibernate, Spring, EJB, JSF...
eBook: Hibernate 3 - DeveloperGuide
Paper book: Hibernate 3 - Das Praxisbuch
http://www.laliluna.de


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 30, 2007 3:32 pm 
Regular
Regular

Joined: Fri Mar 26, 2004 11:45 am
Posts: 75
Thanks, I am pretty sure this should be possible to map with pure hbm files as well.

But still, how to map an enum, which has a sub-enum and several other properties?

Any help from Hibernate gurus?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 30, 2007 7:09 pm 
Newbie

Joined: Wed Jan 17, 2007 9:48 pm
Posts: 9
LaLiLuna - the drawback with that approach is that you're storing the same string over and over which is a waste of DB space. The other common approach is therefore to store the ordinal which is an int. The drawback is that the declared order of the enum must remain constant. This reduces flexibility.

The code I added above allows one to define the int that gets used. However, like you, I'd like the value to be more practical at the DB level e.g. when using SQL queries. Hence, similar to alecswan I'd like to store the data of the enum into a DB table.

e.g. I'd like to store the values of Country below into a table called Place and have any other class that uses the enum to have a foreign key to the value of the country's ID.
Code:
public enum Country implements Place, HibernateEnum {

    // ID values intermix with State

    AUSTRALIA(1, "AU", PlaceType.COUNTRY, "61", new String[]{"Australia"});

    private Integer id;
    private String isoTerritoryCode;
    private PlaceType placeType;
    private String phoneCode;
    private String[] values;


    Country(Integer id, String isoTerritoryCode, PlaceType placeType, String phoneCode, String[] values) {
        this.id = id;
        this.isoTerritoryCode = isoTerritoryCode;
        this.placeType = placeType;
        this.phoneCode = phoneCode;
        this.values = values;
    }

    public Integer getId() {
        return id;
    }

    public String getIsoTerritoryCode() {
        return isoTerritoryCode;
    }

    public PlaceType getType() {
        return placeType;
    }

    public String getPhoneCode() {
        return phoneCode;
    }

    public Place getCountry() {
        return null;
    }

    public String getValue() // Get default / primary value
    {
        return values[0];
    }

    public String[] getValues() // Get all value variations
    {
        return values;
    }

    // Alternative approach is to use a switch statement
    public static Country fromId(Integer key) {
        for (Country country : values()) {
            if (country.getId().equals(key)) {
                return country;
            }
        }
        return null;
    }
}

By taking this approach the values have the usability within code of the enum and are constant (defined by the enum) yet persisted to the DB for longer term visibility.

When we choose to remove a value then the values remain in the DB but nolonger in the enum so code wise they can nolonger be used.

Can implementing CompositeUserType achieve the cascade to a DB table and thus the ability to use as a foreign key type arrangement?


Top
 Profile  
 
 Post subject: Re: Mapping complex enums
PostPosted: Fri Jun 18, 2010 5:35 pm 
Newbie

Joined: Fri Jun 18, 2010 5:22 pm
Posts: 1
Hi there.

Three-years-old post, but I just started with Hibernate and gone through the same problem of alecswan and pj. Google and I'm just finding solutions to accomplish the simple ENUM problem, where a field in my table is an ENUM.

I have a table with possible values of Status (fields are: status_id, status_name, status_description), many tables point to this table to set their statuses. The foreign-key is an integer (mapped to the status_id). Is there a way to achieve this? Which is the best/clean way to achieve?

Thank you!
CaioToOn!


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.