特集:VBプログラマーのためのLINQ超入門(後編)

LINQによるデータベース・アクセスとO/Rマッピング

デジタルアドバンテージ 遠藤 孝信
2009/05/08
Page1 Page2 Page3

■オブジェクト・リレーショナル・デザイナ(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からのVBのソース・コードの生成は、VS 2008に付属のコマンドライン・ツールである「SqlMetal.exe」でも可能です。

 ソリューション・エクスプローラでは、NorthWind.dbmlファイルの配下に「NorthWind.designer.vbファイル」が見えますが、このファイルは.dbmlファイルから自動生成されたVBのソース・コードで、この中には先ほど述べた、NorthWindDataContextクラスとOrdersクラスが定義されています。

 O/Rデザイナにテーブルを追加するなどの変更を加えるたびに、.designer.vbファイルは自動的に修正されます。

■エンティティ・クラスとデータコンテキスト

 自動生成された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と同様にして、次のように記述できます。

Dim records = _
  From n In dc.Orders _
  Join m In dc.Employees On n.EmployeeID Equals m.EmployeeID _
  Where n.ShipCountry = "Norway" _
  Select New With { _
    n.OrderID, m.FirstName, n.OrderDate, n.ShipCountry _
  }
リスト14 LINQ to SQLでJoin句を使用したクエリ

 しかし実は、OrdersテーブルとEmployeesテーブル間のリレーションシップに従って、Ordersクラスには「Employeesプロパティ」が自動的に追加されています。

 このプロパティを経由して、Ordersクラスからそれに関連するEmployeesクラスにアクセスすれば、Join句を使わなくても2つのテーブルを扱うことができます。そのクエリは次のようになります。

Dim records = _
  From n In dc.Orders _
  Where n.ShipCountry = "Norway" _
  Select New With { _
    n.OrderID, n.Employees.FirstName, n.OrderDate, n.ShipCountry _
  }
リスト15 Join句を使用せずに2つのテーブルにアクセスするクエリ

 「n.Employees.FirstName」の部分がポイントです。このような“ドット表記”を用いてリレーションシップをたどることができるため、LINQ to SQLの場合にはJoin句による結合は不要というわけです。

■実際に実行されるSQL文の確認

  LINQ to SQLでは、LINQのクエリがSQL文に変換されると述べましたが、実際にはどのようなSQL文が実行されているのでしょうか。データコンテキストには、Logプロパティが用意されており、これを使うことによって、データベースに送られる最終的なSQL文を確認することができます。

 例えばリスト13で、NorthWindDataContextクラスをインスタンス化している行の次に、「dc.Log = Console.Out」を追加し、プログラムを実行してみてください。

Dim dc As 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
Logプロパティを設定した場合のリスト13の実行結果

 少し見にくいですが、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
Logプロパティを設定した場合のリスト11の実行結果

 SQL文にJOIN句が追加されているのが分かります。

■遅延実行されるクエリ

 LINQ to SQLであっても、LINQ to Objectと同様に、クエリは遅延実行されます。つまり、クエリ結果がFor Each文により処理されるときに初めて、SQL文が生成されるということになります。

 それでは、次のようにクエリを2つに分けて記述するとどうなるでしょうか?

Module Module1

  Sub Main()

    ' NorthWindDataContextクラスはVS 2008により自動生成
    Dim dc As New NorthWindDataContext()

    dc.Log = Console.Out

    ' LINQによる問い合わせ
    Dim records1 = _
      From n In dc.Orders _
      Where n.ShipCountry = "Norway" _
      Select n

    ' さらにOrderDateが1997年のレコードのみを抽出
    Dim records2 = _
      From n In records1 _
      Where n.OrderDate.Value.Year = 1997 _
      Select n

    ' 問い合わせ結果の表示
    For Each r In records2
      Console.WriteLine("{0}, {1}, {2}, {3}", _
        r.OrderID, r.EmployeeID, r.OrderDate, r.ShipCountry)
    Next

    Console.ReadLine()
  End Sub

End Module
リスト16 2つの連続したクエリの記述例

 安心してください。この実行結果は次のようになります。

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
リスト16の実行結果

 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(Of T)インターフェイスを実装しているオブジェクトです(データコンテキストはこのインターフェイスを実装しています)。

 Queryableクラスには、Enumerableクラスと同様に、標準クエリ演算子として一連のメソッドが用意されていますが、大きく異なるのは、WhereメソッドやSelectメソッドなどの引数の型です。Enumerableクラスのメソッドはデリゲートを引数として受け取るのに対し、Queryableクラスのメソッドの引数は「Expression(Of TDelegate)型」となっています。

 加えて、VB 9.0のコンパイラには、ラムダ式がExpression(Of TDelegate)型の変数に割り当てられた場合には、「式ツリー」を出力するという仕様があります。式ツリーというのは、ラムダ式を式の要素に分解し、ツリー状のデータ構造として格納したもので、これによりラムダ式の内容をプログラムで変更したり、チェックしたりできるようになります。

 結局、WhereメソッドやSelectメソッドは、式ツリーを受け取ることになります。そして、例えばWhereメソッドでは、受け取った式ツリーにいくつかのノードを追加して、検索条件全体を表す式ツリーを作ります。同様にして、クエリ内のほかの句(メソッド)も式ツリーを作ります。そして最終的に、 LINQのクエリは大きな1つの式ツリーにその姿を変えます。イメージとしては次の図のようになります。


図8 LINQのクエリから作成された式ツリー

 式ツリーは、クエリのエッセンスを抽出した、問い合わせの中立的な表現といえます。LINQ to SQLでは、このようにして出来上がった式ツリーからSQL文が作成されるわけです。

 VS 2008に付属するサンプル・プログラム*6には、実際に式ツリーの内容を確認するための「Expression Tree Visualizer」というVS 2008用のプラグインが含まれています。これを使うと、例えば次のリスト17のようなクエリは、図9のように可視化できます(ただし表記はC#)。

Dim records = _
      From n In dc.Orders _
      Where n.ShipCountry = "Norway" _
      Order By n.EmployeeID _
      Select New With { n.OrderID, n.EmployeeID }
リスト17 クエリのサンプル


図9 リスト17のクエリから生成された式ツリーの内容(一部省略)
画像をクリックするとウィンドウ全体が表示されます。

 式ツリー内容の説明についてはここでは割愛しますが、実際にツリー状に構成されているのが分かります。

*6 C:\Program Files\Microsoft Visual Studio 9.0\Samples\1041にあるVBSamples.zipに含まれています。

 以上、LINQ超入門ということで、LINQをマスターするのにまず必要となるであろう基本事項について解説してきました。いまだに発展を続ける.NETのデータ・アクセス技術ですが、言語に多くの機能拡張を行ってまで導入されたLINQは、その中心に位置する技術要素だと思われます。本稿がVBでLINQを学ぶ際の第一歩となれば幸いです。End of Article

 

 INDEX
  VBプログラマーのためのLINQ超入門(前編)
  LINQ(リンク)の基礎知識
    1.ADO.NET+SQL文によるデータベースへの問い合わせ
    2.LINQによるデータベース/コレクションへの問い合わせ
    3.クエリの実行を支えるさまざまなLINQプロバイダ/LINQの書き方
    4.そのほかのポイント/LINQPadの紹介
 
  VBプログラマーのためのLINQ超入門(後編)
  LINQによるデータベース・アクセスとO/Rマッピング
    1.LINQのメソッド構文
    2.データベースの問い合わせを行うLINQ to SQL
  3.O/RマッピングとLINQ to SQL


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間