Поддержка пользовательских полей в Hibernate

 

При реализации бизнес-приложений уровня корпорации (Enterprise Scale) очень часто заказчик требует реализовать поддержку расширяемости (extensibility) объектной модели приложения без модификации исходного кода системы. Использование расширяемой объектной модели (Extensible Domain Model) позволяет без дополнительных усилий и затрат на разработку новой функциональности:

1) использовать приложение более длительный срок
2) модифицировать процесс работы системы с течением времени и при изменении внешних обстоятельств
3) «подстраивать» приложение под особенности работы предприятия, на котором оно внедрено.

Самым простым и дешевым способом достижения требуемой функциональности является реализация в приложении расширяемых бизнес сущностей (Extensible Business Entities) с поддержкой пользовательских полей (Custom Fields).

 

Что такое «пользовательское поле»?

Что такое «пользовательское поле» и что оно дает конечному пользователю системы?
«Пользовательское поле» – это атрибут объекта, который не был создан разработчиком системы на этапе разработки системы, а был добавлен пользователем системы в этот объект в ходе эксплуатации системы, без внесения изменений в исходный кол приложения.

Как такая функциональность может быть востребована?

Попробуем разобраться в этом на примере предметной области CRM. Предположим, что у нас в системе есть объект «Клиент». Теоретически у данного объекта может быть сколь угодно много различных атрибутов: несколько email-ов, несколько различных видов телефонов, различные адреса, и т.п. Одни из них могут использоваться сотрудниками отдела продаж одной фирмы, но могут совершенно не использоваться в другой организации. Внести в объект все возможные атрибуты, которые могут (а могут и не) использоваться конечными пользователями системы, по крайней мере, расточительно и мало оправдано.

В этом случае лучше дать возможность самому пользователю (или администратору) системы создать те атрибуты, которые необходимы для работы менеджеров по продажам конкретной организации.

Например, администратор может создать атрибут «рабочий телефон», если в этом поле есть необходимость или атрибут «домашний адрес» и т.п. В дальнейшем эти поля могут использоваться в процессе работы приложения, например, по ним может осуществляться фильтрация и поиск данных.

Постановка задачи

В ходе реализации проекта Enterra CRM заказчиком была поставлена задача поддержки приложением функции Custom Fields в следующей формулировке:

«Администратор системы должен иметь возможность создания/удаления пользовательского поля без необходимости рестарта системы».

Для разработки бэк-энда системы был использован фрэймворк Hibernate 3.0. Это обстоятельство (технологическое ограничение) стало ключевым, с учетом которого было реализовано данное требование.

Техническое решение

В данном разделе мы приведем только ключевые моменты реализации с учетом особенностей использования фрэймворка Hibernate.

Среда

Приведенный демонстрационный вариант реализации разрабатывался с использованием:
1) JDK 1.5;
2) фрэймоворка Hibernate 3.2.0;
3) MySQL 4.1.

Ограничения

Для простоты мы не будем использовать Hibernate EntityManager, Hibernate Annotations. Маппинг персистентных объектов построим на основе xml mapping файлов. Более того, стоит отметить, что при использовании Hibernate Annotations приведенный вариант реализации не будет работоспособен, т.к. он основывается на управлении xml mapping файлами.

Задача

Наша задача реализовать механизм позволяющий создать/удалить пользовательское поле в режиме реального времени, без перезагрузки приложения, добавить в него какое-нибудь значение и убедиться, что данное значение отражено в базе данных приложения. Более того, нам необходимо убедиться, что мы можем использовать данное пользовательское поле в запросах.

Решение

Доменная модель

Начнем с того, что нам понадобится класс бизнес сущности, над которым мы будем производить эксперименты. Пусть это будет класс Contact. В нем будет два персистентных поля: id и name.
Но кроме этих постоянных и неизменных полей, данный класс должен предоставлять некоторую конструкцию для хранения значений пользовательских полей. Идеальной конструкцией для этих целей является Map.
Сделаем общий базовый класс для всех бизнес сущностей поддерживающих пользовательские поля – СustomizableEntity, который содержит Map CustomProperties для работы с пользовательскими полями:

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);  }    }

Листинг 1 – базовый класс CustomizableEntity

Унаследуем наш класс Contact от этого базового класса:

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;  }    }

Листинг 2 – Класс Contact унаследованный от CustomizableEntity.

Не стоит забывать про маппинг файл этого для класса:

<?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>

Листинг 3 – Маппинг класс Contact.
На что стоит обратить внимание, что свойства id и name – оформляются как и все обычные свойства, а вот для customProperties мы используем тэг . Документация по Hibernate 3.2.0GA говорит нам, что назначение dynamic-component состоит в:

“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.”

Собственно на основе этого положения из документации Hibernate мы и будем выстраивать механизм работы данной функции.

HibernateUtil и hibernate.cfg.xml

После того как мы определились с доменной моделью нашего приложения нам необходимо создать необходимые условия для функционирования фрэймворка Hibernate. Для этого необходимо создать конфигурационный файл hibernate.cfg.xml и класс для работы с базовыми функциями Hibernate.

<?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>

Листинг 4 – конфигурационный файл Hibernate.

Файл hibernate.cfg.xml не содержит ничего примечательного кроме, пожалуй, строчки:

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

Листинг 5 – использование авто-апдейта.

Позднее я остановлюсь на ее назначении, и расскажу, как можно было бы обойтись без нее.
Существует несколько реализаций класса HibernateUtil. Наша реализация будет немного отличаться от известных мне именно из-за того, что нам в ходе работы придется изменять конфигурацию Hibernate.

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());  }  }

Листинг 6 –HibernateUtils класс.
Наряду с обычными методами, такими как getCurrentSession(), getConfiguration(), который необходим для обычной работы приложения на базе Hibernate, мы реализовали такие методы как: reset() и getClassMapping(Class entityClass).
В методе getConfiguration(), мы производим конфигурирование Hibernate и добавляем класс Contact в конфигурацию.
Метод reset() используется для закрытия всех используемых Hibernate ресурсов и сброса всех его настроек:

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;  }

Листинг 7 – метод reset()
Метод getClassMapping(Class entityClass) возвращает объект PersistentClass, который содержит полную информацию о маппинге переданной ему сущности. Именно манипуляции с объектом PersistentClass и позволяют модифицировать состав атрибутов класса сущности в режиме run-time.

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

Листинг 8 – метод getClassMapping(Class entityClass).

Манипуляции с маппингом

После того как у нас есть класс бизнес сущности (Contact) и основной класс для взаимодействия с Hibernate мы можем начинать работу. Мы можем создавать и сохранять экземпляры класса Contact. Мы можем даже помещать какие-нибудь данные в наш Map customProperties, но мы должны знать, что эти данные (хранящиеся в Map-е customProperties) не сохранятся в БД.
Для того, чтобы они сохранились, нам надо обеспечить механизм создания пользовательских полей в нашем классе, и сделать так, чтобы Hibernate научился с ними работать.
Для того чтобы обеспечить возможность манипуляций маппингом класса мы должны создать некоторый интерфейс для этого. Назовем его CustomizableEntityManager. Его название должно отражать назначение данного интерфейса – управление бизнес сущностью, ее содержимым, ее атрибутами:

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();  }

Листинг 9 – Интерфейс CustomizableEntityManager
Основными методами данного интерфейса являются естественно: void addCustomField(String name) и void removeCustomField(String name). Которые должны создать и удалить наше пользовательское поле в маппинге соотвествующего класса.
Ниже приводится реализация данного интерфейса:

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);      }  }

Листинг 10 – реализация интерфейса CustomizableEntityManager
Прежде всего, надо отметить, что при создании класса CustomizableEntityManager мы указываем каким собственно классом бизнес-сущности будет управлять этот менеджер. Этот класс передается в качестве параметра конструктору CustomizableEntityManager:

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

Листинг 11 – конструктор класса CustomizableEntityManagerImpl
Теперь нас больше всего интересует реализация метода 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();  }

Листинг 12 – Создание пользовательского поля.
Как видим из реализации, Hibernate предоставляет большие возможности по работе со свойствами персистентных объектов и их отображением в БД. По сути данного метода:
1) Мы создаем объект класс SimpleValue, которые позволяет нам указать, каким образом будет храниться значение данного пользовательского поля в БД – в каком поле и в какой таблице БД:

SimpleValue simpleValue = new SimpleValue();  simpleValue.addColumn(new Column("fld_" + name));  simpleValue.setTypeName(String.class.getName());    PersistentClass persistentClass = getPersistentClass();  simpleValue.setTable(persistentClass.getTable());

Листинг 13 – создание колонки таблицы.

2) Создаем свойство персистентного объекта и добавляем его в динамический компонент (!), который мы и планировали использовать для данной цели:

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

Листинг 14 – создание свойства объекта.

3) Последнее что нам нужно сделать – это сделать так, чтобы наше приложение произвело соответствующие изменения в xml файлах и обновило конфигурацию Hibernate. Делается это методом updateMapping();
Необходимо прояснить назначение еще двух методов вызовы, которых производились в описанном выше коде. Первый метод – это getCustomProperties():

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

Листинг 15 – получение CustomProperties как Component.
Данный метод находи и возвращает объект Component соответствующий тэгу в маппинге нашей бизнес-сущности.
Второй метод это updateMapping():

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

Листинг 16 – метод updateMapping().
Он отвечает за сохранение измененного маппинга персистентного класса, и сбрасывает состояние конфигурации Hibernate, для того, чтобы при следующем обращении изменения, внесенные нами, вступили в силу.
Кстати, тут стоит вернуться к строке:
<property name=”hibernate.hbm2ddl.auto”>update</property>
конфигурации Hibernate. Если бы этой строки не было, то нам необходимо было бы еще и запустить обновление схемы БД средствами Hibernate. Но использование указанной настройки позволяет нам не делать этого.

Сохранение маппинга

Модификации маппинга произведенные в run-time не сохраняются сами по себе в соответствующий xml файл маппинга, и для того, чтобы внесенные изменения вступили в силу и при следующем запуске приложения, нам необходимо самостоятельно (врусную) сохранить изменения в соответствующий маппинг файл.
Для этого мы будем использовать класс MappingManager основная задача которого – сохранение маппинга указанной бизнес-сущности в ее xml маппинг-файл:

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;      }  }

Листинг 17 – утилита для обновления Маппинга персистентного класса.
Данный класс делает буквально следующее:
1) Определяет местонахождение и загружает xml маппинг для указанной бизнес-сущности в объект DOM Document для последующих манипуляций с ним;
2) Находит элемент данного документ . Именно в нем мы храним пользовательские поля и именно его содержимое меняем;
3) Удаляем (!) все вложенные элементы из данного элемента;
4) Для каждого персистентного свойства (Property) содержащегося в нашем Компоненте, отвечающем за хранение пользовательских полей, создаем соответствующий элемент документа и устанавливаем этому элементу атрибуты из соответствующего объекта Property;
5) Сохраняем этот вновь созданный файл маппинга.
При манипуляциях с XML мы используем (как видно из кода) класс XMLUtil, который в принципе может быть реализован как угодно, лишь бы правильно загружал и сохранял xml файл.
Наша реализация приведена на следующем листинге:

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();      }  }

Листинг 18 – утилита для манипуляций с XML.

Тестирование

Теперь, когда весь необходимый нам рабочий код написан, мы можем написать тесты и посмотреть как же это все работает.
Первый тест будет создавать пользовательское поле email, создавать и сохранять объект класса контакт и устанавливать ему свойство email.
Для начала посмотрим на таблицу данных tbl_contact. Она содержит два поля: fld_id, fld_name.
Код теста приведен ниже:

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 = "[email protected]";        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);          }      }  }

Листинг 19 – тест создания пользовательского поля.
Данный метод делает следующее:
1) Создает CustomizableEntityManager для класса Contact;
2) Создает новое пользовательское поле в нем под названием email;
3) Затем в транзакции создаем новый контакт и присваеваем пользовательскому полю значение “[email protected]”;
4) Сохраняем Контакт;
5) Получаем значение пользовательского поля “email”.
В результате выполнения видим следующее:

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 = [email protected]

Листинг 20 – результат теста.
В базе данных видим следующую запись:

+--------+---------------------+----------------------+  | fld_id    | fld_name                 | fld_email                   |  +--------+---------------------+----------------------+  |     1     | Contact Name 1        | [email protected]          |  +--------+---------------------+----------------------+

Листинг 21 – Результат в БД.
Как мы видим новое поле создано в режиме run-time и значение этого поля успешно сохранено.
Второй тест производит запрос к БД с использованием вновь созданного поля:

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", "[email protected]"));          List list = criteria.list();          System.out.println("list.size() = " + list.size());      }  }

Листинг 22 – Тест запроса по пользовательскому полю.
Результат выполнения:

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

Листинг 23 – Результат выполнения запроса.
Как мы видим пользовательское поле, созданное по нашей технологии, очень легко участвует в запросах к БД.

Дальнейшее развитие идеи

Очевидно, что реализация, приведенная в данной статье, достаточно примитивна. Она не отражает всего многообразия возникающих при работе с данной функциональности вариантов. Однако она показывает общий механизм работы данного решения на предложенной технологической платформе.

Очевидно также, что данное требование может быть реализовано с использованием другим механизмов (например, кодогенерация), которые возможно будут рассмотрены в других статьях.

Данная реализация поддерживает только тип String в качестве Custom Fields, в реальном же приложении, построенном на базе этого подхода (Enterra CRM), была реализована полная поддержка, как всех примитивных типов, так и объектных типов (ссылок на бизнес-объекты) и коллекционных полей.

Для поддержки пользовательских полей в части пользовательского интерфейса была реализована система мета-описателей пользовательских полей, которые использовались системой генерации пользовательского интерфейса. Но механизм работы данного генератора – это тема для отдельной статьи.

Заключение

В результате работы команды проекта Enterra CRM была разработана, апробирована и применена на практике архитектура расширяемой объектной модели на базе ORM платформы Hibernate, которая позволила удовлетворить потребности заказчика в плане настройки приложения под потребности конечных пользователей в режиме эксплуатации системы, без необходимости внесения изменений в исходный код приложения.