実行時の状態に基づくクエリの実行 (Visual Basic)
データ ソースに対して IQueryable または IQueryable(Of T) が定義されているコードについて考えてみます。
Dim companyNames As String() = {
"Consolidated Messenger", "Alpine Ski House", "Southridge Video",
"City Power & Light", "Coho Winery", "Wide World Importers",
"Graphic Design Institute", "Adventure Works", "Humongous Insurance",
"Woodgrove Bank", "Margie's Travel", "Northwind Traders",
"Blue Yonder Airlines", "Trey Research", "The Phone Company",
"Wingtip Toys", "Lucerne Publishing", "Fourth Coffee"
}
' We're using an in-memory array as the data source, but the IQueryable could have come
' from anywhere -- an ORM backed by a database, a web request, Or any other LINQ provider.
Dim companyNamesSource As IQueryable(Of String) = companyNames.AsQueryable
Dim fixedQry = companyNamesSource.OrderBy(Function(x) x)
このコードを実行するたびに、同じクエリが実行されます。 実行時の状態に応じてさまざまなクエリを実行するコードが必要になる可能性があるため、これは多くの場合、あまり役に立ちません。 この記事では、実行時の状態に基づいて別のクエリを実行する方法について説明します。
IQueryable または IQueryable(Of T) と式ツリー
基本的に、IQueryable には次の 2 つのコンポーネントがあります。
- Expression— 式ツリーの形式である、現在のクエリのコンポーネントの言語およびデータソースに依存しない表現。
- Provider— 現在のクエリを値または値のセットに具体化する方法を認識している LINQ プロバイダーのインスタンス。
動的なクエリのコンテキストでは、通常、プロバイダーは同じままとなります。クエリの式ツリーはクエリによって異なります。
式ツリーは変更できません。別の式ツリー — したがって、別のクエリ — が必要な場合は、既存の式ツリーを新しいもの (したがって、新しい IQueryable) に変換する必要があります。
次のセクションでは、実行時の状態に応じて異なる方法でクエリを実行する特定の手法について説明します。
- 式ツリー内から実行時の状態を使用する
- 追加の LINQ メソッドを呼び出す
- LINQ メソッドに渡される式ツリーを変更する
- Expression でファクトリ メソッドを使用して、Expression(Of TDelegate) 式ツリー式を作成する
- IQueryable の式ツリーにメソッド呼び出しノードを追加する
- 文字列を構築し、動的 LINQ ライブラリを使用する
式ツリー内から実行時の状態を使用する
LINQ プロバイダーでサポートされていると仮定した場合にクエリを動的に実行する最も簡単な方法は、次のコード例の length
など、閉じ込められた変数を使用して、クエリ内の実行時の状態を直接参照することです。
Dim length = 1
Dim qry = companyNamesSource.
Select(Function(x) x.Substring(0, length)).
Distinct
Console.WriteLine(String.Join(", ", qry))
' prints: C, A, S, W, G, H, M, N, B, T, L, F
length = 2
Console.WriteLine(String.Join(", ", qry))
' prints: Co, Al, So, Ci, Wi, Gr, Ad, Hu, Wo, Ma, No, Bl, Tr, Th, Lu, Fo
内部式ツリー — したがって、クエリ — は変更されていません。このクエリの場合は、length
の値が変更されているため、異なる値が返されます。
追加の LINQ メソッドを呼び出す
一般に、Queryable の組み込みの LINQ メソッドでは、次の 2 つの手順を行います。
- メソッド呼び出しを表す MethodCallExpression で現在の式ツリーをラップする。
- ラップされた式ツリーをプロバイダーに戻し、プロバイダーの IQueryProvider.Execute メソッドを使用して値を返すか、IQueryProvider.CreateQuery メソッドを使用して変換されたクエリ オブジェクトを返す。
元のクエリを、IQueryable(Of T) を返すメソッドの結果に置き換えて、新しいクエリを取得できます。 次の例のように、実行時の状態に基づいて条件付きでこれを行うことができます。
' Dim sortByLength As Boolean = ...
Dim qry = companyNamesSource
If sortByLength Then qry = qry.OrderBy(Function(x) x.Length)
LINQ メソッドに渡される式ツリーを変更する
実行時の状態に応じて、LINQ メソッドに異なる式を渡すことができます。
' Dim startsWith As String = ...
' Dim endsWith As String = ...
Dim expr As Expression(Of Func(Of String, Boolean))
If String.IsNullOrEmpty(startsWith) AndAlso String.IsNullOrEmpty(endsWith) Then
expr = Function(x) True
ElseIf String.IsNullOrEmpty(startsWith) Then
expr = Function(x) x.EndsWith(endsWith)
ElseIf String.IsNullOrEmpty(endsWith) Then
expr = Function(x) x.StartsWith(startsWith)
Else
expr = Function(x) x.StartsWith(startsWith) AndAlso x.EndsWith(endsWith)
End If
Dim qry = companyNamesSource.Where(expr)
LinqKit の PredicateBuilder などのサードパーティ製ライブラリを使用して、さまざまな部分式を構成することもできます。
' This is functionally equivalent to the previous example.
' Imports LinqKit
' Dim startsWith As String = ...
' Dim endsWith As String = ...
Dim expr As Expression(Of Func(Of String, Boolean)) = PredicateBuilder.[New](Of String)(False)
Dim original = expr
If Not String.IsNullOrEmpty(startsWith) Then expr = expr.Or(Function(x) x.StartsWith(startsWith))
If Not String.IsNullOrEmpty(endsWith) Then expr = expr.Or(Function(x) x.EndsWith(endsWith))
If expr Is original Then expr = Function(x) True
Dim qry = companyNamesSource.Where(expr)
ファクトリ メソッドを使用して式ツリーとクエリを構築する
この時点までのすべての例では、コンパイル時に要素型 —String
— したがって、クエリの型 —IQueryable(Of String)
がわかっています。 要素型に関係なくクエリにコンポーネントを追加したり、要素型に応じて異なるコンポーネントを追加したりすることが、必要になる場合があります。 System.Linq.Expressions.Expression でファクトリ メソッドを使用して、最初から式ツリーを作成し、実行時に特定の要素型に合わせて式を調整することができます。
Expression(Of TDelegate) の構築
LINQ メソッドのいずれかに渡す式を構築する場合、実際には Expression(Of TDelegate) のインスタンスを構築します。TDelegate
は、Func(Of String, Boolean)
、Action
、カスタム デリゲート型などの、何らかのデリゲート型です。
Expression(Of TDelegate) は、次のような完全なラムダ式を表す LambdaExpression から継承されます。
Dim expr As Expression(Of Func(Of String, Boolean)) = Function(x As String) x.StartsWith("a")
LambdaExpression には次の 2 つのコンポーネントがあります。
- パラメーター リスト —
(x As String)
— Parameters プロパティによって表されます。 - 本文 —
x.StartsWith("a")
— Body プロパティによって表されます。
Expression(Of TDelegate) を構築する基本的な手順は次のとおりです。
Parameter ファクトリ メソッドを使用して、ラムダ式内の各パラメーター (存在する場合) に ParameterExpression オブジェクトを定義する。
Dim x As ParameterExpression = Parameter(GetType(String), "x")
定義した ParameterExpression と、Expression のファクトリ メソッドを使用して、LambdaExpression の本体を作成します。 たとえば、
x.StartsWith("a")
を表す式はこのように構築できます。Dim body As Expression = [Call]( x, GetType(String).GetMethod("StartsWith", {GetType(String)}), Constant("a") )
適切な Lambda ファクトリ メソッドのオーバーロードを使用して、コンパイル時に型指定される Expression(Of TDelegate) にパラメーターと本文をラップします。
Dim expr As Expression(Of Func(Of String, Boolean)) = Lambda(Of Func(Of String, Boolean))(body, x)
次のセクションでは、LINQ メソッドに渡す Expression(Of TDelegate) を構築することが望ましいシナリオについて説明し、ファクトリ メソッドを使用してそれを行う方法の完全な例を示します。
シナリオ
たとえば、複数のエンティティ型があるとします。
Public Class Person
Property LastName As String
Property FirstName As String
Property DateOfBirth As Date
End Class
Public Class Car
Property Model As String
Property Year As Integer
End Class
これらのいずれかのエンティティ型については、string
フィールドの 1 つに特定のテキストが含まれているエンティティのみをフィルター処理して返す必要があります。 Person
については、FirstName
と LastName
のプロパティを検索する必要があります。
' Dim term = ...
Dim personsQry = (New List(Of Person)).AsQueryable.
Where(Function(x) x.FirstName.Contains(term) OrElse x.LastName.Contains(term))
しかし、Car
については、Model
プロパティのみを検索する必要があります。
' Dim term = ...
Dim carsQry = (New List(Of Car)).AsQueryable.
Where(Function(x) x.Model.Contains(term))
IQueryable(Of Person)
用にカスタム関数を 1 つと、IQueryable(Of Car)
用にもう 1 つを記述することもできますが、次の関数では、特定の要素型に関係なく、このフィルターを既存のすべてのクエリに追加します。
例
' Imports System.Linq.Expressions.Expression
Function TextFilter(Of T)(source As IQueryable(Of T), term As String) As IQueryable(Of T)
If String.IsNullOrEmpty(term) Then Return source
' T is a compile-time placeholder for the element type of the query
Dim elementType = GetType(T)
' Get all the string properties on this specific type
Dim stringProperties As PropertyInfo() =
elementType.GetProperties.
Where(Function(x) x.PropertyType = GetType(String)).
ToArray
If stringProperties.Length = 0 Then Return source
' Get the right overload of String.Contains
Dim containsMethod As MethodInfo =
GetType(String).GetMethod("Contains", {GetType(String)})
' Create the parameter for the expression tree --
' the 'x' in 'Function(x) x.PropertyName.Contains("term")'
' The type of the parameter is the query's element type
Dim prm As ParameterExpression =
Parameter(elementType)
' Generate an expression tree node corresponding to each property
Dim expressions As IEnumerable(Of Expression) =
stringProperties.Select(Of Expression)(Function(prp)
' For each property, we want an expression node like this:
' x.PropertyName.Contains("term")
Return [Call]( ' .Contains(...)
[Property]( ' .PropertyName
prm, ' x
prp
),
containsMethod,
Constant(term) ' "term"
)
End Function)
' Combine the individual nodes into a single expression tree node using OrElse
Dim body As Expression =
expressions.Aggregate(Function(prev, current) [OrElse](prev, current))
' Wrap the expression body in a compile-time-typed lambda expression
Dim lmbd As Expression(Of Func(Of T, Boolean)) =
Lambda(Of Func(Of T, Boolean))(body, prm)
' Because the lambda is compile-time-typed, we can use it with the Where method
Return source.Where(lmbd)
End Function
TextFilter
関数は IQueryable(Of T) (IQueryable だけでなく) を受け取って返すため、コンパイル時に型指定されるクエリ要素をテキスト フィルターの後にさらに追加できます。
Dim qry = TextFilter(
(New List(Of Person)).AsQueryable,
"abcd"
).Where(Function(x) x.DateOfBirth < #1/1/2001#)
Dim qry1 = TextFilter(
(New List(Of Car)).AsQueryable,
"abcd"
).Where(Function(x) x.Year = 2010)
IQueryable の式ツリーにメソッド呼び出しノードを追加する
IQueryable(Of T) の代わりに IQueryable を使用する場合は、汎用 LINQ メソッドを直接呼び出すことはできません。 代替手段の 1 つは、上記のように内部式ツリーをビルドし、リフレクションを使用して、式ツリーに渡すときに適切な LINQ メソッドを呼び出すことです。
LINQ メソッドの呼び出しを表す MethodCallExpression でツリー全体をラップすることにより、LINQ メソッドの機能を複製することもできます。
Function TextFilter_Untyped(source As IQueryable, term As String) As IQueryable
If String.IsNullOrEmpty(term) Then Return source
Dim elementType = source.ElementType
' The logic for building the ParameterExpression And the LambdaExpression's body is the same as in
' the previous example, but has been refactored into the ConstructBody function.
Dim x As (Expression, ParameterExpression) = ConstructBody(elementType, term)
Dim body As Expression = x.Item1
Dim prm As ParameterExpression = x.Item2
If body Is Nothing Then Return source
Dim filteredTree As Expression = [Call](
GetType(Queryable),
"Where",
{elementType},
source.Expression,
Lambda(body, prm)
)
Return source.Provider.CreateQuery(filteredTree)
End Function
この場合、コンパイル時の T
汎用プレースホルダーがないため、コンパイル時に型情報が必要なく、Expression(Of TDelegate) の代わりに LambdaExpression が生成される、Lambda のオーバーロードを使用します。
動的 LINQ ライブラリ
ファクトリ メソッドを使用した式ツリーの構築は比較的複雑です。文字列を作成する方が簡単です。 動的 LINQ ライブラリでは、Queryableで標準 LINQ メソッドに対応する IQueryable の拡張メソッドのセットを公開し、式ツリーではなく特殊な構文で文字列を受け入れます。 ライブラリで文字列から適切な式ツリーが生成され、結果として変換された IQueryable を返すことができます。
たとえば、前の例 (式ツリーの構築を含む) は次のように書き換えることができます。
' Imports System.Linq.Dynamic.Core
Function TextFilter_Strings(source As IQueryable, term As String) As IQueryable
If String.IsNullOrEmpty(term) Then Return source
Dim elementType = source.ElementType
Dim stringProperties = elementType.GetProperties.
Where(Function(x) x.PropertyType = GetType(String)).
ToArray
If stringProperties.Length = 0 Then Return source
Dim filterExpr = String.Join(
" || ",
stringProperties.Select(Function(prp) $"{prp.Name}.Contains(@0)")
)
Return source.Where(filterExpr, term)
End Function
関連項目
.NET