(Apache) Wicket+CayenneでWebアプリ
ApacheのWicketとCayenneの組み合わせでWebアプリを作ってみることにします。
導入した際のまとめです。
(2011-12-17)訂正および追記しました。(朱色の箇所。)
前置き
Webアプリケーションフレームワークは、既に自作フレームワークもどきで自分用のWebアプリを運用しているので、それの機能追加にフレームワークもどきでないものを使って迅速に機能追加をしたいという要求が発生しました。少し前まではLiftの採用を決めていましたが、学習しながらの開発ではこの要求に応えられません。
Scalaは魅力的な言語ですが、使いこなすにはまだ時間がかかりそうだし、何より今の環境では重すぎます。
他の動機として、利用経験のあるフレームワークは(保守的でお堅い)マイナーなプロプライエタリ製品ばかりで、OSSなフレームワークで自分の定番を持っておきたいという思いもありましたので、訓練と実益を兼ねて、OSSなフレームワークでの運用実績を作ってみようと思い立ちました。
WicketとCayenneについて
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
- DataNode: potageNode
大まかな説明: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の準備ができました。
WicketとCayenneをつなげる
ここは正直、お作法が良く分かっていないところです。今分かっている範囲でやってみます。
無駄な設定変更を省くため、最初に生成されたファイルを改造して進めます。
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を開発してみて確かめてみたいと思います。