LINQによるデータベース・アクセスとO/Rマッピング:特集:C#プログラマーのためのLINQ超入門(後編)(3/3 ページ)
LINQ(リンク)でDBを検索するためのLINQ to SQLと、そのベースとなるO/Rマッピング機能について分かりやすく解説。
■オブジェクト・リレーショナル・デザイナ(O/Rデザイナ)
先ほどVS 2008上で利用したO/Rデザイナは、O/Rマッピングの際に使用されるクラスを定義するためのツールです。
O/Rマッピングでは、検索結果のレコードをオブジェクトにマッピングするために、テーブルと同じ構造を持つクラスを定義する必要がありますが、O/Rデザイナでは、サーバ・エクスプローラからドラッグ&ドロップされたテーブルからクラスを自動生成することができます。
先の図3では、ドラッグ&ドロップによりOrdersテーブルがO/Rデザイナ上に配置されましたが、実はこれはOrdersクラスの構造を示すクラス図(クラス・ダイアグラム)です。
最初から順に説明していきましょう。
まず「LINQ to SQLクラス」をプロジェクトに追加しましたが、これによりプロジェクトには「.dbmlファイル」が追加されます。このファイルは実際にはO/Rデザイナでデザインした内容を記録するXMLファイルです。O/Rデザイナはこのファイルを編集するためのGUIツール*5といえます。
*5 データベースのテーブルからの.dbmlファイルの作成や、(次に述べている).dbmlからのC#のソース・コードの生成は、VS 2008に付属のコマンドライン・ツールである「SqlMetal.exe」でも可能です。
ソリューション・エクスプローラでは、NorthWind.dbmlファイルの配下に「NorthWind.designer.csファイル」が見えますが、このファイルは.dbmlファイルから自動生成されたC#のソース・コードで、この中には先ほど述べた、NorthWindDataContextクラスとOrdersクラスが定義されています。
O/Rデザイナにテーブルを追加するなどの変更を加えるたびに、.designer.csファイルは自動的に修正されます。
■エンティティ・クラスとデータコンテキスト
自動生成された2つのクラスのうち、Ordersクラスは、Ordersテーブルから取得したレコードの値を入れるためのものです。このような、テーブルに対応したクラスは「エンティティ・クラス」とも呼ばれます(「Entity」は「実体」という意味)。
ここでは詳しくは述べませんが、エンティティ・クラスの定義にはTable属性が付けられます。また、エンティティ・クラスには、基となったテーブルの列がプロパティとして定義されますが、これらのプロパティにはColumn属性が付けられます。
そして、もう一方のNorthWindDataContextクラスは、「データコンテキスト」と呼ばれます。データコンテキストは、LINQ to SQLを利用する際のデータベースへの接続から、SQL文の実行、エンティティ・クラスのインスタンス化、そのインスタンスの管理などを行います。
リスト13を見ても分かるように、データコンテキストがLINQのクエリとデータベース・アクセスをつなぐ唯一のポイントとなります。
■join句が不要となるLINQ to SQLのクエリ
O/Rマッピングにより作成されたオブジェクトは、異なるテーブルのデータ同士がプロパティを介して関連付けられています。そのようなオブジェクトの構造は、複数のテーブルを扱うクエリを実行するときに、非常に便利に働きます。
ということで、先ほどリスト11で行ったjoin句を使ったクエリを、今度はLINQ to SQLで行ってみましょう。
まず、ソリューション・エクスプローラでNorthWind.dbmlをダブルクリックして、再度O/Rデザイナを開きます。そしてEmployeesテーブルを追加します。
図7 O/RデザイナでEmployeesテーブルを追加
OrdersクラスとEmployeesクラス間には関連付けを示す矢印が自動的に付けられる。Employeesクラスが自身を指す矢印は、ReportsTo列により別のEmployeesレコードを参照していることを示している。
さて、クエリは、リスト11と同様にして、次のように記述できます。
var records =
from n in dc.Orders
join m in dc.Employees on n.EmployeeID equals m.EmployeeID
where n.ShipCountry == "Norway"
select new {
n.OrderID, m.FirstName, n.OrderDate, n.ShipCountry
};
しかし実は、OrdersテーブルとEmployeesテーブル間のリレーションシップに従って、Ordersクラスには「Employeesプロパティ」が自動的に追加されています。
このプロパティを経由して、Ordersクラスからそれに関連するEmployeesクラスにアクセスすれば、join句を使わなくても2つのテーブルを扱うことができます。そのクエリは次のようになります。
var records =
from n in dc.Orders
where n.ShipCountry == "Norway"
select new {
n.OrderID, n.Employees.FirstName, n.OrderDate, n.ShipCountry
};
「n.Employees.FirstName」の部分がポイントです。このような“ドット表記”を用いてリレーションシップをたどることができるため、LINQ to SQLの場合にはjoin句による結合は不要というわけです。
■実際に実行されるSQL文の確認
LINQ to SQLでは、LINQのクエリがSQL文に変換されると述べましたが、実際にはどのようなSQL文が実行されているのでしょうか。データコンテキストには、Logプロパティが用意されており、これを使うことによって、データベースに送られる最終的なSQL文を確認することができます。
例えばリスト13で、NorthWindDataContextクラスをインスタンス化している行の次に、「dc.Log = Console.Out;」を追加し、プログラムを実行してみてください。
NorthWindDataContext dc = new NorthWindDataContext();
dc.Log = Console.Out;
プログラムを実行すると次のような内容が出力されるはずです。
SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry]
FROM [dbo].[Orders] AS [t0]
WHERE [t0].[ShipCountry] = @p0
-- @p0: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Norway]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
10387, 1, 1996/12/18 0:00:00, Norway
10520, 7, 1997/04/29 0:00:00, Norway
10639, 7, 1997/08/20 0:00:00, Norway
10831, 3, 1998/01/14 0:00:00, Norway
10909, 1, 1998/02/26 0:00:00, Norway
11015, 2, 1998/04/10 0:00:00, Norway
少し見にくいですが、SELECT文が1つ含まれているのが分かります。
また、クエリをリスト15に変更した場合の出力は次のようになります。この結果はリスト14のクエリでもまったく同様です。
SELECT [t0].[OrderID], [t1].[FirstName], [t0].[OrderDate], [t0].[ShipCountry]
FROM [dbo].[Orders] AS [t0]
LEFT OUTER JOIN [dbo].[Employees] AS [t1] ON [t1].[EmployeeID] = [t0].[EmployeeID]
WHERE [t0].[ShipCountry] = @p0
-- @p0: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Norway]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
10387, Nancy, 1996/12/18 0:00:00, Norway
10520, Robert, 1997/04/29 0:00:00, Norway
10639, Robert, 1997/08/20 0:00:00, Norway
10831, Janet, 1998/01/14 0:00:00, Norway
10909, Nancy, 1998/02/26 0:00:00, Norway
11015, Andrew, 1998/04/10 0:00:00, Norway
SQL文にJOIN句が追加されているのが分かります。
■遅延実行されるクエリ
LINQ to SQLであっても、LINQ to Objectと同様に、クエリは遅延実行されます。つまり、クエリ結果がforeach文により処理されるときに初めて、SQL文が生成されるということになります。
それでは、次のようにクエリを2つに分けて記述するとどうなるでしょうか?
using System;
using System.Linq;
using System.Collections.Generic;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// NorthWindDataContextクラスはVS 2008により自動生成
NorthWindDataContext dc = new NorthWindDataContext();
dc.Log = Console.Out;
// LINQによる問い合わせ
var records1 =
from n in dc.Orders
where n.ShipCountry == "Norway"
select n;
// さらにOrderDateが1997年のレコードのみを抽出
var records2 =
from n in records1
where n.OrderDate.Value.Year == 1997
select n;
// 問い合わせ結果の表示
foreach (var r in records2)
{
Console.WriteLine("{0}, {1}, {2}, {3}",
r.OrderID, r.EmployeeID, r.OrderDate, r.ShipCountry);
}
Console.ReadLine();
}
}
}
安心してください。この実行結果は次のようになります。
SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate], [t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight], [t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion], [t0].[ShipPostalCode], [t0].[ShipCountry]
FROM [dbo].[Orders] AS [t0]
WHERE (DATEPART(Year, [t0].[OrderDate]) = @p0) AND ([t0].[ShipCountry] = @p1)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1997]
-- @p1: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Norway]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
10520, 7, 1997/04/29 0:00:00, Norway
10639, 7, 1997/08/20 0:00:00, Norway
SELECT文のWHERE句では、2つのLINQのクエリにおけるフィルタ条件がANDで1つにまとめられているのが分かります。日時データから年部分を取り出すために、T-SQLのDATEPARTという関数が使われているのもいい感じです。
■Queryableクラスのメソッドと式ツリー
最後に、LINQ to SQLのクエリは内部的にどのように処理されるのかについて、簡単に触れておきます。
LINQ to SQLでの処理は、前回および今回の前半で見てきたLINQ(LINQ to Object)とは大きく異なります。なぜなら、LINQ to SQLの場合には、最終的に先ほど見たような1つのSQL文を作り上げなければならないためです。
LINQ to SQLでも、where句やselect句はWhereメソッドやSelectメソッドに置き換えられます。しかし、LINQ to SQLの場合には、Enumerableクラスではなく、System.Linq名前空間のQueryableクラスで定義されているメソッドが使用されます。これらのメソッドもすべて拡張メソッドであり、その拡張の対象は、同じ名前空間のIQueryable<T>インターフェイスを実装しているオブジェクトです(データコンテキストはこのインターフェイスを実装しています)。
Queryableクラスには、Enumerableクラスと同様に、標準クエリ演算子として一連のメソッドが用意されていますが、大きく異なるのは、WhereメソッドやSelectメソッドなどの引数の型です。Enumerableクラスのメソッドはデリゲートを引数として受け取るのに対し、Queryableクラスのメソッドの引数は「Expression<TDelegate>型」となっています。
加えて、C#コンパイラには、ラムダ式がExpression<TDelegate>型の変数に割り当てられた場合には、「式ツリー」を出力するという仕様があります。式ツリーというのは、ラムダ式を式の要素に分解し、ツリー状のデータ構造として格納したもので、これによりラムダ式の内容をプログラムで変更したり、チェックしたりできるようになります。
結局、WhereメソッドやSelectメソッドは、式ツリーを受け取ることになります。そして、例えばWhereメソッドでは、受け取った式ツリーにいくつかのノードを追加して、検索条件全体を表す式ツリーを作ります。同様にして、クエリ内のほかの句(メソッド)も式ツリーを作ります。そして最終的に、LINQのクエリは大きな1つの式ツリーにその姿を変えます。イメージとしては次の図のようになります。
式ツリーは、クエリのエッセンスを抽出した、問い合わせの中立的な表現といえます。LINQ to SQLでは、このようにして出来上がった式ツリーからSQL文が作成されるわけです。
VS 2008に付属するサンプル・プログラム*6には、実際に式ツリーの内容を確認するための「Expression Tree Visualizer」というVS 2008用のプラグインが含まれています。これを使うと、例えば次のリスト17のようなクエリは、図9のように可視化できます。
var records =
from n in dc.Orders
where n.ShipCountry == "Norway"
orderby n.EmployeeID
select new { n.OrderID, n.EmployeeID };
式ツリー内容の説明についてはここでは割愛させていただきますが、実際にツリー状に構成されているのが分かります。
*6 C:\Program Files\Microsoft Visual Studio 9.0\Samples\1041にあるCSharpSamples.zipに含まれています。
以上、LINQ超入門ということで、LINQをマスターするのにまず必要となるであろう基本事項について解説してきました。いまだに発展を続ける.NETのデータ・アクセス技術ですが、言語に多くの機能拡張を行ってまで導入されたLINQは、その中心に位置する技術要素だと思われます。本稿がLINQを学ぶ際の第一歩となれば幸いです。
Copyright© Digital Advantage Corp. All Rights Reserved.