argius note

プログラミング関連

(Apache) Wicket+CayenneでWebアプリ

ApacheWicketCayenneの組み合わせでWebアプリを作ってみることにします。
導入した際のまとめです。


(2011-12-17)訂正および追記しました。(朱色の箇所。)


前置き

Webアプリケーションフレームワークは、既に自作フレームワークもどきで自分用のWebアプリを運用しているので、それの機能追加にフレームワークもどきでないものを使って迅速に機能追加をしたいという要求が発生しました。少し前まではLiftの採用を決めていましたが、学習しながらの開発ではこの要求に応えられません。
Scalaは魅力的な言語ですが、使いこなすにはまだ時間がかかりそうだし、何より今の環境では重すぎます。


他の動機として、利用経験のあるフレームワークは(保守的でお堅い)マイナーなプロプライエタリ製品ばかりで、OSSなフレームワークで自分の定番を持っておきたいという思いもありましたので、訓練と実益を兼ねて、OSSなフレームワークでの運用実績を作ってみようと思い立ちました。

WicketCayenneについて

Java標準に近いほうが導入(どこかで採用される時とか)に有利だという考えの下、Apacheプロジェクトで固める方針で選定しました。
XMLレスというのも重要。MavenのPOM.xmlとかWARのweb.xml以外では使いたくない。(Cayenneみたいにツールが全面的に面倒見てくれるなら問題ありません。)

Wicketは、SwingでGUIアプリを作るようにWebアプリが作れるという触れ込みで、ちょうどバージョン1.5が出てきたこともあって、やってみたらとっつきやすいので採用しました。
Cayenneは、persistence層は自動生成機能が便利なのが良いよね、ってところと、EntityBean的な操作感に親近感を覚えたので採用しました。

開発環境

小規模な開発向け。ここではまだ作りませんが、プライベートな練習として、スマートフォンでも使いやすいTaskListのようなアプリを作るのを想定します。
中規模以上になる場合やもっと今っぽい操作を求めるのであれば、DIコンテナJavaScriptライブラリ(jQueryとか)を組み合わせてみても良いかもしれません。


インターネットにつながらない環境も考慮して、メイン開発者だけMavenでライブラリ構成を構築し、それ以降はTomcatだけでもできるようにします。
作り終わったらSCMに登録し、ビルド環境でWARファイルを作り、アプリケーションサーバにデプロイします。


利用するソフトとバージョンはこんな↓感じで。

ソフト バージョン
Eclipse 3.7.1(Indigo)
Tomcat 5.5.34
TomcatPlugin 3.3.0 Eclipse Tomcatプラグイン
Maven 3.0.3
m2e 3.3.0 Eclipse Mavenプラグイン
Wicket 1.5.3
Cayenne 3.0.2

Wicketプロジェクト作成

Wicketのサイトの"Quickstart"ページに行きます。

"Creating the project - with Maven"と書かれている下に、フォームがあります。そこに必要な情報を入力すると、generateコマンドを生成してくれます。
TaskListアプリの名前は"potage"とします。

GroupId net.argius.potage
ArtifactId potage

コマンドはこうなります。

mvn archetype:generate -DarchetypeGroupId=org.apache.wicket -DarchetypeArtifactId=wicket-archetype-quickstart -DarchetypeVersion=1.5.3 -DgroupId=net.argius.potage -DartifactId=potage -DarchetypeRepository=https://repository.apache.org/ -DinteractiveMode=false

実行し、生成が完了したら、"cd potage"してから"mvn eclipse:eclipse"し、Eclipseの"Import">"Existing Projects into Workspace"でEclipseにインポートします。
"mvn compile"してからTomcatなどで起動してみると、"Congratulations!"ページが表示されます。(この時点ではjettyほうが簡単。)


m2eの"Maven POM Editor"で、"Dependencies"を追加します。
generateコマンドではwicket-coreだけがインストールされますが、"wicket-extensions","wicket-request","wicket-util"あたりは"wicket-core","wicket-request","wicket-util"がインストールされますが、"wicket-extensions"も追加で入れておくと楽かと思います。

追加した後で、"mvn compile"を実行すれば、インストールされます。Eclipseの"Referenced Library"に追加されていることを確認します。

Cayenneインストール

"Dependencies"にCayenneを追加します。

GroupId org.apache.cayenne
ArtifactId cayenne-server
Version 3.0.2

CayenneModelerプラグインの設定も追加します。これはPOMエディタでなくXMLエディタで"plugins"の箇所に追加します。

GroupId org.apache.cayenne.plugins
ArtifactId maven-cayenne-modeler-plugin
Version 3.0.2

念のため一旦"mvn compile"して、インストールを確認します。POMを変更したら、確認も兼ねて"mvn eclipse:eclipse"でEclipseへ反映しておきます。
modelerアプリを起動します。

$ mvn cayenne-modeler:run

GUIアプリが起動したら、最初に"Tools">"Preferences"で、動作確認のためのLocalデータソースを設定しておきます。ここではSQLiteを使いましょう。JDBCドライバが必要です。無ければJavaDB(Derby)でもOKです。
SQLiteJDBCを"Dependencies"に追加します。ここではorg.xerialのものを使いました。

GroupId org.xerial
ArtifactId sqlite-jdbc
Version 3.7.2

次に、プロジェクトの設定をします。

  • DataDomain: potage
    • DataNode: potageNode
      • DataMap: potageMap

大まかな説明:DataDomainは、ここでは特に考えません。DataNodeはデータソースのようなものです。DataMapはDBとオブジェクトのマッピングを管理します。Mapにはカスタムクエリ(たとえばSQLを直接書いたりするとか)を作成することもできます。
先にプロジェクトファイルを保存しておきましょう。potage/src/main/resources/cayenne.xmlに保存すればpotageアプリケーションから参照できます。
既存のデータベースからマッピングを作成する場合は、"Tools">"Reengineer Database Schema"を使えば簡単に作れます。DBに慣れている人はこちらで試してみると理解が早いかもしれません。定義からはじめる場合は、"Project">"Create DbEntity"でDBエンティティを定義します。

(どうせならObjEntityを作った後の状態を載せれば良かった。これではObjEntityとDbEntityの関係が分かりづらいですね。あと、ObjEntityの名前はTaskItemに変更したほうが分かりやすいかもしれませんね。)
potageで利用するDBエンティティとして、仮に"TaskList"を作り、データの登録は何らかのツールで行うとして、以下のような内容にします。

Seq Title Done
1 task1 1
2 task2 0
3 task3 0

"Tools">"Generate Database Schema"で、DBエンティティを実際にDB上に作成します。

最後に、potageMapを設定します。

Java Package net.argius.potage.dao
Custom Superclass PersistentBase

設定が終わったら、"Update..."ボタンで各Entityに反映させます。
"Custom Superclass"は必須ではありませんが、極力Plainなオブジェクトにするために"PersistentBase"クラスを実装しています。

package net.argius.potage.dao;

import java.io.*;

import org.apache.cayenne.*;

abstract class PersistentBase implements Persistent, Serializable {

    protected transient ObjectContext objectContext;

    private ObjectId objectId;
    private int persistenceState;

    @Override
    public ObjectId getObjectId() {
        return objectId;
    }

    @Override
    public void setObjectId(ObjectId objectId) {
        this.objectId = objectId;
    }

    @Override
    public ObjectContext getObjectContext() {
        return objectContext;
    }

    @Override
    public void setObjectContext(ObjectContext objectContext) {
        this.objectContext = objectContext;
    }

    @Override
    public int getPersistenceState() {
        return persistenceState;
    }

    @Override
    public void setPersistenceState(int persistenceState) {
        this.persistenceState = persistenceState;
    }

}

ここまで設定したら、"Tools">"Generate Classes"でEntityクラスを生成します。"Type=Advanced","superclass-package=net.argius.potage.dao","Template=Standard Client Superclass/Subclass"に設定して実行します。
あと、DataNodeの"JDBC Configuration"は、"Sync with Local"でLocalデータソースの設定を反映させておきます。
ここで一旦Modelerを閉じます。最後にプロジェクトファイルの保存を忘れないようにしてください。


作成したEntityクラスを動かしてみます。
sqliteのライブラリは予めPOMか直接Eclipseで追加しておきます。

package net.argius.potage;

import net.argius.potage.dao.*;

import org.apache.cayenne.*;
import org.apache.cayenne.access.*;
import org.apache.cayenne.query.*;

public final class Main {

    public static void main(String[] args) {
        ObjectContext ctx = DataContext.createDataContext();
        SelectQuery q = new SelectQuery(TaskList.class);
        for (final Object o : ctx.performQuery(q)) {
            TaskList item = (TaskList)o;
            System.out.printf("[%s] %s %s%n", item.getObjectId(),
                                              item.getTitle(),
                                              item.getDone());
        }
    }

}

Cayenneが大量にログメッセージを出力するので、抑制したい場合はpotage/src/main/resources/log4j.propertiesの"log4j.rootLogger=INFO,Stdout"のINFOをERRORにします。

[<ObjectId:TaskList, seq=1>] task1 1
[<ObjectId:TaskList, seq=2>] task2 0
[<ObjectId:TaskList, seq=3>] task3 0

これでCayenneの準備ができました。


WicketCayenneをつなげる

ここは正直、お作法が良く分かっていないところです。今分かっている範囲でやってみます。
無駄な設定変更を省くため、最初に生成されたファイルを改造して進めます。
HomePage.java(potage/src/main/java/net/argius/potage/HomePage.java)

package net.argius.potage;

import java.util.*;

import net.argius.potage.dao.*;

import org.apache.cayenne.*;
import org.apache.cayenne.access.*;
import org.apache.cayenne.query.*;
import org.apache.wicket.extensions.markup.html.repeater.util.*;
import org.apache.wicket.markup.html.*;
import org.apache.wicket.markup.html.basic.*;
import org.apache.wicket.markup.repeater.*;
import org.apache.wicket.markup.repeater.data.*;
import org.apache.wicket.model.*;
import org.apache.wicket.request.mapper.parameter.*;

public class HomePage extends WebPage {

    private static final long serialVersionUID = 1L;

    public HomePage(final PageParameters parameters) {
        add(new TaskListDataView("list", new TaskListDataProvider()));
    }

}

final class TaskListDataView extends DataView<TaskList> {

    protected TaskListDataView(String id, IDataProvider<TaskList> dataProvider) {
        super(id, dataProvider);
    }

    @Override
    protected void populateItem(Item<TaskList> item) {
        TaskList r = item.getModelObject();
        item.add(new Label("ID", String.valueOf(r.getObjectId())));
        item.add(new Label("TITLE", r.getTitle()));
        item.add(new Label("DONE", String.valueOf(r.getDone())));
    }

}

final class TaskListDataProvider extends SortableDataProvider<TaskList> {

    private List<TaskList> records;

    public TaskListDataProvider() {
        ObjectContext ctx = DataContext.createDataContext();
        SelectQuery q = new SelectQuery(TaskList.class);
        @SuppressWarnings("unchecked")
        List<TaskList> a = Collections.checkedList(ctx.performQuery(q), TaskList.class);
        this.records = a;
    }

    @Override
    public Iterator<? extends TaskList> iterator(int first, int count) {
        return records.iterator(); // ignore parameters
    }

    @Override
    public int size() {
        return records.size();
    }

    @Override
    public IModel<TaskList> model(TaskList o) {
        return new Model<TaskList>(o); // the type of its argument must be Serializable
    }

}

HomePage.html(potage/src/main/java/net/argius/potage/HomePage.html)

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">

<head>
<title>potage - task list app</title>
<meta http-equiv="Content-Type" content="text/html;utf-8">
</head>

<body>

<table>
  <tr wicket:id="list">
    <td><span wicket:id="ID">[ID]</span></td>
    <td><span wicket:id="TITLE">[TITLE]</span></td>
    <td><span wicket:id="DONE">[DONE]</span></td>
  </tr>
</table>

</body>
</html>

DataProviderのコンストラクタCayenneのデータアクセスを実行しています。ここは本格的に実装する前に、Factoryとかに外部化したほうが良いのでしょうね。

更新処理は今回は無しで。


これで、Wicket+CayenneでのWebアプリケーション開発の準備が整いました。
あとは、実際にpotageを開発してみて確かめてみたいと思います。