导言
在开发企业级业务应用(企业规模)时,客户往往要求在不修改系统源代码的情况下对应用对象模型的扩展性提供支持。利用可扩展域模型可以实现新功能的开发,而不需要额外的精力和成本
- 应用的使用周期将被延长;
- 外部因素改变时,系统工作流也可以随之被修改;
- 已经被部署的应用可以被“设定”,使其符合企业的特定情况。
完成以上功能需求最简单、最具成本效益的方法应该是在应用中实现支持自定义字段的可扩展业务实体。
什么是“自定义字段”?
什么是自定义字段?最终用户如何从中受益呢?自定义字段是一种对象属性,它不是由系统开发人员在开发阶段创建的,而是在系统实际使用中由系统用户在不改变任何源代码的情况下添加到对象中的。
可能会需要哪些功能呢?
让我们举一个CRM(客户关系管理系统)应用的例子来领会一下。 假设我们有一个客户“Client”对象。理论上讲,这个对象可以有任意多的各种属性:几个email地址、若干电话号码和地址等。某公司的销售部门可能会使用其中一个属性,但其它公司却会完全忽略它。将最终用户可能会用到的(也可能不会用到的)所有属性都加入到对象当中,这是很浪费并很不合理的。
既然这样,允许系统用户(或者管理员)来创建他们公司的销售经理们需要的属性,也许是更好的做法。例如,如果有需要,管理员可以创建“工作电话”或者“家庭地址”等属性。 此外,这些属性还可以用到数据过滤和查询中去。
简要说明
在实施Enterra CRM项目时,客户提出了在应用中支持自定义字段的目标,“系统管理员不需要重启系统就可以创建或删除自定义字段”。
系统后端开发使用了Hibernate 3.0框架,这个因素(技术约束)是考虑实现这个需求的关键。
实现
在这一章里面我们将介绍采用Hibernate框架实现的关键环节。
环境
例子的开发环境如下所示:
- JDK 1.5;
- Hibernate 3.2.0框架;
- MySQL 4.1。
限制
简单起见,我们不使用Hibernate EntityManager(译注一)和Hibernate Annotations(译注二)。持久化对象的映射关系将基于xml映射文件。此外,值得一提的是,由于演示用例是基于xml映射文件管理映射,所以使用Hibernate Annotations的话,它将不能正常运行。
功能定义
我们必须实现一种机制——允许实时地创建/删除自定义字段而不重启应用,向其中添加值并保证值能保存到应用的数据库中。此外我们还必须保证自定义字段能用于查询。
解决方案
域模型
首先,我们需要一个进行试验的业务实体类。假设是Contact类,它有两个持久化字段:id和name。
但是,除了这些持久不变的字段外,这个类还应该有一些存储自定义字段值的数据结构。Map也许是针对于此的理想数据结构。
为所有支持自定义字段的业务实体创建一个基类——CustomizableEntity,它包含处理自定义字段的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-继承自CustomizableEntity的Contact类
别忘了这个类的映射文件:
<?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>
清单3-Contact类的映射
注意id和name属性都是当作普通的属性来处理,但对于customProperties,我们使用了(动态组件)标签。Hibernate 3.2.0GA文档里面关于dynamic-component的要点如下:
<dynamic-component>映射的语义与<component>是一样的。该映射的优点是仅仅通过编辑映射文件,就能在部署时确定bean的现行属性。使用DOM解析器,映射文件的运行时操作也是可行的。甚至,你可以通过Configuration对象,来访问(和修改)Hibernate的配置时元模型。
基于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-使用auto-update(自动更新)
后面我们将详细解释其目的,并更多地讲解没有它我们怎样实现。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对象的处理允许在运行时修改实体类的属性设置。
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 property = new Property()
property.setName(name)
property.setValue(simpleValue)
getCustomProperties().addProperty(property)
清单14-创建对象属性
3)最后应该让应用修改xml文件,并更新Hibernate配置。这个可以由updateMapping()方法来完成;
阐明上面代码中另外两个get方法的用途还是很有必要的。第一个方法是getCustomProperties():
public Component getCustomProperties() {
if (customProperties == null) {
Property property = getPersistentClass().getProperty(CUSTOM_COMPONENT_NAME);
customProperties = (Component) property.getValue();
}
return customProperties;
}
清单15-获取组件CustomProperties
该方法找到并返回与业务实体映射中标签相对应的组件(Component)对象。
第二个方法是updateMapping():
private synchronized void updateMapping() {
MappingManager.updateClassMapping(this);
HibernateUtil.getInstance().reset();
// updateDBSchema();
}
清单16-updateMapping()方法
该方法负责存储更新后的持久化类映射,并且更新Hibernate的配置状态,以进一步使改变生效。
顺便,我们回过头来看看Hibernate配置中的语句:
<property name="hibernate.hbm2ddl.auto">update</property>
如果缺少该配置,我们就必须使用Hibernate工具类来执行数据库schema的更新。然而使用该设置让我们避免了那么做。
保存映射
运行时对映射的修改不会将自身保存到相应的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 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-更新持久化类映射的工具类
该类一一执行了下面的操作:
- 对于指定的业务实体,定义其xml映射的位置,并加载到DOM Document对象中,以供进一步操作;
- 查找到Document对象中的元素。我们将在这里存储自定义字段和我们所做的内容变化;
- 将该元素内嵌套的所有元素都删除;
- 对于负责自定义字段存储的组件所包含的任意持久化属性,我们都创建一个特定的document元素,并根据相应的属性为元素定义属性;
- 保存这个新建的映射文件。
虽然我们这里用了XMLUtil类(正如从代码中看到的一样)来处理XML,但是一般而言,可以换成任何一种方式来实现,不过XMLUtil已经足以加载并保存xml文件。
我们的实现如下面的清单所示:
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”,创建并保存Contact类的实例,并给它定义“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 = "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);
}
}
}
清单19-测试创建自定义字段
这个类的main方法负责执行下面的工作:
- 创建Contact类的CustomizableEntityManager;
- 创建名为“email”的自定义字段;
- 在事务中,我们创建一个新的Contact对象,并设置自定义字段的值为“test@test.com”;
- 保存Contact;
- 获取自定义字段“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 = test@test.com
清单20-测试结果
在数据库里,可以看到如下所示的记录:
+--------+---------------------+----------------------+
| fld_id | fld_name | fld_email |
+--------+---------------------+----------------------+
| 1 | Contact Name 1 | test@test.com |
+--------+---------------------+----------------------+
清单21-DB结果
正如看到的那样,新的字段在运行时被创建,其值也被成功保存。
第二个测试使用新创建的字段来查询数据库:
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());
}
}
清单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类型的自定义字段,但是,基于该方法的实际应用(Enterra CRM)中, 已经实现了对所有原始类型、对象类型(链接到业务对象)以及集合字段的完全支持。
为了在用户界面支持自定义字段,已经实现了针对自定义字段的元描述符系统,该系统使用了用户界面生成系统。但是生成器的机制是另外一篇文章的主题。
结论
最后,Enterra CRM团队创建、验证并在实践中应用了基于ORM平台Hibernate的开放对象模型架构,它满足了客户在运行时不需要对应用源代码做任何改动、就可以按照最终用户的实际需求设置应用的需求。
源码包:customfieldsdemo.zip