LINQにも色々
LINQからSQLへの変換について説明するにあたり、LINQのバリエーションについて少し解説しておきます。第1回で説明したとおり、LINQの対象は、配列・コレクション・データベース・XMLなど多岐にわたっています。これまでLINQとしてひとくくりに扱ってきましたが、クエリ対象によって幾らかのバリエーションがあります(表1)。
LINQの種類 | クエリ対象 |
---|---|
LINQ to Objects | 配列・コレクションなど、任意のオブジェクト |
LINQ to Entities | Entity Framework経由でデータベース(SQL Server/Oracle/MySQL/SQLite等) |
LINQ to XML | XML |
LINQ to SQL | SQL Server |
過去2回のデータベースプログラミングで解説してきたのは、Entity Framework経由でデータベースにアクセスするLINQ to Entitiesです。XMLを対象とするLINQ to XMLはやや毛色が異なるため本連載では扱いません。また、LINQ to SQLは、Entity Framework登場以前に使われていたSQL Server専用の機能のため、こちらも割愛します。
SQLに変換されないモノ
さて、LINQについて紹介する記事では、配列やコレクションなどをクエリ対象とするLINQ to Objectsを使って解説されることが多いのですが、実はLINQ to ObjectsとLINQ to Entitiesには違いがあり、LINQ to Objectsでは普通に動くLINQが、LINQ to Entitiesでは動かない、というケースがしばしば生じます。
たとえば、リスト1はLINQ to Objectsで、文字列長が6かどうかでフィルタするサンプルです。
//LINQ to Objects クエリ対象文字列配列 string[] names = { "Doi", "Tanaka", "Nakamura", "Saitou", "Yamada" }; //①文字列長6のものだけ取り出す var query = names.Where(x => x.Length == 6); //出力結果:Tanaka,Saitou,Yamada Console.WriteLine(string.Join(",", query)); //②条件をIsLength6メソッドに置き換えてみる query = names.Where(x => IsLength6(x)); //出力結果は変わらない Console.WriteLine(string.Join(",", query)); //文字列の長さが6かどうかを判別するメソッド private bool IsLength6(string str) { return str.Length == 6; }
①では普通にラムダ式を使っているのに対し、②では自分で定義したIsLength6メソッドを使っています。「文字列の長さが6かどうか」という検索条件は変わりませんので、当然出力結果は同じです。
続いて同じようにLINQ to Entitiesで書いてみましょう(リスト2)。
//①シンプルに文字列長でフィルタ products = context.Products.Where(x => x.Name.Length == 6); DumpProducts(products); //問題無く出力される //②IsLength6メソッドを使う products = context.Products.Where(x => IsLength6(x.Name)); DumpProducts(products); //例外発生!!!
今度は不可解な結果となりました。①のWhereメソッドは「WHERE 6 = ( CAST(LEN([Extent1].[Name]) AS int))」のようなWHERE文の付いたSQLに正しく変換され、その後の処理にも問題はありません。一方自分で定義したIsLength6メソッドを使った②では、System.NotSupportedException例外が発生します。①と②は処理内容としてはそれほど変わったように見えないのですが、何が違うのでしょうか?
NotSupportedException例外のメッセージを見ると、「メソッド 'Boolean IsLength6(System.String)' は LINQ to Entities では認識されないため、ストア式に変換できません。」とあります。やはりIsLength6というメソッドがLINQ to Entitiesでは認識されず、使用することができないようです。
「LINQ to Objectsで動くのに、LINQ to Entitiesで動かないなんて……」と気を落とすことはありません。この違いは、図1のように、LINQ to ObjectsとLINQ to Entitiesでは、LINQが実際に実行される方法の違いによって発生します。
LINQ to Objectsでは、LINQがそのまま、C#のプログラムと同様に実行されます。そのため、LINQの中から自分が定義したメソッドなどを自由に呼び出すことができます。一方、LINQ to Entitiesでは、LINQはSQLに変換され、データベース上で実行されます。そのため、元々のC#プログラム内で定義したメソッドなどをLINQの中から呼び出すことはできません。つまり「LINQ to Entitiesで使用する機能はすべてSQLに変換可能でなければならない」のです。仕組みを考えてみると当たり前ではあるのですが、LINQに慣れて、色々なクエリを試すようになると、引っかかりやすいポイントでもあります。