レベル: 中級 Noel Rappin (noelrappin@gmail.com), Senior Software Engineer, Motorola, Inc.
2007年 1月 23日 GWT (Google Web Toolkit) を使用した Ajax (Asynchronous JavaScript + XML) アプリケーションのビルド方法についての連載第 2 回目では、Web アプリケーションのための Apache Derby データベースをビルドして GWT の制御に使用する方法について学びます。第 1 回の記事では GWT の概要と、GWT を使用して Web アプリケーションのリッチ・クライアント・フロントエンドを作成する方法を紹介しました。今回は舞台裏に回り、データベース、そして GWT が使用できる形式にデータを変換するためのコードでバックエンドをセットアップします。この記事を読み終える頃には、フロントエンドとバックエンドが対話する準備が完了しているはずです。
今回の記事では、データベース (Web アプリケーションのバックエンド) をインストールして構成する手順、そしてデータベース・スキーマの作成手順を説明し、最後にデータベースにデータを入力するための単純なツールを紹介します。使用するデータベースは Apache Derby です。この 100% 純 Java™ リレーショナル・データベースは当初、Cloudscape™ という名前で開発されましたが、その後、Cloudscape のコードを取得した IBM® がオープン・ソース・バージョンを Apache プロジェクトに寄贈しました。同プロジェクトは、JavaDB という名前で Sun Microsystems によっても配布されていますが、まったくややこしい話ではありません。
 | |
Ajax Resource Center にアクセスしてください。ここには記事、チュートリアル、ディスカッション・フォーラム、ブログ、ウィキ、イベント、そしてニュースなど、Ajax プログラミング・モデルに関する情報が豊富に用意されており、ワンストップ・ショップになっています。新しい情報もここに記載されます。 |
|
私が Derby を選んだ理由は、この 3 つのネーム・バリューだけではなく、このデータベースが軽量で構成しやすいからです。大抵のリレーショナル・データベースとは違い、Derby は Java サイドのサーバー・コードと同じ Java 仮想マシン (JVM) 内で実行することができます (お望みとあれば、別の JVM 内でも実行可能です)。そのため開発とデプロイメントがより簡単で、しかも Derby は小中規模の Web アプリケーションには十分なほどの処理速度を持ちます。
手順を始める前に、いくつか注意事項があります。まず、この記事の手順に従うにはリレーショナル・データベース、JDBC、そして構造化照会言語 (SQL) についての基礎知識が必要です。次に、この記事に記載しているコードの一部は解説を目的にしているため、実動システムに適切とは言えません。該当するコードに関してはその都度指摘するよう務めますが、ここではパフォーマンスの調整については説明しません。
Derby を入手する
Derby は Apache DB プロジェクトの一部として入手できます。この記事を書いている時点での最新リリースは、バージョン 10.1.3.1 です。Eclipse 統合開発環境 (IDE) で作業するのであれば、derby_core_plugin と derby_ui_plugin という 2 つのプラグインを手に入れるだけで十分です。そうでない場合は、必要に応じて他の配布を入手してください。配布には、ライブラリー・ファイルだけのもの、ライブラリーとドキュメンテーションを併せたもの、ライブラリーにデバッグ情報が付随するもの、そしてソース・コードだけのものがあります。Derby は Java 技術のみをベースとしており、バージョン 1.3 以降のどの JVM でも動作します。ここに記載するコードの例は、Java 1.4 を前提としています。
Eclipse がない場合の Derby セットアップ手順
Eclipse を使用しない場合は、ダウンロードした配布を適当な場所に解凍してください。次に、lib/derby.jar および lib/derbytools.jar というファイルが classpath 変数に含まれるようにしてください。この作業はシステム・レベルで行えますが、その場合、環境変数 DERBY_INSTALL を Derby が常駐するディレクトリーに設定すると便利かもしれません (/opt/bin/db-derby-10.1.3.1-bin というように、Derby ディレクトリー自体を含めてください)。また、IDEやランチャー・スクリプト内で行うこともできます。Derby をクライアント/サーバー・モードと組み込みモードの両方で使用したい場合は、lib/derbyclient.jar ファイルと lib/derbynet.jar ファイルも classpath に含める必要があります。
Eclipse を使用している場合の Derby セットアップ手順
Eclipse を使用している場合は、開発用のセットアップが多少簡単になります。以下の手順に従って、Eclipse で Derby をセットアップしてください。
- 2 つのプラグイン・ファイルを解凍します。それぞれのトップ・ディレクトリーには plugin という名前がついています。
- 上記のディレクトリーの内容を Eclipse プラグイン・ディレクトリーにコピーします。
- Eclipse でプロジェクトを開きます。
-
Project > Add Apache Derby Nature の順にクリックして、Derby の素晴らしい世界を開きます。これにより、4 つのライブラリー・ファイルがプロジェクト classpath に追加され、ij コマンド・ライン・プロンプトにアクセスできるようになります。
図 1 は、Derby Nature が追加された後の Derby メニューです。
図 1. Eclipse の Derby メニュー
Eclipse を開発に使用するとしても、アプリケーションをデプロイする際に適切な JAR ファイルを使用できるようにしておかなければなりません。これについては、今後の記事で詳しく説明します。
スキーマを設計する
データベースを使い始める前に、少々時間を割いてデータベースに保管する内容を明確にしてください。Slicr アプリケーションの要件についてはまだ説明していないので、まず、このデータベースには基本的なカスタマー情報と注文情報を保管するという前提にします。
このような初期段階の製品でデータベースを操作する秘訣は、データベースを単純にして、最初に Java コードで追加処理を行うことになるとしてもデータベース・システム固有の機能はなるべく使わないようにすることです。データベースはサード・パーティーに大きく依存したものとなるので、データベースの決定によってアプリケーションの残りの部分が左右されてしまうという事態は避けなければなりません。プログラムとデータベースの接点は最小限にして、どこかの時点でシステムを変更するにしても、その変更を実際に実現できるようにします。データベースのパフォーマンスを改善するために何かを行うと、大抵の場合は特定のシステムに縛られることになってしまいます。そのため、そのような最適化を行うのはプロジェクトのできるだけ最後の段階まで待つようにしてください。
データベース設計は始まりの時点では単純なものです。まず、カスタマーが注文します。注文の内容は 1 つ以上のピザです (これ以外の食べ物もこのレストランで売られている可能性は、とりあえず無視してください)。ピザのトッピングはない場合もあれば、複数ある場合もあり、ピザの半分に載せられる場合と全体に載せられる場合があります。
Customer テーブルの作成
現時点で注意することは、注文の配達と確認ができるだけの十分なカスタマー情報があるかどうかだけです。リスト 1 を見てください。
リスト 1. Customer テーブル
CREATE TABLE customers (
id int generated always as identity constraint cust_pk primary key,
first_name varchar(255),
last_name varchar(255),
phone varchar(15),
address_1 varchar(200),
address_2 varchar(200),
city varchar(100),
state varchar(2),
zip varchar(10)
)
|
CREATE ステートメントには、多少標準とは異なる SQL 構文があります。ここで作成している ID 列は、新しい行ごとに Derby によってオート・インクリメントされます。以下の文節が、この振る舞いを指定します。
id int generated always as identity
ID 列のもう 1 つのオプションとしては、以下があります。
generate by default as identity
上記の違いは、generate by default では常に独自の値を列に配置できる一方、generate alwaysではそれができないという点です。また、ID 列はテーブルの主キーとしても識別されています。
データベースには、現実に存在している値とはまったく関係のない ID を含める必要があります。そのうちチームの誰かが、電話番号もカスタマーを一意に識別するため、こういったものをキーとして使用できるはずだと納得させようとするかもしれませんが、それは禁物です。誰かが引っ越して電話番号を変更したがためにデータベース全体の更新が必要となるのは、最も避けなければならないことだからです。
Order テーブルの作成
Order テーブル (リスト 2 を参照) では、注文をカスタマーと日付と結びつけ、ディスカウントできるようにします。残高はコードで計算できます。
リスト 2. Order テーブル
(
id int generated always as identity constraint ord_pk primary key,
customer_id int constraint cust_foreign_key references customers,
order_time timestamp,
discount float
)
|
上記では id 主キーの他、customer_id 列を Customer テーブルを参照する外部キーとして宣言しています (宣言に外部列を組み込まない場合、Derby は他のテーブルの主キーを参照していることを前提とします)。これはつまり、Derby がこのテーブルに追加された customer_id が実際にシステム内のカスタマーに一致するかどうかを検証することを意味します。データベース管理者は必ずこのようにしなければならないと指示するでしょうが、データベースが常に厳密な検証を行うのを避けるほうが正当な場合もあると思います。例えば、外部値が何かわからないまま、あるいは検証できるようになる前にデータを入力しなければならないことがあります。また、外部行を削除する一方で、テーブルの行は維持したい場合もあります。このような場合、例えばカスタマーは削除するけれども、カスタマーの注文はデータを収集する目的で維持することも考えられます。Derby でこのようにすることは可能ですが、別のデータベース・システムには移植できなくなる可能性があります。
Toppings テーブルの作成
データベース設計で最後に残るのはピザとトッピングの問題ですが、トッピングは問題になりません。リスト 3 を見るとわかるように、このテーブルはかなり単純です。
リスト 3. Toppings テーブル
CREATE TABLE toppings(
id int generated always as identity constraint top_pk primary key,
name varchar(100),
price float
)
|
問題は、ピザとトッピングの関係をどのように管理するかです。1 枚のピザは、注文、サイズ、そしてトッピングのセットから成っています。従来のデータベース標準化に従えば、Pizza テーブルを作成した上で、ピザ ID をトッピング ID に関連付ける多対多のテーブルを作成することになります。この方法には多くの利点があり、なかでもとりわけ便利なのは、ピザに載せられるトッピングの数が無限になるということです。ただし、テーブル間のデータベースの関係を管理するには、パフォーマンス上の犠牲が伴います。無限のトッピングが必要でなければ、Pizza テーブルに複数のトッピング・フィールド (topping_1、topping_2 など) を含めるという方法があります。概念的には少々単純な方法ですが、例えば注文データを調べて最も人気の高いトッピングを判断するのは非常に厄介です。冒険心にとりわけ溢れているのであれば、単一のトッピング・フィールドにビットマップや連結ストリングを入力するという手もありますが、あまりお勧めはできません。
Pizza テーブルの作成
しばらく考えた末、私が決定したのはまったく一般的なフォームにすることです。十分な種類のトッピングをピザに載せられるようにトッピングすべてを同じテーブルに含めると、テーブルが見苦しくなってしまいます。そこで、リスト 4 のコードを使用します。
リスト 4. Pizza テーブル
CREATE TABLE pizzas (
id int generated always as identity constraint piz_pk primary key,
order_id int constraint order_foreign_key references orders,
size int
)
CREATE TABLE pizza_topping_map (
id int generated always as identity constraint ptmap_pk primary key,
pizza_id int constraint pizza_fk references pizzas,
topping_id int constraint topping_fk references toppings,
placement int
)
|
念のため説明しておきますが、サイズは 1、2、3、4 とあり、それぞれ小、中、大、特大を表します。トッピングの配置は -1、0、1 で、それぞれピザの左半分、全体、右半分となります。もちろん、マッピングごとに異なる ID も必要です。例えばペパロニを追加注文できるように、同じピザでペパロニを 2 回、トッピングとして表示するためです。
注: まだ言っていなかったかもしれませんが、constraint の後に続ける名前は、それぞれがデータベース全体で固有のものでなければなりません。実際には Derby が舞台裏で索引を作成するのですが、その索引にはそれぞれ固有の名前がなければならないためです。
データベース・スキーマとしてはこれで十分です。今度はこれをデータベースに組み込みます。
データベースにデータを入力する
スキーマが出来上がったので、今度はこれをセットアップして初期データを所定の位置に配置します。ここでは、セットアップを実行する短いスタンドアロンのプログラムを作成しますが、方法はそれだけではありません。Derby の ij コマンド・ラインで SQL コマンドを直接入力するという方法、あるいはグラフィカル SQL ツールを使用するという方法もあります。しかし、プログラムによる方法は、Derby を起動する方法や、Derby とその他の JDBC データベースの違いについて理解するためには、整然としていてわかりやすい構成になっています。また、実際には SQL スキーマをその独自の SQL スクリプトに維持することになるはずです。
まず、極めて静的なデータから取り掛かります。つまり、第 1 回の記事で Slicr ページに組み込んだピザのトッピング・リストです。ここでも、主に静的なデータを挿入するという理由からプログラマチックなやり方が功を奏します。セットアップする Toppings テーブルには、それぞれに名前と基本料金を持つトッピングが含まれます。このデータをセットアップするのが、リスト 5 に示すコードです。とりあえず、すべてのトッピングの料金は同じだということにしてください。
リスト 5. Derby での Toppings テーブルのセットアップ
public class SlicrPopulatr {
public static final String[] TOPPINGS = new String[] {
"Anchovy", "Gardineria", "Garlic",
"Green Pepper", "Mushrooms", "Olives",
"Onions", "Pepperoni", "Pineapple",
"Sausage", "Spinach"
}
public void populateDatabase() throws Exception {
Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
Connection con = DriverManager.getConnection(
"jdbc:derby:slicr;create=true");
con.setAutoCommit(false);
Statement s = con.createStatement();
s.execute("DROP TABLE toppings");
s.execute("CREATE TABLE toppings(" +
"id int generated always as identity constraint top_pk primary key, " +
"name varchar(100), " +
"price float)");
//
// All the other create table statements from above would go here...
//
for (int i = 0; i < TOPPINGS.length; i++) {
s.execute("insert into toppings values (DEFAULT, '" +
TOPPINGS[i] + "', 1.25)");
}
con.commit();
con.close();
try {
DriverManager.getConnection("jdbc:derby:;shutdown=true");
} catch (SQLException ignore) {}
}
public static void main(String[] args) throws Exception {
(new SlicrPopulatr()).populateDatabase();
}
|
JDBC に精通していれば、このコードのほとんどはおなじみだと思いますが、説明が必要な Derby 特有の機能もいくつかあります。それは、まず始めに、Class.forName イディオムを使ってドライバー・クラスをロードするという点です。組み込みバージョンの Derby を使用することになるため、ドライバーのクラス名は org.apache.derby.jdbc.EmbeddedDriver となります。次に、接続ストリングを作成します。Derby URL のフォームは以下のとおりです。
jdbc:derby:database name;[attr=value]
データベース名は、データベースを参照するために使用する名前です。サーバー・コードでデータベースを再びオープンするときに一貫している限り、どの名前を選んでも構いません。
接続を作成した後は、標準 JDBC の領域です。テーブルのドロップと再作成を行うコマンドを実行するための Statement を作成します。データベースが壊れた場合、このプログラムからデータベースをリセットできるようにするためです (そうでないと、既存のテーブルを作成しようとすると Derby が例外をスローします)。テーブルを作成したら、トッピング配列に含まれるエントリーごとに insert ステートメントを使用します。
insert ステートメントに含まれる SQL コードの特徴は、思いがけなかいものだったかもしれません。ここでは、キーワード DEFAULT が ID 列のプレースホルダーとして使用されています。insert ステートメントに列のリストが指定されていないと、Derby は ID 列のスロットにこのキーワードがあると期待するためです。
プログラムが存在する前は、特殊な呼び出し "jdbc:derby:;shutdown=true" で URL に接続します。この場合、データベースを指定する必要はありません。この呼び出しは Derby システムに対し、シャットダウンしてアクティブなすべての接続を解放するように指示します。
このちょっとしたプログラムを実行すると、アプリケーションの最上位ディレクトリー内に derbyDb というディレクトリーが表示されます。Derby は、このディレクトリーに保管されたバイナリー・ファイルにデータを保管します。そのため、これらのファイルは多少なりとも変更してはなりません。
GWT のデータを用意する
データベース・スキーマを配備して静的データをロードしたら、次にそのデータをクライアントとの間で通信する方法を指定しなければなりません。最終的には、クライアント・サーバー接続でデータを直列化することになります。直列化が機能するためには、最終的なデータ・クラスを GWT が見つけて操作できるようにする必要があります。つまり、クラスを client パッケージの中で定義して、GWT の Java から JavaScript へのコンパイラーでコンパイルできるようにしなければならないということです。
直列化対象のクライアント・クラスには、制約事項が追加されます。その 1 つは、クライアント・クラスは com.google.gwt.user.client.rpc.IsSerializable インターフェースを実装しなければならないということです。これはマーカー・インターフェースで、メソッドを定義しません。さらに、クラス内のすべてのデータ・フィールド自体が直列化可能でなければなりません (通常の Java の直列化の場合と同じく、フィールドに transient のマークを付けると、フィールドが直列化されなくても済むようにできます)。
では、どのような場合に直列化可能なフィールドになるのでしょう。まず、直列化可能なフィールドは、そのフィールドが属しているクラスまたはスーパークラスが IsSerializable を実装している場合があります。あるいはフィールドの型が、Java プリミティブ、あらゆるプリミティブ・ラッパー・クラス、Date、String などの基本型である場合もあります。また、直列化可能な型の配列あるいはコレクションも、直列化できるフィールドです。ただし Collection または List を直列化する場合、GWT にとって望ましいのは、実際の型を指定する Javadoc コメントで注釈を付け、コンパイラーがフィールドを最適化できるようにすることです。リスト 6 に、フィールドとメソッドのサンプルを示します。
リスト 6. 直列化可能なフィールドおよびメソッド
/**
* @gwt.typeArgs <java.lang.Integer>
*/
private List aList;
/**
* @gwt.typeArgs <java.lang.Double>
* @gwt.typeArgs argument <java.lang.String>
*/
public List doSomethingThatReturnsAList(List argument) {
// Stuff goes here
}
|
注: メソッド・リストに含まれる引数はコメント内の名前で指定しなければなりませんが、戻り値の場合は違います。
直列化可能オブジェクトのリストには、java.sql と JDBC に関係するものが一切ないことに注目してください。結果セットからデータ・オブジェクトにアクセスするために何を行うにしても、サーバー・サイドのコードで行わなければなりません。
この時点で、ORM (Object-Relational Mapping) の世界に足を踏み入れることになります。つまり、データをリレーショナル・データベースの構造から Java プログラムのオブジェクト指向の構造に移行させるということです。複合 Java 実動システムの場合はおそらく、Hibernate や Castor などの既存の本格的 ORM システムを使うことになります。この 2 つのシステムはいずれも、データベースのデータをお好みの Java オブジェクトに自動的にロードしますが、使用する前に大々的な構成が必要です。この記事での話題の中心は Derby と GWT なので、開発の初期段階で役立つ簡単なコンバーターを紹介します。いずれは、もっと強力なツールと取り替えられます。
単純な ORM コンバーター
まず始めに、データベース・テーブルすべてを対象とした Bean クラスを作成してください。ここでは例として、Topping クラスを使います。このクラスは単純で、すでにデータを持っているためです。テーブル内の各列には通常の Bean 命名規則を使用しますが、下線は大/小文字混合に変換します (例えば、topping_id は getToppingId となります)。リスト 7 に、Topping クラスを示します。
リスト 7. Topping クラス
package com.ibm.examples.client;
import com.google.gwt.user.client.rpc.IsSerializable;
public class Topping implements IsSerializable {
private Integer id;
private String name;
private Double price;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}
|
リスト 8. 単純な ORM ツール
package com.ibm.examples.server;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class ObjectFactory {
public static String convertPropertyName(String name) {
String lowerName = name.toLowerCase();
String[] pieces = lowerName.split("_");
if (pieces.length == 1) {
return lowerName;
}
StringBuffer result = new StringBuffer(pieces[0]);
for (int i = 1; i < pieces.length; i++) {
result.append(Character.toUpperCase(pieces[i].charAt(0)));
result.append(pieces[i].substring(1));
}
return result.toString();
}
public static List convertToObjects(ResultSet rs, Class cl) {
List result = new ArrayList();
try {
int colCount = rs.getMetaData().getColumnCount();
while (rs.next()) {
Object item = cl.newInstance();
for (int i = 1; i <= colCount; i += 1 ) {
String colName = rs.getMetaData().getColumnName(i);
String propertyName = convertPropertyName(colName);
Object value = rs.getObject(i);
PropertyDescriptor pd = new PropertyDescriptor(propertyName, cl);
Method mt = pd.getWriteMethod();
mt.invoke(item, new Object[] {value});
}
result.add(item);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return result;
}
}
|
convertToObjects() メソッドはただ単に結果セットをループし、JavaBean リフレクションを使用してプロパティー・ゲッターを推測してすべての値を設定します。convertPropertyName() メソッドが、SQL 下線付き命名規則と Java 大/小文字混合規則を切り替えます。
ORM ツールにない機能
 |
What the ORM tool doesn't do
このツールに欠けている便利な ORM 機能をすべて挙げるとなったら、1 冊の本が書けるほどです。例えば、このツールには以下の機能がありません。
- 同じオブジェクトの複数のバージョンを作成しないようにする機能
- データベースに書き戻せるようにする機能
- 高速処理する機能
|
|
上記のコードの成果は予想以上のものではありませんか? さらに構成しなくても、このコードはあらゆるデータベース・ツールでそのまま実行できます。スキーマが後で変更される可能性のある初期開発段階では、マッピング・ファイルをデータベースと同期させておく必要はありません。また、時機が来たときに一層強力なツールと取り替えるのも難しいことではありません。
リスト 9 に示すのは、先ほど作成したすべての Topping インスタンスをツールが実際に読み込んでいる様子です。
リスト 9. ORM ツールのテスト
public class ToppingTestr {
public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";
public static final String PROTOCOL = "jdbc:derby:slicr;";
public static void main(String[] args) throws Exception {
try {
Class.forName(DRIVER).newInstance();
Connection con = DriverManager.getConnection(PROTOCOL);
Statement s = con.createStatement();
ResultSet rs = s.executeQuery("SELECT * FROM toppings");
List result = ObjectFactory.convertToObjects(rs, Topping.class);
for (Iterator itr = result.iterator(); itr.hasNext();) {
Topping t = (Topping) itr.next();
System.out.println("Topping " + t.getId() + ": " +
t.getName() + " is $" + t.getPrice());
}
} finally {
try {
DriverManager.getConnection("jdbc:derby:;shutdown=true");
} catch (SQLException ignore) {}
}
}
}
|
上記のテスト・プログラムは、Slicr データベースへの Derby 接続を作成します (もはや、必要に応じてデータベースを作成するようにプロトコル・ストリングに要求することはしていません)。このテストでは、単純な SQL クエリーを実行してその結果をファクトリーに渡した後、自由に結果のリストでループしてデータベースを終了しています。
次回の予告
これで、データベースのインストールと構成は完了です。データベース・スキーマを作成し、データベースにデータを入力するための単純なツールについても理解できたはずです。この連載のこれまでの 2 回の記事で、Slicr プロジェクトには単純ながらも機能的なフロント・エンドとバック・エンドが用意されました。次のステップは通信です。連載第 3 回目では、GWT で使用するフレームワークについて説明します。このフレームワークを使用してリモート・プロシージャー呼び出し (RPC) のコードを簡単に作成して管理する方法を学んでください。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | |  | ジョージア工科大学 Graphics, Visualization, and Usability Center 出身の博士、Noel Rappin は、Motorola 社のシニア・ソフトウェア・エンジニアです。「wxPython in Action」および「Jython Essentials」の共著者でもあります。 |
記事の評価
|