Metadata driven applications

Posted by    |      

Hibernate is great at representing strongly-typed, static object models. Not all applications are like this. Metadata-driven applications define entity type information in the database. Both the object model and the relational model support dynamic addition of new types, and perhaps even redefinition of existing types. Actually, most complex applications contain a mix of both static models and dynamic models.

Suppose that our system allowed supports various types of item, each with specialized attributes. If there were a static, predefined set of item types, we would probably model this using inheritance. But what if new types may be defined dynamically by the user - with the type definitions stored in the database?

We could define an ItemType class representing the definition of an item type. Each ItemType instance would own a collection of ItemTypeAttribute instances, each representing a named attribute that applies to that particular item type. ItemType and ItemTypeAttribute define the /meta-model/.

Item instances would each have a unique ItemType and would own a collection of ItemAttributeValue instances, representing concrete values of the applicable ItemTypeAttributes.

http://hibernate.sourceforge.net/metadata.gif

The mappings for the metamodel classes is quite straightforward. The only really interesting thing is that ItemType and ItemTypeAttribute are perfect examples of classes that should have second-level caching enabled: updates are infrequent, there are relatively few instances, instances are shared between many users and many instances of the Item class.

The mappings for ItemType and ItemTypeAttribute might look like this:

<class name="ItemType">
    <cache usage="nonstrict-read-write"/>
    <id name="id">
        <generator class="native"/>
     </id>
    <property name="name" 
            not-null="true" 
            length="20"/>
    <property name="description" 
            not-null="true" 
            length="100"/>
    <set name="attributes" 
            lazy="true" 
            inverse="true">
        <key column="itemType"/>
        <one-to-many class="ItemTypeAttribute"/>
    </set>
</class>

<class name="ItemTypeAttribute">
    <cache usage="nonstrict-read-write"/>
    <id name="id">
        <generator class="native"/>
    </id>
    <property name="name" 
             not-null="true" 
             length="20"/>
    <property name="description" 
             not-null="true" 
             length="100"/>
    <property name="type" 
             type="AttributeTypeUserType" 
             not-null="true"/>
    <many-to-one name="itemType" 
             class="ItemType" 
             not-null="true"/>
</class>

We do not enable proxies for these classes, since we expect that instances will always be cached. We'll leave the definition of the custom type AttributeTypeUserType to the you!

The mappings for Item and ItemAttributeValue are also straightforward:

<class name="Item" 
        lazy="true">
    <id name="id">
        <generator class="native"/>
    </id>
    <many-to-one name="type" 
        class="ItemType" 
        not-null="true" 
        outer-join="false"/>
    <set name="attributeValues" 
            lazy="true" 
            inverse="true">
        <key column="item"/>
        <one-to-many class="Item"/>
    </set>
</class>

<class name="ItemAttributeValue" 
        lazy="true">
    <id name="id">
        <generator class="native"/>
    </id>
    <many-to-one name="item" 
        class="Item" 
        not-null="true"/>
    <many-to-one name="type" 
        class="ItemTypeAttribute" 
        not-null="true" 
        outer-join="false"/>
    <property name="value" type="AttributeValueUserType">
        <column name="intValue"/>
        <column name="floatValue"/>
        <column name="datetimeValue"/>
        <column name="stringValue"/>
    </property>
</class>

Notice that we must explicitly set outer-join="false" to prevent Hibernate from outer join fetching the associated objects which we expect to find in the cache.

Finally, we need to define the custom type AttributeValueUserType, that takes the value of an ItemAttributeValue and stores it in the correct database column for it's type.

public class AttributeValueUserType implements UserType {

    public int[] sqlTypes() {
        return new int[] { Types.BIGINT, Types.DOUBLE, Types.TIMESTAMP, Types.VARCHAR };
    }
    
    public Class returnedClass() { return Object.class; }
    
    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) 
        throws HibernateException, SQLException {
        
        Long intValue = (Long) Hibernate.LONG.nullSafeGet(rs, names[0], owner);
        if (intValue!=null) return intValue;
        
        Double floatValue = (Double) Hibernate.DOUBLE.nullSafeGet(rs, names[1], owner);
        if (floatValue!=null) return floatValue;
        
        Date datetimeValue = (Date) Hibernate.TIMESTAMP.nullSafeGet(rs, names[2], owner);
        if (datetimeValue!=null) return datetimeValue;
        
        String stringValue = (String) Hibernate.STRING.nullSafeGet(rs, names[3], owner);
        return stringValue;
        
    }
    
    public void nullSafeSet(PreparedStatement st, Object value, int index) 
        throws HibernateException, SQLException {
        
        Hibernate.LONG.nullSafeSet( st, (value instanceof Long) ? value : null, index );
        Hibernate.DOUBLE.nullSafeSet( st, (value instanceof Double) ? value : null, index+1 );
        Hibernate.TIMESTAMP.nullSafeSet( st, (value instanceof Date) ? value : null, index+2 );
        Hibernate.STRING.nullSafeSet( st, (value instanceof String) ? value : null, index+3 );
    }
    
    public boolean equals(Object x, Object y) throws HibernateException {
        return x==null ? y==null : x.equals(y);
    }
    
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }
    
    public boolean isMutable() {
        return false;
    }
    
}

Thats it!

UPDATE: I don't know what I was thinking! Of course, we need to be able to query the attributes of our items, so AttributeValueUserType should be a /composite/ custom type!

public interface CompositeUserType {
    
    public String[] getPropertyNames() {
        return new String[] { "intValue", "floatValue", "stringValue", "datetimeValue" };
    }
    
    public Type[] getPropertyTypes() {
        return new Type[] { Hibernate.LONG, Hibernate.DOUBLE, Hibernate.STRING, Hibernate.TIMESTAMP };
    }
            
    public Object getPropertyValue(Object component, int property) throws HibernateException {
        switch (property) {
            case 0: return (component instanceof Long) ? component : null;
            case 1: return (component instanceof Double) ? component : null;
            case 2: return (component instanceof String) ? component : null;
            case 3: return (component instanceof Date) ? component : null;
        }
        throw new IllegalArgumentException();
    }
    
    public void setPropertyValue(Object component, int property, Object value) throws HibernateException {
        throw new UnsupportedOperationException();
    }
    
    public Class returnedClass() {
        return Object.class;
    }
    
    public boolean equals(Object x, Object y) throws HibernateException {
        return x==null ? y==null : x.equals(y);
    }
    
    public Object nullSafeGet(ResultSet rs, String[] names, SessionImplementor session, Object owner) 
        throws HibernateException, SQLException {
        
        //as above!
    }
    
    public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) 
        throws HibernateException, SQLException {
        
        //as above!
    }
    
    public Object deepCopy(Object value) throws HibernateException {
        return value;
    }
    
    public boolean isMutable() {
        return false;
    }
    
    public Serializable disassemble(Object value, SessionImplementor session) throws HibernateException {
        return value;
    }
    
    public Object assemble(Serializable cached, SessionImplementor session, Object owner) throws HibernateException {
        return value;
    }
}

Now we can write queries like this one:

from Item i join i.attributeValues value where value.name = 'foo' and value.inValue = 69

Back to top