BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Hibernate を使ってカスタムドメインオブジェクトフィールドをサポートする

Hibernate を使ってカスタムドメインオブジェクトフィールドをサポートする

イントロダクション

企業レベルの業務アプリケーション(エンタープライズ規模)を開発するとき、システムのソースコードを変更せずにアプリケーションのオブジェクトモデルを拡張できるような実装を顧客に要求されることがよくあります。拡張可能なドメインモデルを利用すれば、付加的な労力やオーバーヘッドなしに新たな機能性を開発することができるようになります。

  1. そのアプリケーションが相当長期にわたって利用される
  2. 時間が経過し外部要因が変化すればシステムのワークフローが変わる可能性がある
  3. デプロイされた企業の特性に合わせてアプリケーションを設定する

要求される機能性を達成する最もシンプルかつコストパフォーマンスのよい方法は、カスタムフィールドをサポートすることによって、拡張可能なビジネスエンティティをアプリケーションに実装することでしょう。

カスタムフィールドとは?

カスタムフィールドとは何でしょうか?エンドユーザはどのようにそこから利益を得るのでしょうか?カスタムフィールドとは、開発時にシステム開発者が作成するのではなく、ユーザが実際にシステムを使うときにアプリケーションのソースコードを一切変更せずにオブジェクトに追加する属性です。

どの機能性が要求される可能性があるでしょうか?

CRM アプリケーションの例をもとにこれを理解してみましょう。仮に "Client" というオブジェクトがあるとします。理論上、このオブジェクトは各種の属性を好きなだけもつことができます。複数のメールアドレスにたくさんの電話番号や住所など。これらのうちのあるものは、ある企業の営業部門によって使用されるかもしれませんが、他の組織からは大体無視されます。エンドユーザから利用されるかどうかわからない属性をすべてオブジェクトに設定するのは無意味ですし、そんなことはすべきではありません。

このケースでは、システムのユーザ(または管理者)が、ある特定の組織のセールスマネージャが必要とする属性を作成することができるほうがよいでしょう。たとえば管理者は「業務用TEL」や「自宅住所」などが実際に必要とされるなら、これらの属性を作成すればよいのです。さらに、これらのフィールドはアプリケーション内でデータのフィルタリングや検索などのために利用することが可能です。

概要

Enterra CRM プロジェクトの実装の際、顧客はカスタムフィールドをサポートするタスクを「システム管理者はシステムを再起動せずにカスタムフィールドを追加・削除できるべき」であるとしました。

バックエンドの開発には Hibernate 3.0 フレームワークが利用されていました。この要因(技術的制約)が顧客の要求を実現するために考慮されたカギとなりました。

実装

この章では Hibernate をフレームワークとして利用することの特性について、実装で考慮する際の重要な点について述べます。

環境

以降に示す実装デモのバリアントの開発には次のテクノロジを使用しました。

  1. JDK 1.5
  2. Hibernate 3.2.0 フレームワーク
  3. MySQL 4.1

制限事項

シンプルにするため Hibernate EntityManager や Hibernate Annotations は利用しません。永続オブジェクトのマッピングには XML ファイルを用います。また、Hibernate Annotations は XML ファイルマッピングによる管理がベースにあるため、それを用いるとサンプルの実装デモは機能的にはならない、ということは述べておく価値があります。

タスクの定義

私たちはアプリケーションの再起動を避けながらリアルタイムにカスタムフィールドを作成・削除することを可能にするメカニズムを実装し、それに値を追加したときにその値が確実にアプリケーションのデータベースに保存されるようにする必要があります。さらに、カスタムフィールドがクエリで利用できるようにしなくてはなりません。

ソリューション

ドメインモデル

まずは実験用のビジネスエンティティクラスが必要です。Contact クラスとしましょう。このクラスには id と name という2つの永続フィールドがあります。

これらのフィールドは永久に存在し名前も変化しませんが、このクラスはそれ以外にカスタムフィールドの値を格納するための構造をもたなくてはなりません。そのための構造としては Mapが理想的でしょう。

カスタムフィールドをサポートするすべてのビジネスエンティティ用に基底クラス CustomizableEntity を作成しましょう。このクラスはカスタムフィールドを扱うために customProperties という Map を保持しています。

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>
</hibernate-mapping>

リスト 3 - Contact クラスのマッピング

プロパティ id と name は通常のプロパティと同じように書きますが、customProperties には タグを用いるところに注意してください。Hibernate 3.2.0GA のドキュメントによると dynamic-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 )が利用可能になり、main クラスが Hibernate とのやりとりを開始したら、私たちは作業を始めることができます。私たちは Contact クラスのサンプルオブジェクトを作成・保存することができますし、customPropertiesマップにいくつかのデータを格納することもできますが、 customPropertiesに格納したデータはDBには保存されないということは知っておく必要があります。

データを保存するには、クラスにカスタムフィールドを作成するメカニズムを準備して、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 は永続オブジェクトのプロパティやそれらの DB 内部表現を扱うための追加オプションを提供しています。それは次に挙げるメソッドの重要部分が示すとおりです。

1) カスタムフィールドが DB のどのテーブルのどのフィールドにどのように格納されるかを示す 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() メソッドを通じて実行されます。

上述のコードに使われた他の2つの get メソッドの目的をはっきりさせる必要があります。まずは 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 ユーティリティを使って DB スキーマの更新を実行しなければならないでしょう。しかしこの設定を利用することでそれを避けることができます。

マッピングの保存

実行時に行われたマッピングの変更は対応する xml マッピングファイルに自動的に保存されないため、次回のアプリケーション起動時にも変更内容が有効になるように、私たちは手動でこれを行う必要があります。

これを行うため、指定されたビジネスエンティティのマッピングを xml マッピングファイルに保存することを主目的とする MappingManager クラスを使っています。

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 - 永続クラスのマッピングを更新するユーティリティ

このクラスは以下に挙げた処理をそのまま実行します。

  1. 保存先ファイルを決めて、指定されたビジネスエンティティの xml マッピングを DOM ドキュメントオブジェクトに読み込み、以降の操作に備えます。
  2. このドキュメントの 要素を見つけます。ここにカスタムフィールドとその値を格納します。
  3. この要素から、格納されているすべての要素を削除(!)します。
  4. カスタムフィールドの格納を担当しているコンポーネントに含まれているすべての永続プロパティに対してそれぞれドキュメント要素を作成し、属性をその要素に対応するプロパティから定義しmさう。
  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" というカスタムフィールドを作成し、Contact クラスのオブジェクトを作成・保存してカスタムフィールドを "email" というプロパティとして定義します。

はじめにデータテーブル tbl_contact を見てみましょう。テーブルには fld_id と fld_name という2つのフィールドが含まれます。コードを次に示します。

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 - カスタムフィールド生成のテスト

このメソッドは次のことを担当します。

  1. Contact クラス用 CustomizableEntityManager の作成
  2. カスタムフィールド "email" の作成
  3. 次にトランザクションを開始して新しい Contact オブジェクトを生成し、カスタムフィールドの値として "test@test.com"をセット
  4. Contact の保存
  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 = test@test.com

リスト 20 - テスト結果

データベースには次のレコードが確認できます。

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

リスト 21 - DB 結果

見てのとおり、新しいフィールドが実行時に作成されてその値が首尾よく保存されます。

二つ目のテストは新たに作成したフィールドを使って DB に向けたクエリを作成します。

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

リスト 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 - クエリの実行結果

見てのとおり、この技術を使って作成したカスタムフィールドは DB へのクエリに簡単に加えることができます。

さらなる改善

私たちが上述した実装は明らかに初歩的なものであり、この機能の本来の実装に現れる豊富なオプションのすべてを反映していません。しかし、提示された技術プラットフォームにおけるソリューションの一般的な動作メカニズムは示しています。

また、言うまでもないですが、この要求は他のメカニズム(たとえばコード生成)を使っても実装可能です。それについては他の記事で考察するかもしれません。

この実装ではカスタムフィールドとして String 型のみをサポートしていますが、このアプローチで構築した実際のアプリケーション( Enterra CRM )ではオブジェクト型だけでなく全てのプリミティブ型に関しても完全にサポートしています。

ユーザインターフェースでカスタムフィールドをサポートするために、ユーザインターフェース生成システムを使ったカスタムフィールド用のメタディスクリプタシステムが実装されています。しかしジェネレータのメカニズムは別の記事で取り上げるべきテーマです。

結論

Enterra CRM チームが生み出し、その良さを認めて実用化した結果として、ORM プラットフォームである Hibernate をベースとするオープンオブジェクトモデルのアーキテクチャは、ソースコードを変更する必要なしにエンドユーザの実際のニーズに合わせて実行時にアプリケーションの設定を参照するという顧客の要求を満足させることができました。

原文はこちらです:http://www.infoq.com/articles/hibernate-custom-fields

この記事に星をつける

おすすめ度
スタイル

BT