LINQ文で動的にWhere句を組み立てるには?[3.5、C#、VB].NET TIPS

» 2010年03月11日 05時00分 公開
[一色政彦デジタルアドバンテージ]

この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。

「.NET TIPS」のインデックス

連載目次

 .NET Framework 3.5(=Visual Studio 2008)以降では、LINQ(Language INtegrated Query)機能がC#言語やVB言語に導入されている。LINQを使うと、SQL文ライクな構文のプログラム・コードを記述することで、オブジェクト配列やXML、データベースなどに対するクエリ(=データ取得)を効率的に行える。従来のように、SQL文を文字列で記述してクエリする場合と比べて、コードがかなり短くなる。

●LINQの問題と解決方法

 しかしその手軽さの半面、欠点もある。一番大きな問題は、(簡単には)動的にクエリを組み立てられないことだ。

 例えばキーワード検索で、そのキーワードが1つなのか10個なのか事前に決まっていない場合などではLINQは使いにくい。従来の文字列のSQL文であれば、文字列を連結しながら動的にWhere句を組み立てればよかったが、LINQの場合、ソース・ファイルにLINQ文がハード・コーディングされるため、実行時に動的にそれを変更することができない。

 この問題で悩む開発者は多く、実際にさまざまな解決方法が提示されている。

 動的にLINQ文を組み立てるには、通常のクエリ構文(from/select/whereキーワードなど)のLINQでハード・コーディングするのではなく、メソッド構文(Select/Whereメソッドなど)のLINQ(=メソッド・ベースのLINQ)を利用する必要がある。例えばWhereメソッドを使うことで、その引数に動的な値(=式ツリー)を指定できるようになる。メソッド・ベースのLINQで動的に式ツリーを組み立てる解決方法を2つほど紹介しよう。

 1つ目が、こちらのブログ記事で紹介されている「DynamicLINQ」という拡張メソッドを実装したクラスを利用して、(例えば)Whereメソッドの引数を文字列として記述する方法だ。文字列なので、動的に式を組み立てられるというわけだ。

 2つ目が、「XMLを扱えるLINQ ―LINQ to XML― の基礎を学ぼう」の「○ラムダ式を動的に組み上げる方法」で紹介されている、式ツリーをそのままプログラム・コードで動的に作成する方法だ。

 1つ目のDynamicLINQを筆者が試したところ、文字列が解析されてから式ツリーへの変換処理が行われるため、(解析後にどのメソッドが呼ばれるかが簡単に予想できないので)デバッグしにくい。これだと、例えば何らかのエラーが発生したときに、そのデバッグに余計に時間が掛かってしまう可能性がある。また現時点では、DynamicLINQはサンプルという扱いであり、不足する機能が出てきたら自分で拡張しなければならない。DynamicLINQを拡張していくのは、以下で紹介する手法よりもハードルが高く、あまりお勧めできない。

 それに対し、2つ目の手法はストレートに動的LINQを実現できる。メソッド・ベースのLINQコードのラムダ式部分、例えば左辺のパラメータ、右辺の式、さらに式と式を条件AND(=論理積)演算子(&&/AndAlso)で結合した式、というような感じでコーディングすればよい。ラムダ式の内容をストレートにコーディングするので、デバッグもしやすい。もちろんさまざまな意見や好みはあるだろうが、筆者はこの2番目の手法をお勧めする。

 そこで以下では、2番目の方法を説明する。

●ラムダ式を動的に組み立てて式ツリーを取得する方法(Whereメソッドの場合)

 ここでは、プログラム・コードでWhereメソッドの引数として指定する式ツリー型オブジェクト、具体的にはExpression<TDelegate>オブジェクト(System.Linq.Expressions名前空間)を動的に組み立てる方法を説明する。

 まず、静的なラムダ式でハード・コーディングしたメソッド・ベースのLINQコードの例を示そう。

using System;
using System.Linq;

static class Program
{
  static void Main()
  {
    // クエリ対象となるデータソース
    string[] dataSource =
      { "apple", "orange", "peach", "melon", "grape" };

    // 検索キーワードを3つ取得
    Console.WriteLine("指定した文字を含む文字列を検索します。");
    ConsoleKeyInfo info = Console.ReadKey();
    string character = info.KeyChar.ToString();
    Console.WriteLine("かlかoを含むものを検索。");
    string[] keywords = { character, "l", "o" };

    // メソッド・ベースのLINQ文(静的なラムダ式を利用)
    var query = dataSource.AsQueryable().
      Where(item =>
        item.ToLower().Contains(keywords[0].ToLower()) ||
        item.ToLower().Contains(keywords[1].ToLower()) ||
        item.ToLower().Contains(keywords[2].ToLower())).
      OrderByDescending(item => item);

    // 検索された用語を出力
    foreach (string term in query)
    {
      Console.WriteLine(term);
    }
    // 出力例(「m」を入力した場合):
    // 指定した文字を含む文字列を検索します。
    // mかlかoを含むものを検索。
    // orange
    // melon
    // apple

    // 出力を確認するために実行を止める
    Console.ReadKey();
  }
}

Sub Main()

  ' クエリ対象となるデータソース
  Dim dataSource As String() = _
    {"apple", "orange", "peach", "melon", "grape"}

  ' 検索キーワードを3つ取得
  Console.WriteLine("指定した文字を含む文字列を検索します。")
  Dim info As ConsoleKeyInfo = Console.ReadKey()
  Dim character As String = info.KeyChar.ToString()
  Console.WriteLine("かlかoを含むものを検索。")
  Dim keywords As String() = {character, "l", "o"}

  ' メソッド・ベースのLINQ文(静的なラムダ式を利用)
  Dim query = dataSource.AsQueryable(). _
    Where(Function(item) _
      item.ToLower().Contains(keywords(0).ToLower()) OrElse _
      item.ToLower().Contains(keywords(1).ToLower()) OrElse _
      item.ToLower().Contains(keywords(2).ToLower())). _
    OrderByDescending(Function(item) item)

  ' 検索された用語を出力
  For Each term As String In query
    Console.WriteLine(term)
  Next

  ' 出力例(「m」を入力した場合):
  ' 指定した文字を含む文字列を検索します。
  ' mかlかoを含むものを検索。
  ' orange
  ' melon
  ' apple

  ' 出力を確認するために実行を止める
  Console.ReadKey()

End Sub

メソッド・ベースのLINQ文(静的なラムダ式を利用)(上:C#、下:VB)

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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