前回、.NETにはQueryableクラスがあり、Enumerableクラスの代わりに使用されることで、クエリの条件式をExpressionクラス経由で動的に判断できることを示した。
Queryableクラスには、Enumerableクラスで定義されている様々な拡張メソッドが大体同じように定義されている。今までの連載で、LINQ 拡張メソッドの実装方法例を数多く紹介してきた。Queryableクラスの拡張メソッドは、IQueryableを拡張するという違いを除けば、IEnumerableの拡張メソッドと殆ど変わる事は無い。
本当にそうだろうか?
まず第一に、Funcなどのデリゲートで受けていた条件式は、Expressionクラスで受ける必要があるはずだ。そして、例えば拡張する対象がLINQ to SQLなら、SQL文として成立させなければならない。独自の拡張メソッドを、IQueryableインターフェイス向けに実装したとしよう:
public static class LinqExtensions { // 要素の個数を数える(LINQ to Objects) public static IEnumerable<T> QCount<T>( this IEnumerable<T> enumerable, Func<T, bool> predict) { var count = 0; foreach (var value in enumerable) { // 条件が一致する場合だけカウントする if (predict(value) == true) { count++; } } return count; } // 要素の個数を数える(LINQ to SQL / LINQ to Entities) public static IQueryable<T> QCount<T>( this IQueryable<T> queryable, Expression<Func<T, bool>> predict) { var count = 0; foreach (var value in queryable) { // 条件が一致する場合だけカウントする if (predict(value) == true) // <-- 構文エラー { count++; } } return count; } }
このコード例には2つの問題がある。第一に、IQueryableのバージョンでは、要素数を数える処理がSQL文になっていないことだ。LINQ to SQLやLINQ to Entitiesでは、クエリの結果がSQL文としてSQL Server等のRDBシステム上で実行されなければならないが、この実装は手元のCLR上でループを実行しているだけで、見ての通りSQL文はどこにも存在しない。
第二に、条件式を示すpredictがデリゲートではなくExpressionであるため、直接呼び出すことは出来ない(構文エラーとなる)。Expressionは自分で解析して、式がどのようになっているかを把握しなければならない。このQCountをSQL文で表現したとすると、以下のようになるはずだ。
// QCountがSQL文に変換されたとして想定される、擬似的なSQL文 SELECT COUNT(*) FROM [Customer] WHERE predict(row) = TRUE
この擬似SQL文を見ると更に大きな問題がある事が分かる。このSQL文がSQL Serverに送信されて実行される時、predictの呼び出しで、どこのメソッドが呼び出されるのだろうか?.NET側に記述したラムダ式が実行されるのだろうか?SQL Serverからのリモートコールバックで実行されるのか?それはそれは壮大な仕掛けだ。
… そんなわけはない。
第一、仮にそのように動作したとしても、毎行のWHERE評価の度にコールバック呼び出しが発生する事になる。非現実的な遅さだ。では実際にはどこで実行されるのだろうか?
この答えは、「そもそも想定が間違っている」だ。まず、predictはデリゲートではないので、Expressionを自力で評価する必要がある。「評価した式がSQL文に変換可能」であれば、SQL文としてRDBに送信出来ることになる。変換不能であれば、クエリを実行する事は出来ない。それは「実行時例外」という形で表現されることになる。例を示そう:
// 正規表現でマッチングを試みる var regex = new Regex("[a-zA-Z]*"); // LINQ to SQLのコンテキスト using (var context = new AdventureWorksDataContext()) { // LINQ to Objectsなら問題ないが? var customers = from customer in context.Customer where regex.IsMatch(customer.FirstName) == true // 正規表現にマッチすれば select customer; }
Regexクラスは、当然.NET固有のクラスだ。Regex.IsMatchに相当するSQL構文は存在しない。だからこのLINQクエリは、SQL文で「表現できない」。表現できないので、実行しようとすると例外が発生する。
つまり、IQueryableの拡張メソッドは、そのメソッドが意味するところをSQL文に変換出来なければならない。或は逆だ。SQL文で表現可能であれば、その意味するところを拡張メソッドに出来る可能性がある、と。