この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
.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文(静的なラムダ式を利用)」というコメントの下にあるWhereメソッドとOrderByDescendingメソッドの引数はラムダ式になっている(参考:「C#ラムダ式 基礎文法最速マスター」)。Whereメソッドの引数を見ると、3つのキーワードが条件OR(=論理和)演算子(||/OrElse)で連結されている。ハード・コーディングされているため、常に3つのキーワードのどれかが含まれるかという検索しか行えない。
このLINQコードを、何個のキーワードでも自由に検索できるようにしてみよう。今回はWhereメソッドの引数を次のように書き換える。
……省略……
// メソッド・ベースのLINQ文(動的な式ツリー型を利用)
var query = dataSource.AsQueryable().
Where(GetExpressionTreeWhere(keywords)).
OrderByDescending(item => item);
……省略……
……省略……
' メソッド・ベースのLINQ文(動的な式ツリー型を利用)
Dim query = dataSource.AsQueryable(). _
Where(GetExpressionTreeWhere(keywords)). _
OrderByDescending(Function(item) item)
……省略……
GetExpressionTreeWhereメソッドは独自に作成したメソッドで、引数として文字列配列(キーワード群)を受け取り、戻り値として式ツリー型オブジェクト(=Expression<TDelegate>オブジェクト)を返す。具体的なコードは次のとおり。
private static Expression<Func<string, bool>> GetExpressionTreeWhere(string[] keywords)
{
// 引数が適切でない場合には、例外を発行する
int length = keywords.Length;
if (length == 0)
{
throw new ArgumentException("キーワード指定なし。");
}
// ラムダ式における左辺のパラメータの名前
// (「item => ……」の部分に対応)
const string paramName = "item";
// ラムダ式の左辺を構成するパラメータ項目の1つを作成
ParameterExpression parameter =
LambdaUtil.GetStringParameterExpression(paramName);
// ラムダ式の左辺であるパラメータ(全項目)を配列にまとめる
// (※ただし今回はパラメータ項目は1つしかない)
ParameterExpression[] parameters =
LambdaUtil.GetParameterExpressions(parameter);
// ラムダ式の右辺である式を組み立てる
Expression body = null;
for (int i = 0; i < length; i++)
{
// 「item.ToLower().Contains(keywords[0].ToLower())」
// という式を作成して、2つ目以降のキーワードは
// 条件OR演算子(||)で連結する
body = LambdaUtil.GetContainsExpression(
parameter, keywords[i], (i == 0) ? null : body);
}
// 最後に、動的に作成したラムダ式から式ツリーを取得する
return LambdaUtil.GetLambdaExpressionWhere(parameters, body);
}
Private Function GetExpressionTreeWhere(ByVal keywords As String()) As Expression(Of Func(Of String, Boolean))
' 引数が適切でない場合には、例外を発行する
Dim length As Integer = keywords.Length
If length = 0 Then
Throw New ArgumentException("キーワード指定なし。")
End If
' ラムダ式における左辺のパラメータの名前
' (「Function(item) ……」の部分に対応)
Const paramName As String = "item"
' ラムダ式の左辺を構成するパラメータ項目の1つを作成
Dim parameter As ParameterExpression = _
LambdaUtil.GetStringParameterExpression(paramName)
' ラムダ式の左辺であるパラメータ(全項目)を配列にまとめる
' (※ただし今回はパラメータ項目は1つしかない)
Dim parameters As ParameterExpression() = _
LambdaUtil.GetParameterExpressions(parameter)
' ラムダ式の右辺である式を組み立てる
Dim body As Expression = Nothing
For i As Integer = 0 To length - 1
' 「item.ToLower().Contains(keywords[0].ToLower())」
' という式を作成して、2つ目以降のキーワードは
' 条件OR演算子(OrElse)で連結する
body = LambdaUtil.GetContainsExpression( _
parameter, keywords(i), IIf(i = 0, Nothing, body))
Next
' 最後に、動的に作成したラムダ式から式ツリーを取得する
Return LambdaUtil.GetLambdaExpressionWhere(parameters, body)
End Function
こちらも、コード内容についてはコメントを読んでいただきたい。
このメソッドでは、Whereメソッドのラムダ式の部分を動的に作成して、最後にそれを式ツリー型オブジェクトとして取得している。
上記のコードで使われているLambdaUtilクラスは、筆者が独自に作成したクラスである。具体的な内容は次のようになっている。
using System;
using System.Linq.Expressions;
using System.Reflection;
public static class LambdaUtil
{
/// <summary>
/// String.ToLowerメソッド。
/// </summary>
private static readonly MethodInfo ToLower =
typeof(string).GetMethod("ToLower", Type.EmptyTypes);
/// <summary>
/// String.Containsメソッド。
/// </summary>
private static readonly MethodInfo Contains =
typeof(string).GetMethod("Contains");
/// <summary>
/// ラムダ式におけるパラメータを作成する。
/// </summary>
/// <param name="paramName">パラメータ名</param>
/// <returns>パラメータ式</returns>
public static ParameterExpression GetStringParameterExpression(string paramName)
{
return Expression.Parameter(typeof(string), paramName);
}
/// <summary>
/// ラムダ式におけるパラメータ式の配列を作成する。
/// </summary>
/// <param name="parameters">複数のパラメータ式</param>
/// <returns>パラメータ式の配列</returns>
public static ParameterExpression[] GetParameterExpressions(params ParameterExpression[] parameters)
{
return parameters;
}
/// <summary>
/// 「x.Contains("keyword")」を条件OR演算子(||)で連結しながら式を作成する。
/// </summary>
/// <param name="parameter">ラムダ式の左にあるパラメータ</param>
/// <param name="keyword">検索キーワード</param>
/// <param name="curBody">現在の「x.Contains("keyword")」の表現文。初回はnullを指定。2回目以降は前回の戻り値を指定。</param>
/// <returns>「x.Contains("keyword")」を||演算子で連結した式</returns>
public static Expression GetContainsExpression(Expression parameter, string keyword, Expression curBody)
{
var keywordValue = Expression.Constant(keyword, typeof(string));
var newBody = Expression.Call(
Expression.Call(parameter, ToLower),
Contains,
Expression.Call(keywordValue, ToLower));
if (curBody != null)
{
return Expression.OrElse(curBody, newBody);
}
return newBody;
}
/// <summary>
/// 動的に作成したラムダ式から式ツリー型オブジェクトを取得する。
/// </summary>
/// <param name="parameters">パラメータ式の配列(=ラムダ式の左辺)</param>
/// <param name="body">式/文(=ラムダ式の右辺)</param>
/// <returns>式ツリー型オブジェクト</returns>
public static Expression<Func<string, bool>> GetLambdaExpressionWhere(ParameterExpression[] parameters, Expression body)
{
return Expression.Lambda<Func<string, bool>>(body, parameters);
}
}
Imports System.Linq.Expressions
Imports System.Reflection
Public Class LambdaUtil
''' <summary>
''' String.ToLowerメソッド。
''' </summary>
Private Shared ReadOnly ToLower As MethodInfo = _
GetType(String).GetMethod("ToLower", Type.EmptyTypes)
''' <summary>
''' String.Containsメソッド。
''' </summary>
Private Shared ReadOnly Contains As MethodInfo = _
GetType(String).GetMethod("Contains")
''' <summary>
''' ラムダ式におけるパラメータを作成する。
''' </summary>
''' <param name="paramName">パラメータ名</param>
''' <returns>パラメータ式</returns>
Public Shared Function GetStringParameterExpression(ByVal paramName As String) As ParameterExpression
Return Expression.Parameter(GetType(String), paramName)
End Function
''' <summary>
''' ラムダ式におけるパラメータ式の配列を作成する。
''' </summary>
''' <param name="parameters">複数のパラメータ式</param>
''' <returns>パラメータ式の配列</returns>
Public Shared Function GetParameterExpressions(ByVal ParamArray parameters As ParameterExpression()) As ParameterExpression()
Return parameters
End Function
''' <summary>
''' 「x.Contains("keyword")」を条件OR演算子(OrElse)で連結しながら式を作成する。
''' </summary>
''' <param name="parameter">ラムダ式の左にあるパラメータ</param>
''' <param name="keyword">検索キーワード</param>
''' <param name="curBody">現在の「x.Contains("keyword")」の表現文。初回はnullを指定。2回目以降は前回の戻り値を指定。</param>
''' <returns>「x.Contains("keyword")」を||演算子で連結した式</returns>
Public Shared Function GetContainsExpression(ByVal parameter As Expression, ByVal keyword As String, ByVal curBody As Expression) As Expression
Dim keywordValue = Expression.Constant(keyword, GetType(String))
Dim newBody = Expression.Call( _
Expression.Call(parameter, ToLower), _
Contains, _
Expression.Call(keywordValue, ToLower))
If Not curBody Is Nothing Then
Return Expression.OrElse(curBody, newBody)
End If
Return newBody
End Function
''' <summary>
''' 動的に作成したラムダ式から式ツリー型オブジェクトを取得する。
''' </summary>
''' <param name="parameters">パラメータ式の配列(=ラムダ式の左辺)</param>
''' <param name="body">式/文(=ラムダ式の右辺)</param>
''' <returns>式ツリー型オブジェクト</returns>
Public Shared Function GetLambdaExpressionWhere(ByVal parameters As ParameterExpression(), ByVal body As Expression) As Expression(Of Func(Of String, Boolean))
Return Expression.Lambda(Of Func(Of String, Boolean))(body, parameters)
End Function
End Class
コードの内容はコメントを参考にしてほしい。
LambdaUtilクラス内で使われているExpressionクラス(System.Linq.Expressions名前空間)の各メソッドの目的を簡単に示す。動的に式ツリーを組み立てるには、Expressionクラスのメソッドを活用する(今回使っているのは、ほんの一部である)。
以上で完成だ。
テストしやすいように、Visual Studio 2008プロジェクト全体を、下記のリンクからダウンロードできるようにした。
それでは、サンプル・プログラムの「keywords」変数(文字列配列)の部分に指定しているキーワードを1つにしたり、2つにしたりしてみてほしい。LINQコードを書き換えなくてもクエリが実行できることが確認できる。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:クラス・ライブラリ 処理対象:LINQ
使用ライブラリ:Expressionクラス(System.Linq.Expressions名前空間)
使用ライブラリ:Expression<TDelegate>オブジェクト(System.Linq.Expressions名前空間)
Copyright© Digital Advantage Corp. All Rights Reserved.