【実務・中級編】上級プロフェッショナル向け:VB.NETにおける「Expression(Of TDelegate)」を用いた動的クエリ構築:実行時の条件変化に柔軟に対応するLINQ式ツリーの生成と最適化 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

上級プロフェッショナル向け:VB.NETにおける「Expression(Of TDelegate)」を用いた動的クエリ構築

こんにちは、チーフアーキテクトの私だ。
開発現場でよく目にする悪夢がある。ユーザーの入力フォームの組み合わせによって、膨大な`If`文を書き連ね、文字列を連結して動的なクエリやSQLを組み立てるコードだ。

「入力があれば条件を追加する」――その場しのぎの文字列結合は、SQLインジェクションの温床であり、保守フェーズに入った途端に誰も触れないスパゲッティコードへと変貌する。

VB.NETには、この悪夢を根絶し、型安全(Type-Safe)を完全に維持しながら、実行時に自在にLINQのクエリを構築する至高の武器が存在する。それが `Expression(Of TDelegate)`(式ツリー) だ。

今回は、業務システムで避けて通れない「複雑な動的検索条件の構築」を、パフォーマンスと堅牢性を極限まで高めた状態で実装するプロフェッショナルな手法を伝授する。

なぜ文字列連結や泥臭い`If`分岐は悪なのか?

業務システムの検索画面で、以下のような要件を実装したことはないだろうか?

  • 「顧客名」が入力されたら条件追加
  • 「登録日(開始〜終了)」が指定されたら条件追加
  • 「ステータス」が選択されていたら条件追加

これらを愚直にやると、こうなる。

‘ 【アンチパターン】絶対にやってはいけない動的クエリ構築
Dim query = db.Customers.AsQueryable()

If Not String.IsNullOrEmpty(txtName.Text) Then
query = query.Where(Function(c) c.Name.Contains(txtName.Text))
End If

If chkActiveOnly.Checked Then
query = query.Where(Function(c) c.IsActive = True)
End If
‘ 条件が10個に増えたら? 組み合わせは爆発し、デバッグは地獄と化す。

このアプローチは、小規模なら動く。しかし、データベースプロバイダ(Entity Frameworkなど)が背後でどう解釈するか、そしてパフォーマンスの最適化という観点において、上級エンジニアとしては失格の烙印を押ざるを得ない。

ここで求められるのは、「実行時の入力値に応じて、ラムダ式そのものをコードから組み立ててコンパイルする」というメタプログラミング手法、すなわち Expression Trees の活用だ。

Expression(Of TDelegate) とは何か?

通常、VB.NETのラムダ式(`Function(x) x.Age > 20`)は、コンパイルされるとIL(中間言語)のデリゲートとなり、そのまま実行される。

しかし、型を `Expression(Of Func(Of T, Boolean))` として受け取る、あるいは明示的に式ツリーとして定義すると、VB.NETコンパイラはそれを「コードのデータ構造(抽象構文木:AST)」としてメモリ上に保持する。

これにより、以下の離れ業が可能になる:
1. コードを実行前にプログラム側で解析・改変できる。
2. Entity FrameworkなどのORMに渡した際、SQLの `WHERE` 句へと完璧に翻訳される。
3. リフレクションに頼らず、完全に型安全な状態でプロパティやメソッド呼び出しを動的に構築できる。

実践:堅牢な動的クエリビルダーの実装

ここからが本題だ。実際の業務アプリケーション(Entity Framework Coreを想定)で即座に使える、動的条件構築クラスのプロダクションコードを提示する。

ユーザーが入力した複数の検索条件を、安全かつエレガントに合成(AND結合)するエンジニアリングを見てほしい。

Imports System.Linq.Expressions
Imports System.Reflection

Namespace Enterprise.Data.Querying

”’

”’ 型安全な動的LINQ式ツリーを構築するためのビルダー
”’

”’ 対象となるエンティティの型
Public Class DynamicQueryBuilder(Of T)

Private _predicate As Expression(Of Func(Of T, Boolean))
Private _parameter As ParameterExpression

Public Sub New()
‘ ラムダ式のパラメータを定義 (例: Function(x) …)
_parameter = Expression.Parameter(GetType(T), “x”)

‘ 初期状態は常にTrueを返す式 (1 = 1 のような状態)
_predicate = Expression.Lambda(Of Func(Of T, Boolean))(Expression.Constant(True), _parameter)
End Sub

”’

”’ 指定したプロパティが値と等しい条件を追加 (AND結合)
”’

Public Function AndEquals(propertyName As String, value As Object) As DynamicQueryBuilder(Of T)
If value Is Nothing Then Return Me ‘ 値がなければ何もしない

Dim propExpression = Expression.Property(_parameter, propertyName)
Dim valExpression = Expression.Constant(value, propExpression.Type)

‘ x.[propertyName] = [value] を構築
Dim equalsExpression = Expression.Equal(propExpression, valExpression)

Return Combine(equalsExpression, ExpressionType.AndAlso)
End Function

”’

”’ 指定した文字列プロパティが部分一致する条件を追加 (AND結合)
”’

Public Function AndContains(propertyName As String, keyword As String) As DynamicQueryBuilder(Of T)
If String.IsNullOrWhiteSpace(keyword) Then Return Me

Dim propExpression = Expression.Property(_parameter, propertyName)

‘ プロパティがString型であることを確認
If propExpression.Type <> GetType(String) Then
Throw New InvalidOperationException(“Contains can only be applied to string properties.”)
End If

‘ string.Contains(string) メソッドの情報を取得
Dim containsMethod = GetType(String).GetMethod(“Contains”, New Type() {GetType(String)})
Dim valExpression = Expression.Constant(keyword, GetType(String))

‘ x.[propertyName].Contains([keyword]) を構築
Dim callExpression = Expression.Call(propExpression, containsMethod, valExpression)

Return Combine(callExpression, ExpressionType.AndAlso)
End Function

”’

”’ 内部的に式を結合するプライベートメソッド
”’

Private Function Combine(newExpression As Expression, restrictionType As ExpressionType) As DynamicQueryBuilder(Of T)
‘ 既存のボディと新しい式をvisitorパターン的に置換して結合
Dim visitor = New SubstitutionVisitor(_parameter, _parameter)
Dim left = _predicate.Body
Dim right = visitor.Visit(newExpression)

Dim combinedBody As BinaryExpression = Nothing
If restrictionType = ExpressionType.AndAlso Then
combinedBody = Expression.AndAlso(left, right)
ElseIf restrictionType = ExpressionType.OrElse Then
combinedBody = Expression.OrElse(left, right)
End If

_predicate = Expression.Lambda(Of Func(Of T, Boolean))(combinedBody, _parameter)
Return Me
End Function

”’

”’ 完成した式ツリーを取得する
”’

Public Function Build() As Expression(Of Func(Of T, Boolean))
Return _predicate
End Function

End Class

‘ パラメータのバインドを維持するためのExpressionVisitorヘルパー
Friend Class SubstitutionVisitor
Inherits ExpressionVisitor
Private ReadOnly _oldParam As ParameterExpression
Private ReadOnly _newParam As ParameterExpression

Public Sub New(oldParam As ParameterExpression, newParam As ParameterExpression)
_oldParam = oldParam
_newParam = newParam
End Sub

Protected Overrides Function VisitParameter(node As ParameterExpression) As Expression
Return If(node Is _oldParam, _newParam, MyBase.VisitParameter(node))
End Function
End Class

End Namespace

現場でどう使うか? 圧倒的な優位性

上記の `DynamicQueryBuilder` を用いると、画面側のコードは以下のように美しく、かつ強固になる。

Imports Enterprise.Data.Querying

Public Class CustomerSearchService

Public Function SearchCustomers(searchName As String, targetStatus As Nullable(Of Integer), onlyVip As Boolean)

‘ ビルダーのインスタンス化
Dim builder = New DynamicQueryBuilder(Of Customer)()

‘ 条件を流暢に(Fluentに)追加
builder.AndContains(“Name”, searchName) _
.AndEquals(“Status”, targetStatus) _
.AndEquals(“IsVip”, onlyVip)

‘ データベースコンテキストへ適用
Using db As New AppDbContext()
‘ 型安全かつ遅延実行されるクエリとしてDBMSへ送信される
Dim result = db.Customers.Where(builder.Build()).ToList()
Return result
End Using

End Function

End Class

この設計がもたらす圧倒的なメリット

1. SQLインジェクションの完全防御: パラメータはすべて `Expression.Constant` を経由してSQLのパラメータ(`@p0` 等)にバインドされるため、文字列連結による脆弱性が原理的に発生しない。
2. パフォーマンスの最大化: クエリはメモリ上で文字列結合されるのではなく、抽象構文木として構築された後、Entity Frameworkによって最適化されたSQLに一発で変換される。無駄なラウンドトリップやインデックススキャンを防げる。
3. 抜群の保守性: プロパティ名を文字列で指定するためリファクタリング時に注意は必要だが、条件追加のロジックが完全にカプセル化されており、新しい検索条件(例:範囲検索、大小比較など)の拡張が非常に容易。

旨味だけのコードを書き捨て、後任のプログラマーに負債を残す時代は終わった。
VB.NETの表現力と.NETのメタプログラミングの核心を突くことで、コードベースは驚くほど優美で強靭なものに生まれ変わる。

アーキテクトとしての誇りを持ち、真にスケーラブルなコードを君の手で実装してほしい。

タイトルとURLをコピーしました