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>
jquery、jquery.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>
<?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を使うことが出来そうです。