jQuery treeView

http://bassistance.de/jquery-plugins/jquery-plugin-treeview
仕事で使う機会があったのでメモ書き。
jQueryプラグインで、JavaScriptによる開閉可能なツリー表示を行うライブラリです。開閉時にAjaxで動的に子階層を取得することも可能です。
基本的にはjQuery UI系のライブラリと同じで、対象ElementをID指定して追加されたメソッドを実行すればOKです。
jQuery treeViewの場合、ul、liタグで記述した階層一覧のルートのulタグを指定して

$("#example").treeview({});

を実行します。ドキュメントだと引数は無くてもいいとなってますが、Ajax機能を使うときにバグがあるみたいなので、空のオブジェクトリテラルを渡しておくといいです。これで階層一覧がツリー表示されます。
これだけなら特に大したことはないので、Ajaxでツリーを開いたときに子階層の値を取ってくるようにしたいと思います。今回は上司IDを外部キーに持つUsersテーブルを作成し、それをツリー表示してみます。
まずはテーブル作成

CREATE TABLE users (
	id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 'ID'
	,user_id varchar(255) NOT NULL UNIQUE COMMENT 'ユーザID'
	,user_name varchar(255) NOT NULL COMMENT 'ユーザ名'
	,password varchar(255) NOT NULL COMMENT 'パスワード'
	,parent_id INT UNSIGNED COMMENT '上司ID'
	,lock_version INT UNSIGNED NOT NULL COMMENT 'バージョン'
	,created_at DATETIME NOT NULL COMMENT '作成日時'
	,updated_at DATETIME NOT NULL COMMENT '更新日時'
	,FOREIGN KEY (parent_id) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='ユーザー';

parent_idに上司のIDをセットし、上下関係を定義しています。
次に、このテーブルに適当にデータを入れてみます。

INSERT INTO `users` (`id`, `user_id`, `user_name`, `password`, `parent_id`, `lock_version`, `created_at`, `updated_at`) VALUES
(1, 'keel', 'キール・ローレンツ', 'keel', NULL, 0, '2008-05-31 10:57:26', '2008-05-31 10:57:26'),
(2, 'gendou', '碇ゲンドウ', 'gendou', 1, 0, '2008-05-31 11:02:47', '2008-05-31 11:02:47'),
(3, 'fuyutsuki', '冬月コウゾウ', 'fuyutsuki', 2, 0, '2008-05-31 10:59:13', '2008-05-31 10:59:13'),
(4, 'misato', '葛城ミサト', 'misato', 2, 0, '2008-05-31 11:58:09', '2008-05-31 11:58:09'),
(5, 'ritsuko', '赤木リツコ', 'ritsuko', 2, 0, '2008-05-31 11:58:22', '2008-05-31 11:58:22'),
(6, 'maya', '伊吹マヤ', 'maya', 5, 0, '2008-05-31 11:58:32', '2008-05-31 11:58:32'),
(7, 'makoto', '日向マコト', 'makoto', 4, 0, '2008-05-31 11:02:22', '2008-05-31 11:02:22'),
(8, 'sigeru', '青葉シゲル', 'sigeru', 3, 0, '2008-05-31 11:04:30', '2008-05-31 11:04:30'),
(9, 'kaji', '加持リョウジ', 'kaji', 1, 0, '2008-05-31 11:05:45', '2008-05-31 11:05:45'),
(10, 'rei', '綾波レイ', 'rei', 5, 0, '2008-05-31 11:07:36', '2008-05-31 11:07:36'),
(11, 'asuka', '惣流・アスカ・ラングレー', 'asuka', 4, 0, '2008-05-31 11:08:29', '2008-05-31 11:08:29'),
(12, 'shinji', '碇シンジ', 'shinji', 4, 0, '2008-05-31 11:07:36', '2008-05-31 11:07:36'),
(13, 'touji', '鈴原トウジ', 'touji', 5, 0, '2008-05-31 11:10:30', '2008-05-31 11:10:30'),
(14, 'kaworu', '渚カヲル', 'kaworu', 1, 0, '2008-05-31 11:11:31', '2008-05-31 11:11:31');

データは適当ですので(汗)
次は、このデータをツリー表示するクライアント。まずはHTML

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="js/jquery-treeview/jquery.treeview.css" />
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/jquery-treeview/jquery.treeview.js"></script>
<script type="text/javascript" src="js/jquery-treeview/jquery.treeview.async.js"></script>
<script type="text/javascript" src="tree.js"></script>
<title>jQuery treeview サンプル</title>
</head>
<body>
<h1>jQuery treeview サンプル</h1>
<ul id="tree"></ul>
</body>
</html>

jqueryjquery.treeview、jquery.treeview.asyncの3つのJavaScriptファイルと、jquery.treeview.cssを設定します。bodyには今回の表示先となるulタグを記述しています。
次に初期化を行うJavaScript

$(function() {
	$("#tree").treeview({
		url: "resources/users"
	});
 });

treeviewメソッドの引数で、urlを指定すると、jQuery treeViewが自動的にAjaxでデータを取得してツリーを構成します。データはJSON形式になります。
クライアントはこれだけ。次はデータを提供するサーバ側を構築します。今回はJAX-RS+Spring+Hibernateの構成で行います。
まずはJPAのEntityクラス

package mapsample.entity;

import java.io.Serializable;
import java.sql.Timestamp;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Version;

@Entity
public class Users implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 2456410573270809795L;

	@Id
	private Integer id;

	private String user_id;

	private String password;

	private String user_name;

	@Version
	private Integer lock_version;

	private Timestamp created_at;

	private Timestamp updated_at;
	
	@ManyToOne(fetch = FetchType.LAZY)
	private Users parent;
	
	@OneToMany(mappedBy = "parent")
	private List<Users> childs;

(setter、getter省略)

}

同じUsersクラスに対する親子関係をそれぞれ@OneToMany、@ManyToOneで定義しています。
次は、このEntityを使ってデータを取得し、JSONに変換するJAX-RSのResourceクラスを作成します。

package mapsample.resource;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.ProduceMime;
import javax.ws.rs.QueryParam;

import mapsample.entity.Users;
import mapsample.util.HibernateUtil;

import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.hibernate.Criteria;
import org.hibernate.FetchMode;
import org.hibernate.Session;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.transaction.annotation.Transactional;

@Path("/users")
@Configurable
@Transactional
public class UsersResources {
	
	@PersistenceContext(unitName = "mapsample")
	private EntityManager em;
	
	@GET
	@ProduceMime("application/json")
	public JSONArray getUsersTree(@QueryParam("root") String root) throws JSONException {
		
		Session session = HibernateUtil.getSession(em);
		Criteria criteria = session.createCriteria(Users.class)
			.setFetchMode("childs", FetchMode.JOIN)
			.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
		boolean source = "source".equals(root);
		if (source) {
			criteria.add(Restrictions.isNull("parent"));
		} else {
			Integer id = Integer.parseInt(root);
			criteria.add(Restrictions.eq(
				"parent", em.getReference(Users.class, id)));
		}

		@SuppressWarnings("unchecked")
		List<Users> users = criteria.list();		
		JSONArray array = createJSONArray(source, users);				
		return array;
	}

	protected JSONArray createJSONArray(boolean source, List<Users> users)
			throws JSONException {
		JSONArray array = new JSONArray();
		for (Users user : users) {
			JSONObject json = createJSONObject(source, user);
			array.put(json);
		}
		return array;
	}

	protected JSONObject createJSONObject(boolean source, Users user)
			throws JSONException {
		JSONObject json = new JSONObject();
		json.put("id", user.getId());
		json.put("text", user.getUser_name());
		if (source) {
			JSONArray childs = createJSONArray(false, user.getChilds());
			json.put("children", childs);
		} else {
			json.put("hasChildren", user.getChilds().size() > 0);
		}
		json.put("expanded", source);
		return json;
	}

}

@PathによってJAX-RSに認識され、URLと紐づけられます。
@ConfigureによってSpringのDI機能が有効になります。
@TransactionalによってSpringのトランザクションAOPがメソッド毎に有効になります。
@PersistenceContextは、今回はSpringの標準的な設定方法に則って定義してみました。
getUsersTreeメソッドの引数は、送信元からのクエリパラメータになります。jQuery treeViewは、ルートの情報を取得するときは「root=source」を、表示された一覧から選択してその子階層を取得するときには「root=${id}」を送信してきます。Resourceクラス側で@QueryParam("root")と引数に定義することによって、このパラメータを取得できるようになります。
今回はパラメータによって取得内容が変わるので、HibernateのCriteriaを使ってみました。「root=source」の時は上司が存在しないUsers一覧を、それ以外は上司IDを条件として検索処理を実行します。
取得したEntityからJSONObjectを作成します。一覧表示する項目のJSONレイアウトは

{
text: "表示内容",
id: "子階層を取得するときに送信するID値",
children: "子階層データの配列",
hasChildren: "子階層データを持っていて、Ajaxで取得出来る場合true",
expanded: "デフォルトでツリーを開いた状態にしたい場合true"
}

こんな感じになります。今回は、ルート階層のみ直下の子階層をchildrenプロパティに渡し、ツリーを開いた状態で表示します。それ以外はツリーは閉じた状態にしておき、子階層が存在する場合はツリーが開かれたときにAjaxで取得します。
Springの定義は前回のAspectJで定義したのと同じです。今回Resourceクラスのパッケージ・クラス名を変更しているので、関連するところだけ修正しています。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/tx
		http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
		http://www.springframework.org/schema/jee
		http://www.springframework.org/schema/jee/spring-jee-2.5.xsd
		http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-2.5.xsd">
        
    <tx:jta-transaction-manager/>
    <tx:annotation-driven mode="aspectj"/>
    <jee:jndi-lookup id="mapsample"
		jndi-name="java:comp/env/persistence/mapsample" />

    <context:spring-configured/>
    <context:load-time-weaver weaver-class="org.springframework.instrument.classloading.glassfish.GlassFishLoadTimeWeaver"/>
    <context:annotation-config/>
    
    <context:component-scan base-package="mapsample.resource"
    	use-default-filters="false"
    	scope-resolver="mapsample.spring.context.PrototypeScopeMetadataResolver">
    	<context:include-filter type="annotation" expression="org.springframework.beans.factory.annotation.Configurable"/>
    </context:component-scan>

</beans>

一応JPAのpersistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
	xmlns="http://java.sun.com/xml/ns/persistence"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
	<persistence-unit name="mapsample" transaction-type="JTA">
		<provider>org.hibernate.ejb.HibernatePersistence</provider>
		<jta-data-source>jdbc/mapsample</jta-data-source>
		<properties>
			<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
			<property name="hibernate.transaction.manager_lookup_class" value="org.hibernate.transaction.SunONETransactionManagerLookup"/>
			<property name="hibernate.show_sql" value="false"/>
			<property name="hibernate.format_sql" value="true"/>
			<property name="hibernate.use_sql_comments" value="false"/>
			<property name="hibernate.ejb.use_class_enhancer" value="true"/>
		</properties>
	</persistence-unit>
</persistence>

最後にweb.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<listener>
		<listener-class>
			org.springframework.web.context.ContextLoaderListener
		</listener-class>
	</listener>

	<servlet>
		<servlet-name>Jersey Web Application</servlet-name>
		<servlet-class>
			com.sun.ws.rest.spi.container.servlet.ServletContainer
		</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>Jersey Web Application</servlet-name>
		<url-pattern>/resources/*</url-pattern>
	</servlet-mapping>

	<resource-ref>
		<res-ref-name>jdbc/mapsample</res-ref-name>
		<res-type>javax.sql.DataSource</res-type>
		<res-auth>Container</res-auth>
	</resource-ref>

	<persistence-unit-ref>
		<persistence-unit-ref-name>persistence/mapsample</persistence-unit-ref-name>
		<persistence-unit-name>mapsample</persistence-unit-name>
	</persistence-unit-ref>

</web-app>

これで実行

このとき発行されたSQL

    select
        this_.id as id4_1_,
        this_.created_at as created2_4_1_,
        this_.lock_version as lock3_4_1_,
        this_.parent_id as parent8_4_1_,
        this_.password as password4_1_,
        this_.updated_at as updated5_4_1_,
        this_.user_id as user6_4_1_,
        this_.user_name as user7_4_1_,
        childs2_.parent_id as parent8_3_,
        childs2_.id as id3_,
        childs2_.id as id4_0_,
        childs2_.created_at as created2_4_0_,
        childs2_.lock_version as lock3_4_0_,
        childs2_.parent_id as parent8_4_0_,
        childs2_.password as password4_0_,
        childs2_.updated_at as updated5_4_0_,
        childs2_.user_id as user6_4_0_,
        childs2_.user_name as user7_4_0_ 
    from
        Users this_ 
    left outer join
        Users childs2_ 
            on this_.id=childs2_.parent_id 
    where
        this_.parent_id is null

    select
        childs0_.parent_id as parent8_1_,
        childs0_.id as id1_,
        childs0_.id as id4_0_,
        childs0_.created_at as created2_4_0_,
        childs0_.lock_version as lock3_4_0_,
        childs0_.parent_id as parent8_4_0_,
        childs0_.password as password4_0_,
        childs0_.updated_at as updated5_4_0_,
        childs0_.user_id as user6_4_0_,
        childs0_.user_name as user7_4_0_ 
    from
        Users childs0_ 
    where
        childs0_.parent_id=?

    select
        childs0_.parent_id as parent8_1_,
        childs0_.id as id1_,
        childs0_.id as id4_0_,
        childs0_.created_at as created2_4_0_,
        childs0_.lock_version as lock3_4_0_,
        childs0_.parent_id as parent8_4_0_,
        childs0_.password as password4_0_,
        childs0_.updated_at as updated5_4_0_,
        childs0_.user_id as user6_4_0_,
        childs0_.user_name as user7_4_0_ 
    from
        Users childs0_ 
    where
        childs0_.parent_id=?

    select
        childs0_.parent_id as parent8_1_,
        childs0_.id as id1_,
        childs0_.id as id4_0_,
        childs0_.created_at as created2_4_0_,
        childs0_.lock_version as lock3_4_0_,
        childs0_.parent_id as parent8_4_0_,
        childs0_.password as password4_0_,
        childs0_.updated_at as updated5_4_0_,
        childs0_.user_id as user6_4_0_,
        childs0_.user_name as user7_4_0_ 
    from
        Users childs0_ 
    where
        childs0_.parent_id=?

ルートの子階層はJOINで取得し、その更に子階層はLAZYロードでSQLを発行しています。LAZYロードの部分はCOUNT使ってもいいのですが、めんどくさいので省略(汗)
ゲンドウのツリーを開くと

このとき発行されたSQL

    select
        this_.id as id4_1_,
        this_.created_at as created2_4_1_,
        this_.lock_version as lock3_4_1_,
        this_.parent_id as parent8_4_1_,
        this_.password as password4_1_,
        this_.updated_at as updated5_4_1_,
        this_.user_id as user6_4_1_,
        this_.user_name as user7_4_1_,
        childs2_.parent_id as parent8_3_,
        childs2_.id as id3_,
        childs2_.id as id4_0_,
        childs2_.created_at as created2_4_0_,
        childs2_.lock_version as lock3_4_0_,
        childs2_.parent_id as parent8_4_0_,
        childs2_.password as password4_0_,
        childs2_.updated_at as updated5_4_0_,
        childs2_.user_id as user6_4_0_,
        childs2_.user_name as user7_4_0_ 
    from
        Users this_ 
    left outer join
        Users childs2_ 
            on this_.id=childs2_.parent_id 
    where
        this_.parent_id=?

OKですね。Hibernateに限らず、O/RマッパーとJSON出力機能のあるフレームワークを使えば、簡単にこのjQuery treeViewを使うことが出来そうです。