BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Using Hibernate to Support Custom Domain Object Fields

Using Hibernate to Support Custom Domain Object Fields

This item in japanese

Bookmarks

Introduction

When developing corporate-level business applications (Enterprise Scale) customers often requires implementing support for extensibility of the application object model not modifying the system source code. Use of extensible domain model allows for development of new functionality without additional effort and overheads:

  1. the application will be used for a more lengthy period
  2. the system workflow can be modified over time when changing external factors
  3. "setting" the application to fit specifics of an enterprise where it has been deployed.

The most simple and cost effective way to achieve the required functionality would be implementing extensible business entities in an application with the support of custom fields.

What is a "Custom Field"?

What is a custom field and how the end user benefit from this? A custom field is an attribute of an object which has not been created by the system developer at the development stage but that has been added by the system user into the object when actually using the system not introducing any changes into the source code of the application.

Which functionality may be demanded?

Let's try to figure out this based on an example of a CRM application. Let's say we have an object "Client". Theoretically this object can have any number of various attributes: several email addresses, many phone numbers, addresses etc. One of these can be used by Sales Department of one company however will be totally ignored by another organization. To enter all possible attributes into the object that may (may not) be used by the end users is wasteful and unjustified.

In this case if would be better to allow the user (or administrator) of the system to create the attributes that are necessary for sales managers in a specific organization. For example, administrator can create an attribute "work phone" if this field is actually required or "home address" and etc. Further these fields can be used in the application for example for filtering and searching of data.

Brief Description

While implementing the Enterra CRM project the customer set forth the task to support Custom Filed in the application as: "System administrator should be able to create/delete custom fields without the need to restart the system".

Hibernate 3.0 framework was used to develop the back end. This factor (technological constraint) was the key that was taken into consideration to implement the requirement.

Implementation

In this chapter we will provide key moments of the implementation considering specifics of using Hibernate as the framework.

Environment

The implementation demo variant from below was developed using:

  1. JDK 1.5
  2. Hibernate 3.2.0 framework;
  3. MySQL 4.1.

Limitations

To make it simpler we will not be using Hibernate EntityManager, Hibernate Annotations. Mapping of persistent objects will be built on xml file mapping. Moreover, it is worth mentioning that when using Hibernate Annotations the sample implementation demo will not be functional since is it based on management by xml file mapping.

Task definition

We will have to implement a mechanism allowing for creating/deleting custom fields in real time avoiding the application restart, add a value into it and make sure the value is present in the application database. Besides we will have to make sure that the custom field can be used in queries.

Solution

Domain Model

We will first need a business entity class which we will experiment with. Let is be Contact class. There will be two persistent fields: id and name.

However besides these permanent and unchangeable fields the class should be some sort of construction to store values of custom fields. Map would be an ideal construction for this.

Let's create a base class for all business entities supporting custom fields - CustomizableEntity, that contains Map CustomProperties to work with custom fields:

package com.enterra.customfieldsdemo.domain;

import java.util.Map;
import java.util.HashMap;

public abstract class CustomizableEntity {

private Map customProperties;

public Map getCustomProperties() {
if (customProperties == null)
customProperties = new HashMap();
return customProperties;
}
public void setCustomProperties(Map customProperties) {
this.customProperties = customProperties;
}

public Object getValueOfCustomField(String name) {
return getCustomProperties().get(name);
}

public void setValueOfCustomField(String name, Object value) {
getCustomProperties().put(name, value);
}

}

Listing 1 - base class CustomizableEntity

Inherit our class Contact from this base class:

package com.enterra.customfieldsdemo.domain;

import com.enterra.customfieldsdemo.domain.CustomizableEntity;

public class Contact extends CustomizableEntity {

private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

}

Listing 2 - Class Contact inherited from CustomizableEntity.

We should not forget about the mapping file for this class:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping auto-import="true" default-access="property" default-cascade="none" default-lazy="true">

<class abstract="false" name="com.enterra.customfieldsdemo.domain.Contact" table="tbl_contact">

<id column="fld_id" name="id">
<generator class="native"/>
</id>

<property name="name" column="fld_name" type="string"/>
<dynamic-component insert="true" name="customProperties" optimistic-lock="true" unique="false" update="true">
</dynamic-component>
</class>
</hibernate-mapping>

Listing 3 - Mapping Class Contact.

Please note that properties id and name are done as all ordinary properties, however for customProperties we use a tag <dynamic-component>. Documentation on Hibernate 3.2.0GA says that the point of a dynamic-component is:

"The semantics of a <dynamic-component> mapping are identical to <component>. The advantage of this kind of mapping is the ability to determine the actual properties of the bean at deployment time, just by editing the mapping document. Runtime manipulation of the mapping document is also possible, using a DOM parser. Even better, you can access (and change) Hibernate's configuration-time metamodel via the Configuration object."

Based on this regulation from Hibernate documentation we will be building this function mechanism.

HibernateUtil and hibernate.cfg.xml

After we are defined with the domain model of our application we have to create necessary conditions for Hibernate framework functioning. For this we have to create a configuration file hibernate.cfg.xml and class to work with the core Hibernate functions.

<?xml version='1.0' encoding='utf-8'?>

<!DOCTYPE hibernate-configuration

PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"

"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

<session-factory>

<property name="show_sql">true</property>
<property name="dialect">
org.hibernate.dialect.MySQLDialect</property>
<property name="cglib.use_reflection_optimizer">true</property>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/custom_fields_test</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password"></property>
<property name="hibernate.c3p0.max_size">50</property>
<property name="hibernate.c3p0.min_size">0</property>
<property name="hibernate.c3p0.timeout">120</property>
<property name="hibernate.c3p0.max_statements">100</property>
<property name="hibernate.c3p0.idle_test_period">0</property>
<property name="hibernate.c3p0.acquire_increment">2</property>
<property name="hibernate.jdbc.batch_size">20</property>
<property name="hibernate.hbm2ddl.auto">update</property>
</session-factory>
</hibernate-configuration>

Listing 4 - Hibernate configuration file.

The file hibernate.cfg.xml does not contain anything noticeable except for this string:

<property name="hibernate.hbm2ddl.auto">update</property>

Listing 5 - using auto-update.

Later we will explain in details on its purpose and tell more how we can go without it. There are several ways to implement class HibernateUtil. Our implementation will differ a bit from well known due to changes into Hibernate configuration.

package com.enterra.customfieldsdemo;

import org.hibernate.*;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.hibernate.cfg.Configuration;
import com.enterra.customfieldsdemo.domain.Contact;

public class HibernateUtil {

private static HibernateUtil instance;
private Configuration configuration;
private SessionFactory sessionFactory;
private Session session;

public synchronized static HibernateUtil getInstance() {
if (instance == null) {
instance = new HibernateUtil();
}
return instance;
}

private synchronized SessionFactory getSessionFactory() {
if (sessionFactory == null) {
sessionFactory = getConfiguration().buildSessionFactory();
}
return sessionFactory;
}

public synchronized Session getCurrentSession() {
if (session == null) {
session = getSessionFactory().openSession();
session.setFlushMode(FlushMode.COMMIT);
System.out.println("session opened.");
}
return session;
}

private synchronized Configuration getConfiguration() {
if (configuration == null) {
System.out.print("configuring Hibernate ... ");
try {
configuration = new Configuration().configure();
configuration.addClass(Contact.class);
System.out.println("ok");
} catch (HibernateException e) {
System.out.println("failure");
e.printStackTrace();
}
}
return configuration;
}
public void reset() {
Session session = getCurrentSession();
if (session != null) {
session.flush();
if (session.isOpen()) {
System.out.print("closing session ... ");
session.close();
System.out.println("ok");
}
}
SessionFactory sf = getSessionFactory();
if (sf != null) {
System.out.print("closing session factory ... ");
sf.close();
System.out.println("ok");
}
this.configuration = null;
this.sessionFactory = null;
this.session = null;
}

public PersistentClass getClassMapping(Class entityClass){
return getConfiguration().getClassMapping(entityClass.getName());
}
}

Listing 6 - HibernateUtils class.

Alongside with usual methods like getCurrentSession(), getConfiguration(), which is necessary for regular work of the application based on Hibernate, we also have implemented such methods as: reset() and getClassMapping(Class entityClass). In the method getConfiguration(), we configure Hibernate and add class Contact into the configuration.

Method reset() has been used to close all used by Hibernate resources and clearing all of its settings:

public void reset() {
Session session = getCurrentSession();
if (session != null) {
session.flush();
if (session.isOpen()) {
System.out.print("closing session ... ");
session.close();
System.out.println("ok");
}
}
SessionFactory sf = getSessionFactory();
if (sf != null) {
System.out.print("closing session factory ... "); sf.close();
System.out.println("ok");
}
this.configuration = null;
this.sessionFactory = null;
this.session = null;
}

Listing 7 - method reset()

Method getClassMapping(Class entityClass) returns object PersistentClass, that contains full information on mapping the related entity. In particular the manipulations with the object PersistentClass allow modifying the set of attributes of the entity class in the run-time.

public PersistentClass getClassMapping(Class entityClass){
return
getConfiguration().getClassMapping(entityClass.getName());
}

Listing 8 - method getClassMapping(Class entityClass).

Manipulations with mapping

Once we have the business entity class (Contact) available and the main class to interact with Hibernate we can start working. We can create and save samples of the Contact class. We can even place some data into our Map customProperties, however we should be aware that this data (stored in Map customProperties) are not saved to the DB.

To have the data saved we should provide for the mechanism of creating custom fields in our classs and make it the way Hibernate knows how to work with them.

To provide for class mapping manipulation we should create some interface. Let's call it CustomizableEntityManager. Its name should reflect the purpose of the interface managing a business entity, its contents and attributes:

package com.enterra.customfieldsdemo;

import org.hibernate.mapping.Component;

public interface CustomizableEntityManager {
public static String CUSTOM_COMPONENT_NAME = "customProperties";

void addCustomField(String name);

void removeCustomField(String name);

Component getCustomProperties();

Class getEntityClass();
}

Listing 9 - Interface CustomizableEntityManager

The main methods for the interface are: void addCustomField(String name) and void removeCustomField(String name). These should created and remove our custom field in the mapping of the corresponding class.

Below is the way to implement the interface:

package com.enterra.customfieldsdemo;

import org.hibernate.cfg.Configuration;
import org.hibernate.mapping.*;
import java.util.Iterator;

public class CustomizableEntityManagerImpl implements CustomizableEntityManager {
private Component customProperties;
private Class entityClass;

public CustomizableEntityManagerImpl(Class entityClass) {
this.entityClass = entityClass;
}

public Class getEntityClass() {
return entityClass;
}

public Component getCustomProperties() {
if (customProperties == null) {
Property property = getPersistentClass().getProperty(CUSTOM_COMPONENT_NAME);
customProperties = (Component) property.getValue();
}
return customProperties;
}

public void addCustomField(String name) {
SimpleValue simpleValue = new SimpleValue();
simpleValue.addColumn(new Column("fld_" + name));
simpleValue.setTypeName(String.class.getName());

PersistentClass persistentClass = getPersistentClass();
simpleValue.setTable(persistentClass.getTable());

Property property = new Property();
property.setName(name);
property.setValue(simpleValue);
getCustomProperties().addProperty(property);

updateMapping();
}

public void removeCustomField(String name) {
Iterator propertyIterator = customProperties.getPropertyIterator();

while (propertyIterator.hasNext()) {
Property property = (Property) propertyIterator.next();
if (property.getName().equals(name)) {
propertyIterator.remove();
updateMapping();
return;
}
}
}

private synchronized void updateMapping() {
MappingManager.updateClassMapping(this);
HibernateUtil.getInstance().reset();
// updateDBSchema();
}

private PersistentClass getPersistentClass() {
return HibernateUtil.getInstance().getClassMapping(this.entityClass);
}
}

Listing 10 - implementing interface CustomizableEntityManager

First of all we should point out that when creating class CustomizableEntityManager we specify the business entity class the manager will operate. This class is passed as a parameter to designer CustomizableEntityManager:

private Class entityClass;

public CustomizableEntityManagerImpl(Class entityClass) {
this.entityClass = entityClass;
}

public Class getEntityClass() {
return entityClass;
}

Listing 11 - class designer CustomizableEntityManagerImpl

Now we should get more interested in how to implement method void addCustomField(String name):

public void addCustomField(String name) {
SimpleValue simpleValue = new SimpleValue();
simpleValue.addColumn(new Column("fld_" + name));
simpleValue.setTypeName(String.class.getName());

PersistentClass persistentClass = getPersistentClass();
simpleValue.setTable(persistentClass.getTable());

Property property = new Property();
property.setName(name);
property.setValue(simpleValue);
getCustomProperties().addProperty(property);

updateMapping();
}

Listing 12 - creating custom field.

As we can see from the implementation, Hibernate offers more options in working with properties of persistent objects and their representation in the DB. As per the essence of the method:

1) We create class SimpleValue that allow us to denote how the value of this custom field will be stored in the DB in which field and table of the DB:

SimpleValue simpleValue = new SimpleValue();
simpleValue.addColumn(new Column("fld_" + name));
simpleValue.setTypeName(String.class.getName());

PersistentClass persistentClass = getPersistentClass();
simpleValue.setTable(persistentClass.getTable());

Listing 13 - creating new column of the table.

2) We create a property of the persistent object and add a dynamic component into it (!), that we have planned to be used for this purpose:

Property property = new Property()
property.setName(name)
property.setValue(simpleValue)
getCustomProperties().addProperty(property)

Listing 14 - creating object property.

3) And finally we should make our application perform certain changes in the xml files and update the Hibernate configuration. This can be done via method updateMapping();

It is necessary to clarify the purpose of another two get-methods which have been used in the code above. The first method is getCustomProperties():

public Component getCustomProperties() {
if (customProperties == null) {
Property property = getPersistentClass().getProperty(CUSTOM_COMPONENT_NAME);
customProperties = (Component) property.getValue();
}
return customProperties;
}

Listing 15 - getting CustomProperties as Component.

This method finds and returns object Component corresponding to the tag in the mapping of our business entity.

The second method is updateMapping():

private synchronized void updateMapping() {
MappingManager.updateClassMapping(this);
HibernateUtil.getInstance().reset();
// updateDBSchema();
}

Listing 16 - method updateMapping().

The method is in charge for storing the updated mapping of the persistent class and updates the configuration status of Hibernate to make further changes that we make valid when the changes take effect.

By the way we should get back to the string:

<property name="hibernate.hbm2ddl.auto">update</property>

of the Hibernate configuration. If this string was missing we would have to launch executing updates of the DB schema using hibernate utilities. However using the setting allows us to avoid this.

Saving mapping

Modifications to mapping made in run-time do not save by themselves into the corresponding xml mapping file and to make the changes to get activated at next launch of the application we need to manually save changes to the corresponding mapping file.

To do this we will be using class MappingManager the main purpose of which is to save mapping of the designated business entity to its xml mapping file:

package com.enterra.customfieldsdemo;

import com.enterra.customfieldsdemo.domain.CustomizableEntity;
import org.hibernate.Session;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Property;
import org.hibernate.type.Type;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Iterator;

public class MappingManager {
public static void updateClassMapping(CustomizableEntityManager entityManager) {
try {
Session session = HibernateUtil.getInstance().getCurrentSession();
Class<? extends CustomizableEntity> entityClass = entityManager.getEntityClass();
String file = entityClass.getResource(entityClass.getSimpleName() + ".hbm.xml").getPath();

Document document = XMLUtil.loadDocument(file);
NodeList componentTags = document.getElementsByTagName("dynamic-component");
Node node = componentTags.item(0);
XMLUtil.removeChildren(node);

Iterator propertyIterator = entityManager.getCustomProperties().getPropertyIterator();
while (propertyIterator.hasNext()) {
Property property = (Property) propertyIterator.next();
Element element = createPropertyElement(document, property);
node.appendChild(element);
}

XMLUtil.saveDocument(document, file);
} catch (Exception e) {
e.printStackTrace();
}
}

private static Element createPropertyElement(Document document, Property property) {
Element element = document.createElement("property");
Type type = property.getType();

element.setAttribute("name", property.getName());
element.setAttribute("column", ((Column)
property.getColumnIterator().next()).getName());
element.setAttribute("type",
type.getReturnedClass().getName());
element.setAttribute("not-null", String.valueOf(false));

return element;
}
}

Listing 17 - the utility to update mapping of the persistent class.

The class literally performs the following:

  1. Defines a location and loads xml mapping for the designated business entity into the DOM Document object for further manipulations with it;
  2. Finds the element of this document <dynamic-component>. In particular here we store the custom fields and its contents we change;
  3. Delete (!) all embedded elements from this element;
  4. For any persistent property contained in our component that is in charge for the custom fields storage, we create a specific document element and define attributes for the element from the corresponding property;
  5. Save this newly created mapping file.

When manipulating XML we use (as we can see from the code) class XMLUtil, that in general can be implemented in any way though it should correctly load and save the xml file.

Our implementation is given at the listing below:

package com.enterra.customfieldsdemo;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.dom.DOMSource;
import java.io.IOException;
import java.io.FileOutputStream;

public class XMLUtil {
public static void removeChildren(Node node) {
NodeList childNodes = node.getChildNodes();
int length = childNodes.getLength();
for (int i = length - 1; i > -1; i--)
node.removeChild(childNodes.item(i));
}

public static Document loadDocument(String file)
throws ParserConfigurationException, SAXException, IOException {

DocumentBuilderFactory factory =DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
return builder.parse(file);
}

public static void saveDocument(Document dom, String file)
throws TransformerException, IOException {

TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();

transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, dom.getDoctype().getPublicId());
transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, dom.getDoctype().getSystemId());

DOMSource source = new DOMSource(dom);
StreamResult result = new StreamResult();

FileOutputStream outputStream = new FileOutputStream(file);
result.setOutputStream(outputStream);
transformer.transform(source, result);

outputStream.flush();
outputStream.close();
}
}

Listing 18 - XML manipulation utility.

Testing

Now when we have all the necessary working code at place we can write tests and see how everything works. The first test will create the custom field "email", create and save the object of the class Contact and define it the "email" property.

First let's take a look at the data table tbl_contact. It contains two fields: fld_id, fld_name. The code is provided below:

package com.enterra.customfieldsdemo.test;

import com.enterra.customfieldsdemo.HibernateUtil;
import com.enterra.customfieldsdemo.CustomizableEntityManager;
import com.enterra.customfieldsdemo.CustomizableEntityManagerImpl;
import com.enterra.customfieldsdemo.domain.Contact;
import org.hibernate.Session;
import org.hibernate.Transaction;
import java.io.Serializable;

public class TestCustomEntities {
private static final String TEST_FIELD_NAME = "email";
private static final String TEST_VALUE = "test@test.com";

public static void main(String[] args) {
HibernateUtil.getInstance().getCurrentSession();

CustomizableEntityManager contactEntityManager = new
CustomizableEntityManagerImpl(Contact.class);

contactEntityManager.addCustomField(TEST_FIELD_NAME);

Session session = HibernateUtil.getInstance().getCurrentSession();

Transaction tx = session.beginTransaction();
try {

Contact contact = new Contact();
contact.setName("Contact Name 1");
contact.setValueOfCustomField(TEST_FIELD_NAME, TEST_VALUE);
Serializable id = session.save(contact); tx.commit();

contact = (Contact) session.get(Contact.class, id);
Object value = contact.getValueOfCustomField(TEST_FIELD_NAME);
System.out.println("value = " + value);

} catch (Exception e) {
tx.rollback();
System.out.println("e = " + e);
}
}
}

Listing 19 - test to creation of the custom field.

This method is responsible for the following:

  1. Creation of CustomizableEntityManager for class Contact;
  2. Creation of new custom field named "email";
  3. Further in transaction we create a new contact and give the custom field value "test@test.com";
  4. Save Contact;
  5. Get the value of the cusom field "email"

As the result of the execution we can see the following:

configuring Hibernate ... ok
session opened.
closing session ... ok
closing session factory ... ok
configuring Hibernate ... ok
session opened.
Hibernate: insert into tbl_contact (fld_name, fld_email) values (?, ?)
value = test@test.com

Listing 20 - test result.

In the database the following record can be seen:

+--------+---------------------+----------------------+
| fld_id | fld_name | fld_email
|
+--------+---------------------+----------------------+
| 1 | Contact Name 1 | test@test.com |
+--------+---------------------+----------------------+

Listing 21 - DB result.

As we can see the new field has been created in the run-time and its value successfully saved.

The second test creates the query to the DB using the newly created field:

package com.enterra.customfieldsdemo.test;

import com.enterra.customfieldsdemo.HibernateUtil;
import com.enterra.customfieldsdemo.CustomizableEntityManager;
import com.enterra.customfieldsdemo.domain.Contact;
import org.hibernate.Session;
import org.hibernate.Criteria;
import org.hibernate.criterion.Restrictions;
import java.util.List;

public class TestQueryCustomFields {
public static void main(String[] args) {
Session session = HibernateUtil.getInstance().getCurrentSession();
Criteria criteria = session.createCriteria(Contact.class);
criteria.add(Restrictions.eq(CustomizableEntityManager.CUSTOM_COMPONENT_NAME + ".email", "test@test.com"));
List list = criteria.list();
System.out.println("list.size() = " + list.size());
}
}

Listing 22 - Query test by the custom field.

Execution result:
configuring Hibernate ... ok
session opened.
Hibernate: select this_.fld_id as fld1_0_0_, this_.fld_name as fld2_0_0_,
this_.fld_email as fld3_0_0_ from tbl_contact this_ where this_.fld_email=?
list.size() = 1

Listing 23 - Query execution result.

As we can see the custom field that has been created using our technology can easily participate in queries to the DB.

Further Improvements

Obviously the implementation we spoke above is rather primitive. It does not reflect all the variety of options that pop up at actual implementation of this functionality. Yet, it shows the general working mechanism of the solution on the technological platform suggested.

It is also obvious that the requirement can be implemented using other mechanisms (e.g. code generation) which may be considered in other articles.

This implementation supports only String type as Custom Fields however in the real application built with this approach (Enterra CRM) a full support has been implemented as for all primitive types as well as object types (links to business objects) and collection fields.

To support custom fields in the part of the user interface the system of meta-descriptors for the custom fields has been implemented which used the user interface generation system. However the mechanism of the generator is a theme for a separate article.

Conclusion

As the result of the Enterra CRM team has created, approved and applied in practice the architecture of open object model based on ORM platform Hibernate which allowed for satisfying of the customer requests referring to the application settings under actual need of the end users in the run-time with no necessity to make changes to the source code of the application.

Rate this Article

Adoption
Style

BT