例えば皆さんが JavaScript を使って、非常に洗練された Web 2.0 アプリケーションのプロトタイプを短時間で作成したいものの、データベースに接続するためのコードの作成に要する時間をなるべく少なくしたいとします。従来の方法では、データベースからデータを取得してフロントエンドで利用するためには、データベースの各テーブル用にクラスを作成し、各クラスにはすべての CRUD (Create、Read、Update、Delete) メソッドを用意する必要があります。さらにそれらのコードを呼び出すマーシャリング・コードを作成することで、フロントエンドへのアクセス・レイヤーを提供する必要があります。さらにまたそのレイヤーの上には、バックエンドにアクセスするための JavaScript ライブラリーを配する必要があります。何て面倒なのでしょう!
この記事では従来の方法に代わる方法として、1 つのデータベース・クラスを使用して複数のデータベース・テーブルをラップする方法について説明します。この方法では、1 つのドライバー・スクリプトによってフロントエンドをバックエンドに接続し、フロントエンドをラップするクラスによって、必要なすべてのテーブルにアクセスします。図 1 は、この方法のスタック全体を示しています。
図 1. JavaScript によるデータ・アクセス・レイヤーのアーキテクチャー
このサンプル・アプリケーションでは、新しいエントリーを追加することのできるリスト・ページがユーザー・インターフェースのベースにあります。このリスト・ページは JavaScript クラスを使用して、バックエンドを介してデータベースにアクセスし、さらにこの JavaScript クラスが jQuery を使って Ajax リクエストを実行します。
Ajax リクエストは PHP によるドライバー・ページによって処理されます。そのドライバー・ページでは、リクエストが有効であることを確認した後、そのリクエストをデータベース・ラッパー・クラスに渡します。データベース・ラッパー・クラスでは、リクエストを処理して何らかの結果を返し、その結果がドライバーによって JSON フォーマットに変換されてブラウザーに返送されます。
この方法には限界があります。第 1 に、各リクエストはテーブルごとに実行されるため、関連する複数のテーブルにまたがる作業には適していません。この点を改善するには、関連する複数のテーブルにまたがって複数のリクエストを実行するための新しいカスタム・メソッドをいくつか追加します。第 2 に、ロールやユーザーをチェックする仕組みがないため、データへのアクセスが適切なアクセスであることが保証されないという、データ・セキュリティーの問題があります。この点も、ユーザーやロールを扱うためのコードを多少追加することで、改善することができます。
こうして改善されたコードによって、高速で容易かつセキュアにデータベースのデータを取得してユーザー・インターフェースに表示する方法が実現され、アプリケーションのプロトタイプを短時間で作成できるようになります。またこのコードは、データベース・アクセス、PHP、Ajax、jQuery、JavaScript 等のスキルを習得するためのツールとしても有効です。
すべてはデータベースから始まります。この例では、本の著者とその著者に関連する本のデータベースを使用します。リスト 1 に、MySQL データベースの初期化コードを示します。
リスト 1. books.sql
DROP TABLE IF EXISTS authors; CREATE TABLE authors ( id BIGINT NOT NULL PRIMARY KEY auto_increment, name VARCHAR(255) NOT NULL ); DROP TABLE IF EXISTS books; CREATE TABLE books ( id BIGINT NOT NULL PRIMARY KEY auto_increment, author_id INT NOT NULL, name VARCHAR(255) NOT NULL ); INSERT INTO authors VALUES ( 0, 'Jack Herrington' ); INSERT INTO authors VALUES ( 0, 'Dave Smith' ); INSERT INTO authors VALUES ( 0, 'Harold Rollins' ); INSERT INTO books VALUES ( 0, 1, 'Code Generation In Action' ); INSERT INTO books VALUES ( 0, 1, 'Podcasting Hacks' ); INSERT INTO books VALUES ( 0, 2, 'Welcome To The Universe' ); |
このコードをデータベースに対して実行するには、最初に以下のようにしてデータベースを作成する必要があります。
% mysqladmin create books |
次に、以下のようにしてテーブルとサンプル・データをデータベースに追加します。
% mysql books < books.sql |
もちろん、データベースのユーザー名とパスワードを指定する必要がある場合には、上記のコマンドとは少し異なる可能性があります。
データベースにアクセスするためには、まず DatabaseWrapper クラスを PHP で作成します。このクラスは、コンストラクターの引数としてデータベースの接続ハンドルとテーブル名を取り、また以下の 5 つのメソッドを持っています。
- get: 整数の ID を指定されると、get メソッドはテーブルから 1 つの行を返します。このメソッドはテーブルの
idフィールドには一意の整数値があることを前提としています。 - getAll: このメソッドはデータベースのすべてのレコードを返します。このメソッドにはいくつかの引数オプションがあります。ここでは、返されるレコード数を制限する limit
オプションのコードのみが実装されています。必要に応じて他のオプションを追加することもできます。例えば
orderオプションを指定すると、指定されたフィールドを基準に並び替えられたレコードが返されます。 - delete: delete メソッドは整数の ID を引数に取り、その行をテーブルから削除します。
- insert: insert メソッドは新しい行をテーブルに追加します。
- update: update メソッドは、更新する行を指定するための整数の ID と、指定した値で更新を行うフィールドのハッシュ・テーブルを引数として取ります。
この 5 つの関数により、すべてのクライアントに対する完全な CRUD インターフェースが提供されます。ただし、これらの関数は制限付きであり、1 つのテーブルに対してのみ機能し、リレーションシップを利用して複数のテーブルを処理することはできません。
リスト 2 に、このデータベース・ラッパー・クラスのコードを示します。
リスト 2. DBWrapper.php
<?php
class DatabaseWrapper {
private $dbh;
public function __construct( $dbh, $table ) {
$this->dbh = $dbh;
$this->table = $table;
}
public function get( $id ) {
$sql = 'SELECT * FROM '.$this->table.' WHERE id = :id';
$sth = $this->dbh->prepare($sql);
$sth->execute(array(':id' => $id));
return $sth->fetchObject();
}
public function getAll( $options = array() ) {
$sql = 'SELECT * FROM '.$this->table;
if ( isset( $options->{'limit'} ) ) {
$sql .= ' LIMIT '.$this->dbh->quote( $options->{'limit'}, PDO::PARAM_INT );
}
$sth = $this->dbh->prepare($sql);
$sth->execute();
return $sth->fetchAll( PDO::FETCH_CLASS );
}
public function delete( $id ) {
$sql = 'DELETE FROM '.$this->table.' WHERE id = :id';
$sth = $this->dbh->prepare($sql);
$sth->execute(array(':id' => $id));
}
public function update( $id, $values ) {
$binds = array( ':id' => $id );
$bindnames = array();
foreach(array_keys($values) as $k) {
$k = $this->clean( $k );
$binds[ ":$k" ] = $values[ $k ];
$bindnames []= "$k=:$k";
}
$bindnames = join( $bindnames, ',' );
$sql = 'UPDATE '.$this->table." SET $bindnames WHERE id=:id";
$sth = $this->dbh->prepare($sql);
$sth->execute( $binds );
}
public function insert( $values ) {
$keys = array();
$binds = array();
$bindnames = array();
foreach(array_keys($values) as $k) {
$k = $this->clean( $k );
$keys []= $k;
$binds[ ":$k" ] = $values[ $k ];
$bindnames []= ":$k";
}
$keys = join( $keys, ',' );
$bindnames = join( $bindnames, ',' );
$sql = 'INSERT INTO '.$this->table." ( $keys ) VALUES ( $bindnames )";
$sth = $this->dbh->prepare($sql);
$sth->execute( $binds );
return $this->dbh->lastInsertId();
}
private function clean( $k ) {
return preg_replace( '[^A-Za-z0-9_]', '', $k );
}
}
?>
|
get と getAll のコードは最も単純です。これらのコードは PHP の PDO ライブラリーと接続ハンドルを使用し、データベースへの SELECT リクエストを実行します。delete メソッドも簡単な DELETE リクエストを実行するだけなので、非常に簡単です。
1 つ重要な注意点として、コロンで区切った置換演算子を SQL 文に使用しています。これらの置換演算子はデータベースへのアクセスをセキュアなアクセスのみにする上で重要です。置換演算子があるおかげで、悪意のあるスクリプトがクエリーに SQL を追加して既存のコードを変更することはできず、機密データへのアクセスを防ぐことができます。そうした悪意のあるスクリプトによる攻撃は「SQL インジェクション」と呼ばれ、極めてよくある攻撃の形です。
insert メソッドと update メソッドはやや複雑ですが、それは入力されたデータを追加するためのフィールドが含まれる SQL 文を作成する必要があるからです。フィールド名のチェックには clean メソッドを使っています。フィールド名として許可されない文字が含まれていると、clean メソッドはそれらの文字を返します。このメソッドにより、SQL インジェクション攻撃を受ける可能性がなくなります。
ブラウザーにデータを表示するためには、Web ページからデータにアクセスできるようにする必要があります。そのために driver.php というページを作成します。この Web ページのコードがリスト 3 です。
リスト 3. driver.php
<?php
include 'dbwrapper.php';
header( 'Content-Type', 'application/json' );
try {
$table = null;
if ( $_GET['table'] == 'books' || $_GET['table'] == 'authors' )
$table = $_GET['table'];
else
throw new Exception( 'Invalid table name' );
$dw = new DatabaseWrapper(
new PDO('mysql:host=localhost;dbname=books', 'root', ''),
$table
);
$additional = array();
foreach( array_keys( $_GET ) as $k ) {
if ( $k != 'table' && $k != 'method' && $k != 'id' ) {
$additional[$k] = $_GET[$k];
}
}
switch( $_GET['method'] ) {
case 'get':
print json_encode( $dw->get( $_GET['id'] ) );
break;
case 'getAll':
print json_encode( $dw->getAll( $additional ) );
break;
case 'insert':
print json_encode( $dw->insert( $additional ) );
break;
case 'update':
$dw->update( $newid, $additional );
print json_encode( true );
break;
case 'delete':
$dw->delete( $_GET['id'] );
print json_encode( true );
break;
default:
throw new Exception( 'Unknown method' );
break;
}
} catch ( Exception $e ) {
print json_encode( array( 'error' => $e->getMessage() ) );
}
?>
|
この単純なスクリプトにより、Web リクエストの GET パラメーターを取得して DatabaseWrapper インスタンスに送信します。それが成功すると結果を JSON 形式にします。この処理の間にエラーが発生すると、例外がスローされ、その例外も JSON 形式にされます。
このスクリプトを使ってデータベースの任意のテーブルにアクセスできないように、このコードはまず、要求されたテーブル名をチェックします。この場合は authors テーブルと books テーブルにのみアクセスすることができます。このコードを皆さん自身のスキーマに使用する場合には、このテーブル名を変更する必要があります。
次にこのコードはメソッドのパラメーターを調べ、5 つのアクション (get、getAll、delete、insert、 update) のどれを呼び出すのかを判断します。insert、update、getAll の場合には、追加のキーの配列も GET 配列に入れて同時に送信します。
このコードをテストするためには、Web ブラウザーを開き、さまざまな URL パラメーターを使って driver.php ページにアクセスします。図 2 は get メソッドによる 1 つのレコードへのアクセスを URL で指定する場合を示しています。
図 2. 1 つのレコードに対する JSON リクエスト
図 2 は要求された著者の行のレコードを JSON にエンコードしたものを示しています。この JSON オブジェクトには、JavaScript のハッシュ・テーブルとしてエンコードされたレコードの id フィールドと name フィールドの両方が含まれています。
このリクエストのパラメーターは以下のとおりです。
driver.php?table=authors&method=get&id=1 |
すべてのレコードを取得するためには、以下のように単純にメソッドを変更し、id の指定を削除します。
driver.php?table=authors&method=getAll |
図 3 は取得した結果を JSON にエンコードしたものを示しています。
図 3. すべてのレコードに対する JSON リクエスト
図 3 では JavaScript レコードの配列が返されており、この配列にはデータベースの各行の name フィールドと id フィールドが格納されたオブジェクトが含まれています。
このサーバー・コードは Web インターフェースを作成するための基礎として十分です。では次にフロントエンドを作成することにしましょう。
Web ブラウザーからデータベースにアクセスするためには、Ajax によって driver.php を使用するクラスが必要です。Ajax リクエストは jQuery や Prototype.js などの JavaScript ライブラリーを使用しないと少し面倒かもしれません。この場合は jQuery を使用しましたが、何ヶ所かコードを変更すればどのライブラリーを使用することもできます。
jQuery の他に、Class 拡張機能も使用します。Class 拡張機能を使用すると、新しい JavaScript クラス (この場合は DbWrapper クラス) を容易に定義することができます (リスト 4)。
リスト 4. dbwrapper.js
var DbWrapper = Class.create({
table: '',
get: function( id, callback ) {
$.getJSON( 'driver.php', { table: this.table, method: 'get', id: id }, callback );
},
getAll: function( callback, params ) {
if ( params == null ) params = {};
params.table = this.table;
params.method = 'getAll';
$.getJSON( 'driver.php', params, callback );
},
insertObject: function( params, callback ) {
params.table = this.table;
params.method = 'insert';
$.getJSON( 'driver.php', params, callback );
},
updateObject: function( id, params, callback ) {
params.table = this.table;
params['id'] = id;
params.method = 'insert';
$.getJSON( 'driver.php', params, callback );
},
deleteObject: function( id, callback ) {
$.getJSON( 'driver.php', { table:this.table,
'id':id, method: 'delete' }, callback );
}
});
|
この簡単なクラスにより、バックエンドの 5 つの各メソッドをラップします。これらのメソッドは、それぞれに異なるパラメーター・セットを使用して getJSON を呼び出します。また、コールバックを引数に取り、リクエストの結果として返されるデータをそのコールバックによって処理します。
insert、update、delete メソッドの名前が insertObject、updateObject、deleteObject に変更されていますが、これは delete が JavaScript の予約語であるためです。
この新しい JavaScript クラスを使用するページをリスト 5 に示します。
リスト 5. Index.html
<html>
<head><title>Database access test</title>
<script src="jquery-1.5.min.js"></script>
<script src="Class-0.0.2.min.js"></script>
<script src="dbwrapper.js"></script>
<script>
var dbw = null;
function updateAuthorsTable() {
dbw.getAll( function( data ) {
$('#authors').html('<table id="authors"><tr><td>ID</td>
<td>Author</td></tr></table>');
$(data).each( function( ind, author ) {
$('#authors tr:last').after('<tr><td>'+author.id+'</td>
<td>'+author.name+'</td></tr>');
} );
} );
}
$(document).ready(function() {
dbw = new DbWrapper();
dbw.table = 'authors';
updateAuthorsTable();
$('#addbutton').click( function() {
dbw.insertObject( { name: $('#authorname').val() },
function( data ) {
updateAuthorsTable();
} );
} );
} );
</script>
</head>
<body>
<table id="authors">
</table>
<h3>Add A New Author</h3>
Name: <input type="text" id="authorname">
<input type="submit" value="Add" id="addbutton">
</body>
</html>
|
このページではまず、jQuery と Class 拡張機能、そして新しく作成した dbwrapper.js ファイルを読み込んでいます。続いて、ページの読み込みが完了すると実行される ready 関数内で DbWrapper が作成されます。作成された DbWrapper インスタンスは updateAuthorsTable 関数で使用されます。updateAuthorsTable 関数はこのラッパーを使用して、データベースに対してすべての著者を要求します。次に updateAuthorsTable 関数はページ上のテーブルに何行かを追加し、データベース・テーブルの内容を表示します。
著者名のフォームはページの一番下、「Add (追加)」ボタンの近くにあります。この「Add (追加)」ボタンのスクリプトは DbWrapper クラスの insertObject メソッドを呼び出し、新しい行をデータベースに追加します。その処理が成功すると、このスクリプトは updateAuthorsTable 関数を呼び出し、新しい行を使用して表示を更新します。
すべてが適切に設定されている場合、このページに初めてアクセスすると、図 4 のようなページが表示されます。
図 4. 著者を動的に HTML リストで表示し、著者の追加を行えるページ
この図から、このページがサーバー・サイドの driver.php スクリプトに接続してデータを要求し、取得したデータを表示するために整形していることがわかります。すべてが適切に動作していれば、この画面には、データベースから取得した著者のリストと、新しい著者のレコードを作成するための入力ボックスおよび「Add (追加)」ボタンが表示されているはずです。
次のステップは、行を適切に追加できるかどうかをチェックすることです。この場合は著者名のフィールドに「Geraldo Rivera」と入力し (図 5)、「Add (追加)」をクリックします。
図 5. 新しいレコードを追加する
「Add (追加)」ボタンのコードにより、DbWrapper クラスの insertObject メソッドが呼び出され、それによってサーバーの insert メソッドが呼び出されます。このステップが成功すると、このページの JavaScript はこのページがロードされる際に最初に呼び出された関数を使用し、authors テーブルを更新します。その結果を図 6 に示します。
図 6. 新しいレコードによって更新されたページ
図 6 は、アプリケーションによって「Geraldo Rivera」という著者が authors テーブルに追加され、その著者の名前が画面上に表示される様子を示しています。この図から、バックエンドのデータベースに対して読み取りアクセスも書き込みアクセスもできることがわかります。この例よりも多くのテーブルを持つ別のスキーマにアクセスする場合には、その新しいテーブルに対するサポートを driver.php スクリプトに追加します。
この記事の最初で触れたように、これらのコードはプロトタイプの作成を短時間で行うには最適だと思いますが、本番用ではありません。その理由は以下のとおりです。
- 誰もがアクセスできるコードであるため、本番用にするにはユーザー、ロール、アクセス許可を扱うコードが必要になります。
- 通常はスキーマ内のテーブル間にはリレーションシップがありますが、そうした場合にはあまり効率的ではありません。
また、この例はあまり RESTful ではないことも欠点です。REST の世界のアプリケーションは、アプリケーションの状態を変更する形で GET リクエストに応答するべきではありません (アプリケーションの状態変化は insert、update、delete リクエストで発生します)。私の言い訳としては、POST や DELETE、その他 REST に必要な URL への書き換え処理すべてを扱えるようにPHP サービスを設定しようとすると、ここで説明した方法よりもはるかに難しくなります。ここではあくまでも説明用として、単純なページを使用しました。
ただしそうした注意点を別にすれば、この例は実際には、PHP、PDO ライブラリー、jQuery を組み合わせ、JavaScript による移植可能なデータ・アクセス・レイヤーを作成する方法を示す例としては、非常に楽しめる例です。たとえ本番用として使用することはできなくても、楽しめること請け合いなので、ぜひ試してみてください。
学ぶために
- jQuery のサイトでは、短時間で Web 開発を行うために必要なものを入手することができます。jQuery ライブラリーをダウンロードすることができ、また必要なドキュメントすべてを入手することもできます。
- jQuery の Class 拡張機能について調べてください。この便利な拡張機能は Google Code サイトでホストされており、クラスの作成や jQuery の操作に役立ちます。
- php.net のサイトは PHP のリファレンス・サイトとしては最も優れています。PHP は広く使用されている汎用のスクリプト言語であり、特に Web 開発に適しています。
- 技術標準に関する素晴らしいサイト、W3C
のサイトを訪れてください。この記事で特に重要な標準は XML 標準です。
- PDO
ライブラリーには、PHP でデータベースにアクセスする場合のさまざまな使用事例など、優れたドキュメントが豊富に用意されています。
- 著者の
Jack Herrington が developerWorks に寄稿した他の記事も読んでください (2005年3月から現在まで)。Ajax、JSON、PHP、XML、その他の技術が解説されています。
- developerWorks の XML ゾーンには、XML の領域でのスキルを磨くためのリソースが豊富に用意されています。
- developerWorks の Open source
ゾーンをご覧ください。オープンソース技術を使った開発や、IBM 製品でオープンソース技術を使用するためのハウ・ツー情報やツール、プロジェクトの更新情報、最も人気のあった記事やチュートリアルの一覧など、豊富な情報が用意されています。
- My developerWorks で developerWorks のエクスペリエンスをパーソナライズしてください。
- XML および関連技術において IBM 認定技術者になる方法については、IBM XML certification を参照してください。
- developerWorks の XML ゾーンを XML
の技術ライブラリーとして利用してください。広範な話題を網羅した技術記事やヒント、チュートリアル、技術標準、および IBM Redbooks
などが用意されています。また、他にも XML に関するヒント記事があります。
- developerWorks の
Technical events and webcasts で最新情報を入手してください。
- developerWorks on Twitter から今すぐ Twitter
に参加して developerWorks のツイートをフォローしてください。
- developerWorks podcasts でソフトウェア開発者のための興味深いインタビューや議論を聞いてください。
- developerWorks On demand
demos をご覧ください。初心者のための製品インストール方法やセットアップのデモから、上級開発者のための高度な機能に至るまで、多様な話題が解説されています。
製品や技術を入手するために
- IBM 製品の評価版をダウンロードするか、あるいは IBM SOA Sandbox
のオンライン試用版で、DB2®、Lotus®、Rational®、Tivoli®、WebSphere® などが提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。
議論するために
- XML zone discussion
forums では XML に関する議論が行われています。
- developerWorks コミュニティーで開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとコミュニケーションをしてください。
