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

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

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


本稿は、すでに公開されている「特集:C#プログラマーのためのLINQ超入門(後編)LINQによるデータベース・アクセスとO/Rマッピング」を、Visual Basicプログラマー向けに加筆・修正したものです。

 本特集の前編では、次のような簡単なLINQ(リンク)のクエリについて解説しました。

From n In data _
Where n.ShipCountry = "Norway" _
Select n
リスト1 LINQのクエリ

 前編で述べているように、クエリ内のWhere句やSelect句は、コンパイル時にメソッドの呼び出しに変換されます。実際、LINQのクエリ構文をまったく使用せずに、メソッド呼び出しだけで同等の処理を記述できます。このような記述方法は、クエリ構文(query syntax)に対して、メソッド構文(method syntax)と呼ばれます。

 後編となる今回は、リスト1をメソッド構文で書き換えるところから始めます。

LINQのメソッド構文

 とはいっても、リスト1の場合には書き換えは簡単で、これは次のようになります。

data _
.Where(Function(n) n.ShipCountry = "Norway") _
.Select(Function(n) n)
リスト2 リスト1のクエリをメソッド構文で記述

 突然現れた「Function」はVisual Basic 2008(=Visual Basic 9.0。以降、VB 9.0)の新機能である「ラムダ式」を記述するためのもので、これは「Function式」とも呼ばれます(メソッド定義のためのFunctionとは別物です)。これについては後述します。

 リスト2は改行して3行で記述しているので少し分かりにくいですが、このコードの大枠は次のような1行です。

data.Where(……).Select(……)
リスト3 リスト2を1行で記述(引数は省略)

 つまり、変数dataに対してWhereメソッドを呼び出し、さらにその結果に対してSelectメソッドを呼び出しているだけです。

■ラムダ式を匿名メソッドに

 WhereとSelectの2つのメソッドで引数に指定している「Function(…) ……」の部分は、「ラムダ式」と呼ばれるものです。

 ラムダ式とは、簡単にいうと、名前のない関数をシンプルに定義するための記述方法です。例えば、「n = n + 1」という関数自体を、あるメソッドに引数として渡したい場合、これまでは中身が1行だけのメソッドを定義し、そのデリゲートを渡す必要がありました。VB 9.0では、そのようなメソッドを「Function(n) n + 1」というラムダ式で記述でき、これをそのままメソッドに渡すことができます。

 ラムダ式は、機械的にデリゲートに置き換えることができます。リスト2の場合は次のように書き換えることができます。ここでは実際にコンパイルして実行できるように、クエリ以外のコードも示しています。

Class Orders

  Public OrderID As Integer
  Public EmployeeID As Integer
  Public OrderDate As DateTime
  Public ShipCountry As String

  ' コンストラクタ
  Public Sub New(ByVal id As Integer, ByVal emp As Integer, ByVal [date] As DateTime, ByVal ship As String)
    Me.OrderID = id
    Me.EmployeeID = emp
    Me.OrderDate = [date]
    Me.ShipCountry = ship
  End Sub

End Class

Module Module1

  Sub Main()

    ' Ordersオブジェクトの配列
    Dim data As Orders() = New Orders() { _
      New Orders(1000, 1, DateTime.Now, "Norway"), _
      New Orders(1001, 3, DateTime.Now, "Germany"), _
      New Orders(1002, 7, DateTime.Now, "Norway"), _
      New Orders(1003, 7, DateTime.Now, "poland")}

    ' 元のクエリ
    ' Dim records As IEnumerable(Of Orders) = _
    '   From n In data _
    '   Where n.ShipCountry = "Norway" _
    '   Select n

    ' 引数のラムダ式をデリゲートに
    Dim records As IEnumerable(Of Orders) = _
      data _
      .Where(New Func(Of Orders, Boolean)(AddressOf MyDelegate1)) _
      .Select(New Func(Of Orders, Orders)(AddressOf MyDelegate2))

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

    Console.ReadLine()
  End Sub

  ' Whereメソッドで使用される条件
  Function MyDelegate1(ByVal n As Orders) As Boolean
    Return n.ShipCountry = "Norway"
  End Function

  ' Selectメソッドで使用されるクエリ結果構築用メソッド
  Function MyDelegate2(ByVal n As Orders) As Orders
    Return n
  End Function

End Module
リスト4 リスト2のラムダ式部分をデリゲートで記述
このコードを実行するには、Visual Studio 2008でコンソール・アプリケーションのプロジェクトを新規作成し、Module1.vbの内容と置き換える。

 ここではデリゲート型として、汎用的なFunc(Of T1, T2)デリゲート型を使用しています。このデリゲート型については、「.NET TIPS:汎用的に使用できる定義済みのデリゲート型は?」を参照してください。

 ここで少しまとめておきましょう。Whereメソッドはフィルタの役目を果たします。つまり、「data.Where(……)」は配列dataの各要素に対して、指定されたフィルタ条件に合致する要素のみを返します。

 フィルタ条件はどうやって指定するかというと、True/Falseを返すメソッドへのデリゲートをWhereメソッドの引数として渡します。Whereメソッド内部では、判定が必要な個所でそのメソッドが使われます。

 一方、Selectメソッドはクエリの結果の要素を作成するためのものです。クエリ結果の要素となるオブジェクトを作成するメソッドへのデリゲートを引数にして呼び出します。リスト1〜4では、Ordersオブジェクト(変数n)をそのまま返していますが、もちろん何を返すかはプログラマーの自由です。

 リスト4のメソッド構文による記述を、元のクエリ構文(コメントで記述している部分)と見比べてみてください。結局、Where句やSelect句などに記述できる式は、デリゲートとなるメソッドがReturn文で返す式、ということになります。

■匿名型のオブジェクトを返すSelect句

 本題とは少し離れますが、ここでSelect句が返す要素について解説しておきます。

 リスト4までは、Select句はOrdersオブジェクトを返していますが、クエリ結果として、例えばOrderIDとEmployeeIDの値だけを返したいという場合には、どうすればよいでしょうか。

 1つのストレートな方法としては、まず次のようなOrderIDとEmployeeIDだけを持つOrderEmployeeクラスとそのコンストラクタを定義しておき……、

Class OrderEmployee

  Public OrderID As Integer
  Public EmployeeID As Integer

  ' コンストラクタ
  Public Sub New(ByVal id As Integer, ByVal emp As Integer)
    Me.OrderID = id
    Me.EmployeeID = emp
  End Sub

End Class
リスト5 OrderEmployeeクラスの定義

クエリのSelect句の部分を次のように書き換えるという手があります。

Dim records As IEnumerable(Of OrderEmployee) = _
    From n In data _
    Where n.ShipCountry = "Norway" _
    Select New OrderEmployee(n.OrderID, n.EmployeeID)
リスト6 Select句でOrderEmployeeオブジェクトを返すクエリ

 もちろんこれでも目的は達成できますが、いちいちクラスを定義するのは非常に面倒です。そこで、VB 9.0では自動的にクラス(およびコンストラクタ)を定義してくれる「匿名型」という機能が導入されています。

 匿名型を使えば、次のようなクエリを記述するだけでコトが足ります。

Dim records = _
  From n In data _
  Where n.ShipCountry = "Norway" _
  Select New With {.OrderID = n.OrderID, .EmployeeID = n.EmployeeID}
リスト7 匿名型のオブジェクトを返すSelect句

 最後の行の「New With { …… }」の記述が、匿名型を使用している個所です。これだけで、OrderIDとEmployeeIDというプロパティを持つクラスが自動的に定義され、それぞれのプロパティにn.OrderIDとn.EmpolyeeIDの値が代入されたオブジェクトが作成されます。

 ただし、自動的に定義されたクラスのクラス名を知ることはできません。そのため、そのオブジェクトを変数で受け取るには、代入されるオブジェクトの型を自動的に設定してくれる、As句なしの「Dim」が不可欠となります(リスト7の1行目)。

 匿名型は、クエリの結果の作成に必要な、一時的なオブジェクトを作成するために導入された機能であり、As句なしのDimステートメントはそのオブジェクトを受け取るために導入された機能であるというわけです*1

*1 型推論のためのAs句なしDimによるローカル変数の宣言は、ほかの場面でも便利に使えますが、匿名型はLINQのSelect句以外には使い道はなさそうです。

 クラス名も分からずにそのオブジェクトが扱えるのか? と少し心配になりますが、リスト7のクエリの結果へのアクセスは、As句なしのFor Eachステートメントを使うことにより、次のようにして行えます。

For Each r In records
  Console.WriteLine("{0}, {1}", r.OrderID, r.EmployeeID)
Next
リスト8 リスト7のクエリ結果の表示

 For Eachステートメントで使用されている変数rは、これ以前にその型が宣言されているわけではなく、コレクションである変数recordsから推論可能であるため、As句を省略できます。

 ちなみに、リスト7の匿名型オブジェクト作成の部分は、次のようにプロパティ名を省略できます。

Select New With { n.OrderID, n.EmployeeID }

■拡張メソッドであるWhereメソッドとSelectメソッド

 ところで、リスト4では変数dataは単なる配列です。その配列に対してWhereメソッドを呼び出しているわけですが、配列(つまりはSystem名前空間のArrayクラス)にWhereメソッドなんかあったでしょうか?

 実は、Whereメソッドは、「拡張メソッド」と呼ばれるVB 9.0の新機能により実現されています。拡張メソッドは、既存のクラスに対して、クラスの外部からメソッドを追加できる仕組みです。この場合では、配列(Arrayクラス)にWhereメソッドが追加される形となっています。

 実際には、配列が実装しているIEnumerable(Of T)インターフェイス(これはジェネリック・インターフェイスです)に対して、Whereメソッドが追加されています。これにより、IEnumerable(Of T)インターフェイスを実装しているすべてのオブジェクトに対して、Whereメソッドを呼び出すことができます。Selectメソッドも同様です。

 なお、前編でも解説したように、WhereメソッドやSelectメソッドの本体自体はEnumerableクラス(System.Linq名前空間)で定義されています。Enumerableクラスには、グループ化のためのGroupByメソッドや、2つのデータソースを結合するためのJoinメソッドなど、LINQ用の標準クエリ演算子が拡張メソッドとして多数定義されています。

■クエリ構文とメソッド構文、どちらを使う?

 以上のように、LINQでクエリは、クエリ構文(リスト1のパターン)でもメソッド構文(リスト2のパターン)でも記述することができます。気に入った方で記述すればよいでしょう。

 ただし、Where句やSelect句などのようなキーワードがEnumerableクラスのメソッドすべてに対して用意されているわけではないので、両者を混在させるしかない場合もあります。

 例えば、検索結果の件数をカウントするには、EnumerableクラスのCountメソッドを使って次のように記述します。

Dim num As Integer = _
  (From n In data _
  Where n.ShipCountry = "Norway" _
  Select n) _
  .Count()
リスト9 検索結果の件数カウント(クエリ構文+メソッド呼び出し)

 こんなふうになるなら、最初から次のリスト10のように、すべてメソッド呼び出しで記述した方がよいという人も多いかもしれません。

Dim num As Integer = _
  data _
  .Where(Function(n) n.ShipCountry = "Norway") _
  .Select(Function(n) n) _
  .Count()
リスト10 検索結果の件数カウント(メソッド構文)

 しかし例えば、2つのデータソースを結合して検索を行う場合には、Joinメソッドの呼び出しよりも、Join句を使ったクエリ構文の方がすっきり記述できます。

 次のリスト11は、Join句を使用したサンプル・プログラムです。ここでは先ほどのリスト4に、従業員(=employee)のIDと名前からなるEmployeesクラスのインスタンスの配列を追加し、クエリの結果において、従業員IDの代わりに従業員の氏名が表示されるようにしています(いわゆる内部結合です)。

Class Orders

  Public OrderID As Integer
  Public EmployeeID As Integer
  Public OrderDate As DateTime
  Public ShipCountry As String

  Public Sub New(ByVal id As Integer, ByVal emp As Integer, ByVal [date] As DateTime, ByVal ship As String)
    Me.OrderID = id
    Me.EmployeeID = emp
    Me.OrderDate = [date]
    Me.ShipCountry = ship
  End Sub

End Class

Class Employees

  Public EmployeeID As Integer
  Public FirstName As String

  Public Sub New(ByVal EmployeeID As Integer, ByVal name As String)
    Me.EmployeeID = EmployeeID
    Me.FirstName = name
  End Sub

End Class

Module Module1

  Sub Main()

    ' Ordersオブジェクトの配列
    Dim data As Orders() = New Orders() { _
      New Orders(1000, 1, DateTime.Now, "Norway"), _
      New Orders(1001, 3, DateTime.Now, "Germany"), _
      New Orders(1002, 7, DateTime.Now, "Norway"), _
      New Orders(1003, 7, DateTime.Now, "poland")}

    ' Employeesオブジェクトの配列
    Dim emps As Employees() = New Employees() { _
      New Employees(1, "ナンシー"), _
      New Employees(3, "ロバート"), _
      New Employees(7, "ジャネット")}

    ' Join句を使用したクエリ
    Dim records = _
      From n In data _
      Join m In emps On n.EmployeeID Equals m.EmployeeID _
      Where n.ShipCountry = "Norway" _
      Select New With { _
        n.OrderID, m.FirstName, n.OrderDate, n.ShipCountry _
      }

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

    Console.ReadLine()
  End Sub

End Module
リスト11 Join句を使ったサンプル・プログラム

 長くなるため詳しくは説明しませんが、2つの配列に共通するEmployeeIDフィールドを軸に、Orders配列とEmployees配列を結合し、それに対してクエリを行っています。実行結果は次のようになります。

1000, ナンシー, 2008/07/18 3:09:37, Norway
1002, ジャネット, 2008/07/18 3:09:37, Norway
リスト11の実行結果

 しかし、リスト11のクエリを、メソッド構文を使って記述すると次のようになってしまいます。

Dim records = _
    data
    .Join( _
      emps, _
      Function(n) n.EmployeeID, _
      Function(m) m.EmployeeID, Function(order, emp) _
      New With { .order = order, .emp = emp } _
    )
    .Where( _
      Function(n) n.order.ShipCountry = "Norway") _
    .Select( _
      Function(n) New With { _
        n.order.OrderID, n.emp.FirstName, _
        n.order.OrderDate, n.order.ShipCountry } _
    )
リスト12 Join句の代わりにJoinメソッドで記述したクエリ

 これを見るとJoin句の有り難みが分かります。

 以上、LINQのクエリが処理されるカラクリについて少し触れましたが、実はここまでの話は、クエリのデータソースが配列やコレクションの場合、つまりLINQ to Objectでの話です。

 クエリ自体の構文は同じですが、データソースがデータベースとなるLINQ to SQLではだいぶ中身が変わってきます。続いてはそんなLINQ to SQLについて説明します。


 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 記事ランキング

本日 月間