連載
» 2014年07月16日 15時35分 公開

第5回 ASP.NETによるWebアプリ開発(アプリケーションの開発 2)連載:ASP.NETによる軽量業務アプリ開発(3/3 ページ)

[arton, ]
前のページへ 1|2|3       

membersテーブルのデータアクセス

 最初に、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)

members.aspxファイル
  (1).NET FrameworkのオブジェクトからJSONを生成するために利用する。
  (2)ここではログアウトをセッション情報の消去によって実現している。ログオフ処理はSQL Serverを利用しないため、SQL Serverへ接続する前に完了させる。
  (3)リクエスト側は特にレスポンスを必要としないのでJSONの空オブジェクトを返す*8
  (4)Response.End()メソッドを呼び出すと内部で例外がスローされASP.NETがその時点で処理を終わらせる。後続の処理をスキップするため例外が利用されるが、クライアントにエラーが報告されるわけではない。
  (5)構造体の型名の後ろに「?」が付いたものはnullを許容する。
  (6)saveパラメーターを持つPOSTメソッドはメンバー情報の更新要求である。
  (7)セッションがユーザーIDを保持していないため、処理を終了する。
  (8)パスワード更新の有無によってSQLを変更する。
  (9)SqlParameterクラスのこのコンストラクターでは「null」はRDBの「NULL」を意味しないため、使い分けている。文字列とDBNull型は異なる型なのでobject型として共通化する*9
  (10)「new { 名前 = 値 [, 名前 = 値 ...] }」形式で匿名クラス*10のオブジェクトを生成できる。JavaScriptSerializerクラスのSerializeメソッド((13))は匿名クラスのオブジェクトもJSONにシリアライズできるため、ASPXからJSONを返送するのは容易である。
  (11)ログインに成功したらログインしたメンバーのユーザーIDをセッションに保存し、以降、メンバーの情報にアクセスする場合にはそれを利用する。
  (12)オブジェクトをJSONに変換するにはJavaScriptSerializerクラスを利用する。
  (13)JavaScriptSerializerクラスのSerializeメソッドにオブジェクトを与えるとJSONが生成されるので、それをレスポンスとする。

*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年くらいでコーディングスタイルやイディオムが様変わりしたプログラミング言語だ。次回はこれらの情報を交えて説明する。

「連載:ASP.NETによる軽量業務アプリ開発」のインデックス

連載:ASP.NETによる軽量業務アプリ開発

前のページへ 1|2|3       

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。