【プログラミング】ASP.NETのObjectDataSource活用(3)―ADO.NETで作る(3)

ASP.NETにおけるObjectDataSourceコントロールの活用、続きです。前回は、データバインドコントロールのページング機能に対応するために、取得レコードの範囲を指定できるタイプのメソッドと、その他の必要なメソッドについて書いてみました。そのとき、メソッド内部の具体的なロジックは省略しましたので、今回はそれについて書いてみましょう。

【プログラミング】ASP.NETのObjectDataSource活用(2)―ADO.NETで作る(2)

いろいろ調べて試してみましたら、「指定範囲のレコードを取ってくる」ためのSQL文というのは、意外と複雑ということがわかりました。ですがその前に、処理内容が単純そうなレコード数取得のためのメソッドのロジックを書いてしまいましょう。シグネチャ部は省略しています。

        ConnectionStringSettings setting =
            ConfigurationManager.ConnectionStrings["ConnectionString"];
        DbProviderFactory factory =
          DbProviderFactories.GetFactory(setting.ProviderName);
        DbConnection db = factory.CreateConnection();
        db.ConnectionString = setting.ConnectionString;
        DbCommand command = factory.CreateCommand();
        command.Connection = db;
        string select_base = "SELECT COUNT(*) FROM FlickrPhoto ";
        command.CommandText =
          select_base + getFilterString(factory, command, category);
        db.Open();
        int count = (int)command.ExecuteScalar();
        db.Close();
        return count;

前半部は、1回目で紹介したコードと同じです。ADO.NETを使う場合の定型的なコードです。SELECT文を作成する箇所で、WHERE句を動的に生成するprivateなgetFilterStringメソッドを呼び出しています。なぜメソッド化しているかというと、レコード取得のためのSelectMethodでも必要になるので、汎用化しているのです。ちなみに、こんな内容です。

    private string getFilterString(DbProviderFactory factory, DbCommand command,
        int category)
    {
        string select_query = "WHERE {0} ";
        string category_filter = "FlickrPhoto.category=@category";
        string category_str = "";
        if (category != 0)
        {
            DbParameter param = factory.CreateParameter();
            param.ParameterName = "@category";
            param.Value = category;
            command.Parameters.Add(param);
            category_str = category_filter;
        }
        return String.Format(select_query, category_str);
    }

categoryが0なら全件検索、それ以外ならその値に従ってフィルタリングするというWHERE句を返します(全件検索の場合は空文字列となります)。

次が本番の、SelectMethodの中身です。行が長くならないように文字列を分割していたりしますが、ご了承下さい。

        ConnectionStringSettings setting =
          ConfigurationManager.ConnectionStrings["ConnectionString"];
        DbProviderFactory factory =
          DbProviderFactories.GetFactory(setting.ProviderName);
        DbConnection db = factory.CreateConnection();
        db.ConnectionString = setting.ConnectionString;
        DbCommand command = factory.CreateCommand();
        command.Connection = db;
        string subquery_base = "SELECT photo_id, ………, " +
          "ROW_NUMBER() OVER({0}) AS rn FROM FlickrPhoto ";
        string mainquery_base = "SELECT * FROM ({0}) AS subquery " +
          "WHERE (rn BETWEEN {1} AND {2})";
        string sub_query =
            string.Format(subquery_base, getOrderString(direction)) +
            getFilterString(factory, command, category);
        command.CommandText = string.Format(mainquery_base, sub_query,
          startRowIndex+1, startRowIndex+maximumRows);
        db.Open();
        return command.ExecuteReader(CommandBehavior.CloseConnection);

こちらも、前半は1日目のものと同じで、ADO.NETの定番的なコードです。核心はそのあとで、subquery_baseとmainquery_baseという2つの文字列(SELECT文)を書いていますが、この中身です。また、ROW_NUMBER()という組み込み関数を使っています。このあたり、ちょっと詳しく書いてみます。

まず、2つのSELECT文の意味ですが、subquery_baseには「サブクエリ」と呼ばれるものを、mainquery_baseには本来のクエリ(CommandTextプロパティに与えるもの)を書いています。サブクエリというのは文字どおり副次的なクエリですが、この場合「絞り込んで並び替える」ために使っています。SelectMethod自体に与えられた条件で絞り込んで並び替えるためのクエリですね。なぜサブクエリを使う必要があるかというのは、あとでわかってきます。

そして、サブクエリの取得フィールドにあるROW_NUMBER()関数ですが、OVER句の中身にgetOrderStringの戻り値を入れています(これは、条件によってORDER BY句を作成するぴゃいべーと名メソッドです)。これはつまり、指定された並び替え条件に基づき連番を振れ、しかもそれをrnという名前とせよ、ということになります。しかも、getFilterStringで絞り込みも行っています。このクエリを仮に単独で呼び出せば、条件に従って並び替えられた、番号も振られた結果を得ることができるはずです。

このサブクエリの結果は、そのままmainquery_baseのFROM句に渡されますから、サブクエリの結果に基づいて何かする、ということになるわけです。こちらのWHERE句がrn(レコード番号)に対するBETWEENになっている点に注意して下さい。BETWEENのパラメータに、開始レコード番号と終了レコード番号を与えることで、指定範囲のレコードのみを取得できる、というわけです。

整理すると、

  1. 絞り込みと並び換えを、まずサブクエリで行ってしまう。
  2. サブクエリの結果から、指定範囲のレコードを取り出す。

という感じになります。これって、頭の中だけで考えていると混乱します。

そういえば、プライベートなメソッドgetOrderStringの中身を書いていませんでした。どういうものか想像は付くと思われますが、こんな感じです(taken_datetimeは並び換えのキーとなるフィールドの例です)。

    private string getOrderString(int direction)
    {
        string order_query = "ORDER BY taken_datetime {0}";
        string sortdir_str = "", order_str = "";
        if (direction == 0)
        {
            sortdir_str = "ASC";
        }
        else
        {
            sortdir_str = "DESC";
        }
        order_str = string.Format(order_query, sortdir_str);
        return order_str;
    }

今回はコードが長くなってしまいました。ですが、ObjectDataSourceでページングに対応したレコード取得を行う、という目的は達成できました。この記事を書いている時点で、「新館」に実装されています。

ですが、ADO.NETはベーシックなしくみゆえ、このようにゴリゴリとコードを書く必要があります。これはこれでよいのですが、実はもっとスマートな方法があるようなのです。そちらについては、ひととおりの検証を済ませた上で、連載の続きとして書いてみたいと思います。

コメント