導言
在開發企業級業務應用(企業規模)時,客戶往往要求在不修改系統源代碼的情況下對應用對象模型的擴展性提供支持。利用可擴展域模型可以實現新功能的開發,而不需要額外的精力和成本
- 應用的使用週期將被延長;
- 外部因素改變時,系統工作流也可以隨之被修改;
- 已經被部署的應用可以被“設定”,使其符合企業的特定情況。
完成以上功能需求最簡單、最具成本效益的方法應該是在應用中實現支持自定義字段的可擴展業務實體。
什麼是“自定義字段”?
什麼是自定義字段?最終用戶如何從中受益呢?自定義字段是一種對象屬性,它不是由系統開發人員在開發階段創建的,而是在系統實際使用中由系統用戶在不改變任何源代碼的情況下添加到對象中的。
可能會需要哪些功能呢?
讓我們舉一個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 = "[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-測試創建自定義字段
這個類的main方法負責執行下面的工作:
- 創建Contact類的CustomizableEntityManager;
- 創建名爲“email”的自定義字段;
- 在事務中,我們創建一個新的Contact對象,並設置自定義字段的值爲“[email protected]”;
- 保存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 = [email protected]
清單20-測試結果
在數據庫裏,可以看到如下所示的記錄:
+--------+---------------------+----------------------+ | fld_id | fld_name | fld_email | +--------+---------------------+----------------------+ | 1 | Contact Name 1 | [email protected] | +--------+---------------------+----------------------+
清單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", "[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類型的自定義字段,但是,基於該方法的實際應用(Enterra CRM)中, 已經實現了對所有原始類型、對象類型(鏈接到業務對象)以及集合字段的完全支持。
爲了在用戶界面支持自定義字段,已經實現了針對自定義字段的元描述符系統,該系統使用了用戶界面生成系統。但是生成器的機制是另外一篇文章的主題。
結論
最後,Enterra CRM團隊創建、驗證並在實踐中應用了基於ORM平臺Hibernate的開放對象模型架構,它滿足了客戶在運行時不需要對應用源代碼做任何改動、就可以按照最終用戶的實際需求設置應用的需求。
原文:http://www.blogjava.net/paulwong/archive/2008/01/23/177227.html