JavaScript による手軽なデータ・アクセス・レイヤーを構築する

PHP、jQuery、JavaScript を使用し、単純かつセキュアにデータベースにアクセスする

この記事では、バックエンドのデータベースにアクセスするためのラッパーを PHP で作成し、そのラッパーと JavaScript で作成したユーザー・インターフェース・レイヤーのアクセス・ラッパーを接続するというプロセス全体について、順を追って説明します。

Jack D. Herrington, Senior Software Engineer, Fortify Software, Inc.

Photo of Jack HerringtonJack Herrington はサンフランシスコ湾岸地域に住む技術者、著作者、講演者です。彼の最新の活動や記事を http://jackherrington.com で知ることができます。



2011年 3月 15日

JavaScript によって手軽にデータにアクセスする

よく使われる頭文字語

  • Ajax: Asynchronous JavaScript + XML
  • HTML: HyperText Markup Language
  • JSON: JavaScript Object Notation
  • PDO: PHP Data Objects
  • REST: Representational State Transfer
  • SQL: Structured Query Language
  • URL: Uniform Resource Locator
  • W3C: World Wide Web Consortium
  • XML: Extensible Markup Language

例えば皆さんが JavaScript を使って、非常に洗練された Web 2.0 アプリケーションのプロトタイプを短時間で作成したいものの、データベースに接続するためのコードの作成に要する時間をなるべく少なくしたいとします。従来の方法では、データベースからデータを取得してフロントエンドで利用するためには、データベースの各テーブル用にクラスを作成し、各クラスにはすべての CRUD (Create、Read、Update、Delete) メソッドを用意する必要があります。さらにそれらのコードを呼び出すマーシャリング・コードを作成することで、フロントエンドへのアクセス・レイヤーを提供する必要があります。さらにまたそのレイヤーの上には、バックエンドにアクセスするための JavaScript ライブラリーを配する必要があります。何て面倒なのでしょう!

この記事では従来の方法に代わる方法として、1 つのデータベース・クラスを使用して複数のデータベース・テーブルをラップする方法について説明します。この方法では、1 つのドライバー・スクリプトによってフロントエンドをバックエンドに接続し、フロントエンドをラップするクラスによって、必要なすべてのテーブルにアクセスします。図 1 は、この方法のスタック全体を示しています。

図 1. JavaScript によるデータ・アクセス・レイヤーのアーキテクチャー
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 フロントエンドを作成する

ブラウザーにデータを表示するためには、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 ページにアクセスします。図 2get メソッドによる 1 つのレコードへのアクセスを URL で指定する場合を示しています。

図 2. 1 つのレコードに対する JSON リクエスト
1 人の著者名 (Jack Herrington) に対するリクエストのスクリーン・キャプチャー

図 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 リクエスト
複数の著者名 (Jack Herrington、Dave Smith、Harold Rollins、Geraldo Rivera) に対するリクエストのスクリーン・キャプチャー

図 3 では JavaScript レコードの配列が返されており、この配列にはデータベースの各行の name フィールドと id フィールドが格納されたオブジェクトが含まれています。

このサーバー・コードは Web インターフェースを作成するための基礎として十分です。では次にフロントエンドを作成することにしましょう。


JavaScript によってアクセスするクラスを作成する

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 リストで表示し、著者の追加を行えるページ
著者のリスト (Jack Herrington、Dave Smith、Harold Rollins) と「Add A New Author (新しい著者の追加)」フィールドが表示されている画面のスクリーン・キャプチャー

この図から、このページがサーバー・サイドの driver.php スクリプトに接続してデータを要求し、取得したデータを表示するために整形していることがわかります。すべてが適切に動作していれば、この画面には、データベースから取得した著者のリストと、新しい著者のレコードを作成するための入力ボックスおよび「Add (追加)」ボタンが表示されているはずです。

次のステップは、行を適切に追加できるかどうかをチェックすることです。この場合は著者名のフィールドに「Geraldo Rivera」と入力し (図 5)、「Add (追加)」をクリックします。

図 5. 新しいレコードを追加する
「Add A New Author (新しい著者の追加)」フィールドに新しい著書名 (Geraldo Rivera) を入力した画面のスクリーン・キャプチャー

「Add (追加)」ボタンのコードにより、DbWrapper クラスの insertObject メソッドが呼び出され、それによってサーバーの insert メソッドが呼び出されます。このステップが成功すると、このページの JavaScript はこのページがロードされる際に最初に呼び出された関数を使用し、authors テーブルを更新します。その結果を図 6 に示します。

図 6. 新しいレコードによって更新されたページ
著者リスト (Jack Herrington、Dave Smith、Harold Rollins) に新しい著者 (Geraldo Rivera) が追加された画面のスクリーン・キャプチャー

図 6 は、アプリケーションによって「Geraldo Rivera」という著者が authors テーブルに追加され、その著者の名前が画面上に表示される様子を示しています。この図から、バックエンドのデータベースに対して読み取りアクセスも書き込みアクセスもできることがわかります。この例よりも多くのテーブルを持つ別のスキーマにアクセスする場合には、その新しいテーブルに対するサポートを driver.php スクリプトに追加します。


まとめ

この記事の最初で触れたように、これらのコードはプロトタイプの作成を短時間で行うには最適だと思いますが、本番用ではありません。その理由は以下のとおりです。

  • 誰もがアクセスできるコードであるため、本番用にするにはユーザー、ロール、アクセス許可を扱うコードが必要になります。
  • 通常はスキーマ内のテーブル間にはリレーションシップがありますが、そうした場合にはあまり効率的ではありません。

また、この例はあまり RESTful ではないことも欠点です。REST の世界のアプリケーションは、アプリケーションの状態を変更する形で GET リクエストに応答するべきではありません (アプリケーションの状態変化は insert、update、delete リクエストで発生します)。私の言い訳としては、POST や DELETE、その他 REST に必要な URL への書き換え処理すべてを扱えるようにPHP サービスを設定しようとすると、ここで説明した方法よりもはるかに難しくなります。ここではあくまでも説明用として、単純なページを使用しました。

ただしそうした注意点を別にすれば、この例は実際には、PHP、PDO ライブラリー、jQuery を組み合わせ、JavaScript による移植可能なデータ・アクセス・レイヤーを作成する方法を示す例としては、非常に楽しめる例です。たとえ本番用として使用することはできなくても、楽しめること請け合いなので、ぜひ試してみてください。

参考文献

学ぶために

製品や技術を入手するために

議論するために

  • XML zone discussion forums では XML に関する議論が行われています。
  • developerWorks コミュニティーで開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとコミュニケーションをしてください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source
ArticleID=645790
ArticleTitle=JavaScript による手軽なデータ・アクセス・レイヤーを構築する
publish-date=03152011