最初に、membersテーブルに対するデータアクセスを担当するmembers.aspxファイルが提供するサービスとその実装について説明する。
このASPXでは、以下の5つのサービスを提供する。
| 機能 | URI | メソッド | リクエスト | レスポンス | 内容 | 
|---|---|---|---|---|---|
| ログイン | members.aspx | POST | フォーム | JSON | ユーザーIDとパスワードを指定してログイン | 
| メンバー情報取得 | members.aspx?q=edit | GET | - | JSON | 更新処理用にメンバーの情報を提供する | 
| メンバー情報更新 | members.aspx | POST | フォーム | JSON | 与えられたデータでメンバーの情報を更新する | 
| メンバー一覧の取得 | members.aspx | GET | - | JSON | ドロップダウンリスト用に名前とIDのリストを提供する | 
| ログアウト | members.aspx | POST | フォーム | JSON | 現在のログイン状態を削除する | 
| members.aspxファイルが提供するサービス | |||||
実装を以下に示す。
<%@ Page Language="C#" EnableViewState="false" Debug="true"%>
<%@ Import namespace="System.Collections.Generic" %>
<%@ Import namespace="System.Data.SqlClient" %>
<%@ Import namespace="System.Web" %>
<%@ Import namespace="System.Web.Script.Serialization" %>   ← (1)
<%
if (Request.HttpMethod == "POST" && Request.Params["logout"] != null)  // ログアウト処理
{
  Session["member"] = null;   ← (2)
  Response.Write("{}");       ← (3)
  Response.End();             ← (4)
}
var result = new object();
using (var connection = new SqlConnection("Data Source=.\\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|domodb.mdf;User Instance=true"))
{
  connection.Open();
  using(var command = connection.CreateCommand())
  {
    var user = Session["member"] as int?;   ← (5)
    if (Request.HttpMethod == "POST")
    {
      if (Request.Params["save"] != null)   // メンバー情報更新処理 ← (6)
      {
        if (user == null)                   ← (7)
        {
          Response.End();
        }
        var psw = Request.Params["psw"];
        command.CommandText = string.Format("update members set zip=@zip,address=@address{0} where id=@id",
          ((psw != null) ? ",password=@psw" : string.Empty));  ← (8)
        command.Parameters.Add(new SqlParameter("@zip", Request.Params["zip"]));
        command.Parameters.Add(new SqlParameter("@address", Request.Params["address"]));
        if (psw != null)
        {
          command.Parameters.Add(new SqlParameter("@psw",
            string.IsNullOrEmpty(psw) ? (object)DBNull.Value : (object)psw)); ← (9)
        }
        command.Parameters.Add(new SqlParameter("@id", user));
        result = new { affected = command.ExecuteNonQuery() };   ← (10)
      }
      else  // ログイン処理
      {
        if (string.IsNullOrEmpty(Request.Params["psw"]))
        {
          command.CommandText = "select id, name from members where id=@id and password is null";
        }
        else
        {
          command.CommandText = "select id, name from members where id=@id and password=@psw";
          command.Parameters.Add(new SqlParameter("@psw", Request.Params["psw"]));
        }
        command.Parameters.Add(new SqlParameter("@id", Request.Params["id"]));
        using (var reader = command.ExecuteReader())
        {
          if (reader.Read())
          {
            Session["member"] = reader["id"];      ← (11)
            result = new { id = (int)reader["id"], name = reader["name"] as string };
          }
        }
      }
    }
    else if (Request.Params["q"] == "edit")  // メンバー情報取得処理
    {
      if (user == null)
      {
        Response.End();
      }
      command.CommandText = "select * from members where id=@id";
      command.Parameters.Add(new SqlParameter("@id", user));
      using (var reader = command.ExecuteReader())
      {
        if (reader.Read())
        {
          result = new { id = (int)reader["id"],
                         name = reader["name"] as string,
                         zip = reader["zip"] as string,
                         address = reader["address"] as string,
                       };
        }
        else
        {
          Session["member"] = null;
        }
      }
    }
    else  // メンバー一覧の取得処理
    {
      var list = new List<object>();
      command.CommandText = "select id, name from members order by name";
      using (var reader = command.ExecuteReader())
      {
        while (reader.Read())
        {
          list.Add(new {id = (int)reader["id"], name = reader["name"] as string});
        }
      }
      result = list.ToArray();
    }
  }
  connection.Close();
}
var ser = new JavaScriptSerializer();   ← (12)
%>
<%= ser.Serialize(result) %>            ← (13)
*8 よりきれいなAPIでは、レスポンスのデータが不要な場合HTTPステータス204(No Content)を返すものだが、ここではもっと単純にASPXで記述しやすい方法を選んだ。
*9 ここでは極端に簡略化しているが、パスワードをデータベースに保存する場合は、ソルトとしてIDなどをパスワードの先頭に付加したうえでSHA-256などのハッシュ関数を複数回適用した結果を格納すべきである。これは本連載の第2回で示したASPXに直接管理者のパスワードを記入するのとはレベルが異なる問題である。というのは、このWebアプリではメンバーは自分でパスワードを設定できる。メンバーの中には、業務上の秘匿を要するパスワードや、プライベートで利用しているSNSのパスワードを、複雑で覚えにくい(ここまでは良い)単一のパスワードの使い回し(褒められたことではない)で運用しているものが間違いなく存在するからである。従って、自分以外の人間のパスワードを預かる場合は、可能な限りパスワードを保護する必要がある。
*10 世の中には「anonymous class」を「無名クラス」と翻訳する流儀がある。しかし生成したオブジェクトのGetType()メソッドを呼び出して得られるTypeオブジェクトはName(FullName)プロパティを持つ。このプロパティからはこのクラスのクラス名が得られる。つまりソースコード上には出て来ないがCLR上はクラス名があり、かつ必要があれば利用できる(つまり処理系依存の実装詳細ではない)。従って「無名」というのはCLRの匿名クラスに関しては明白な誤訳だ。
JavaScriptコードからXMLHttpRequestオブジェクトを利用してWebサービスを呼び出すときに考慮すべき点はいくつかある。例えば、APIの粒度であったり、URIの設計であったりだ。
ここでは、こういった考慮点のうち、リクエストとレスポンスのデータ形式について説明する。
WCFなどの追加のフレームワークを利用しないという選択をした場合、リクエストとレスポンスは自力で処理する必要がある。このときのベストプラクティスとして、筆者はここで示した組み合わせ、すなわちリクエストにはフォームを利用し、レスポンスにはJSONを利用するのがベストと考える。
以下、利用可能なデータ形式とそれぞれの特徴について示す。
| データ形式 | リクエスト/レスポンス | 長所 | 短所 | 
|---|---|---|---|
| XML | 両方 | C#での利用が容易 | 冗長。JavaScriptのみでパースするのは面倒 | 
| JSON | 両方 | JavaScript、C#両方で利用が容易 | 無し | 
| フォーム | リクエスト | JavaScript/C#での利用が容易 | 特に無し | 
| HTML片 | レスポンス | JavaScriptでの利用が容易 | サーバーがHTMLに密結合される | 
| CSV | 両方 | 無し | 無し | 
| データ形式と長所短所 | |||
JavaScriptでJSONを利用するのはJSONの成り立ちから当然、容易である。唯一注意が必要な点は、HTMLを生成する場合にオブジェクトのプロパティから取得した文字列に対してJavaScriptでエスケープ処理が必要となることだ。JavaScriptの関数やDOMのメソッドにHTML用のエスケープは含まれないため、多少面倒になる。それを避けるためにサーバー側があらかじめエスケープするとなると、今度はJavaScriptの利用方法に制約を課すことになる(使い方によってはアンエスケープする必要が出てくる)。
C#にとってもJSONは利用しやすい。本連載の第2回にある「コード表示ブロック」で説明したように、ASPXのコードは生成されたメソッドの内部に展開されるため、クラスやメソッドを宣言できないという制限を持つ。しかしmembers.aspxファイルの例((10))から分かるように、匿名クラスを利用することでレスポンス用のオブジェクトを簡単に作成し、JSON化できる。
リクエストにJSONを使った場合の問題は、ASPXのみでは*11デシリアライズする手段がないことだ。正確にはデシリアライズ直前までは、事前に生成した匿名クラスのオブジェクトの型情報を利用して処理できるのだが、匿名クラスが無引数のpublicなコンストラクターを持たないため、最終的にDeserializeメソッドの呼び出しに失敗する。
*11 もちろん、無引数publicコンストラクターを持つクラスを定義すればデシリアライズ可能である。ここでは単独のASPXを前提しているため、クラスを定義できないという制限がある。
それに対してフォームをリクエストに利用するのは双方にとってメリットがある。JavaScript側でDOMのフォームオブジェクトを利用せずに独自にフォーム形式を作成するとしてもencodeURIComponent関数を呼び出してエンコード可能であるし、ASPX側はRequestオブジェクトのParamsプロパティを利用して容易にアクセスできる。
JavaScriptで返された文字列を使ってHTMLを生成する場合、一番容易なのはASPXがHTML片を返すことだ。その場合、あらかじめテキスト部分をエスケープできるし、受け取ったJavaScript側は適切な要素のinnerHTMLプロパティへレスポンステキストを設定するだけで済む。ただし、この場合、サーバー側の仕様によってクライアント側は利用方法(適用箇所)が限定されることになり、一方サーバー側はクライアント側のHTMLと密結合されることになる。つまり今回退けた埋め込みHTMLとしての利用とそれほど変わらない。
例えば<table>タグに埋め込む行をサーバーが返すと設計した場合、サーバーが生成するHTML片は<tr>タグとその中に含まれる<td>タグとデータとなるだろう。しかしクライアント側がHTMLを見直して<table>ではなく<ul>を利用することになると、サーバーのHTML片の生成方法を変えるか、あるいはクライアント側で<tr>タグの内部を解析してデータを取り出さなければならなくなる。一方、サーバーがJSONなどの汎用的でデータアクセスが容易な形式でクライアントへデータを返すように設計すれば、表示を<table>から<li>へ変えた場合の修正はクライアント側のみで完了する。
次回(最終回)は、ここで示したmembers.aspxファイルを呼び出すJavaScript側の実装について説明する。
JavaScriptはここ10年くらいでコーディングスタイルやイディオムが様変わりしたプログラミング言語だ。次回はこれらの情報を交えて説明する。
Copyright© Digital Advantage Corp. All Rights Reserved.