LINQによるデータベース・アクセスとO/Rマッピング:特集:C#プログラマーのためのLINQ超入門(後編)(1/3 ページ)
LINQ(リンク)でDBを検索するためのLINQ to SQLと、そのベースとなるO/Rマッピング機能について分かりやすく解説。
本特集の前編では、次のような簡単なLINQ(リンク)のクエリについて解説しました。
from n in data
where n.ShipCountry == "Norway"
select n;
前編で述べているように、クエリ内のwhere句やselect句は、コンパイル時にメソッドの呼び出しに変換されます。実際、LINQのクエリ構文をまったく使用せずに、メソッド呼び出しだけで同等の処理を記述できます。このような記述方法は、クエリ構文(query syntax)に対して、メソッド構文(method syntax)と呼ばれます。
後編となる今回は、リスト1をメソッド構文で書き換えるところから始めます。
LINQのメソッド構文
とはいっても、リスト1の場合には書き換えは簡単で、これは次のようになります。
data
.Where(n => n.ShipCountry == "Norway")
.Select(n => n)
見慣れない「=>」はC# 3.0の新機能である「ラムダ式」を記述するためのキーワードです。これについては後述します。リスト2は改行して3行で記述しているので少し分かりにくいですが、このコードの大枠は次のような1行です。
data.Where(……).Select(……);
つまり、変数dataに対してWhereメソッドを呼び出し、さらにその結果に対してSelectメソッドを呼び出しているだけです。
■ラムダ式を匿名メソッドに
WhereとSelectの2つのメソッドで引数に指定している「=>」を含んだ部分は、「ラムダ式」と呼ばれるものです。
ラムダ式とは、簡単にいうと、名前のない関数をシンプルに定義するための記述方法です。例えば、「n = n + 1」という関数自体を、あるメソッドに引数として渡したい場合、これまでは中身が1行だけのメソッドを定義し、そのデリゲートを渡す必要がありました。C# 3.0では、そのようなメソッドを「n => n + 1」というラムダ式で記述でき、これをそのままメソッドに渡すことができます。
ラムダ式は、機械的に匿名メソッドに置き換えることができます。リスト2の場合は次のように書き換えることができます。ここでは実際にコンパイルして実行できるように、クエリ以外のコードも示しています。
using System;
using System.Linq;
using System.Collections.Generic;
namespace ConsoleApplication1
{
class Orders
{
public int OrderID;
public int EmployeeID;
public DateTime OrderDate;
public string ShipCountry;
public Orders(int id, int emp, DateTime date, string ship) {
this.OrderID = id;
this.EmployeeID = emp;
this.OrderDate = date;
this.ShipCountry = ship;
}
}
class Program
{
static void Main(string[] args)
{
// Ordersオブジェクトの配列
Orders[] data = 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"),
};
// 元のクエリ
// IEnumerable<Orders> records =
// from n in data
// where n.ShipCountry == "Norway"
// select n;
// 引数のラムダ式を匿名メソッドに
IEnumerable<Orders> records =
data
.Where(
delegate(Orders n) {
return n.ShipCountry == "Norway";
}
)
.Select(
delegate(Orders n) {
return n;
}
);
foreach (Orders r in records)
{
Console.WriteLine("{0}, {1}, {2}, {3}",
r.OrderID, r.EmployeeID, r.OrderDate, r.ShipCountry);
}
Console.ReadLine();
}
}
}
ここで少しまとめておきましょう。Whereメソッドはフィルタの役目を果たします。つまり、「data.Where(……)」は配列dataの各要素に対して、指定されたフィルタ条件に合致する要素のみを返します。
フィルタ条件はどうやって指定するかというと、true/falseを返すメソッドへのデリゲート(あるいはリスト4のような匿名メソッド)を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 int OrderID;
public int EmployeeID;
// コンストラクタ
public OrderEmployee(int id, int emp) {
this.OrderID = id;
this.EmployeeID = emp;
}
}
クエリのselect句の部分を次のように書き換えるという手があります。
IEnumerable<OrderEmployee> records =
from n in data
where n.ShipCountry == "Norway"
select new OrderEmployee(n.OrderID, n.EmployeeID);
もちろんこれでも目的は達成できますが、いちいちクラスを定義するのは非常に面倒です。そこで、C# 3.0では自動的にクラス(およびコンストラクタ)を定義してくれる「匿名型」という機能が導入されています。
匿名型を使えば、次のようなクエリを記述するだけでコトが足ります。
var records =
from n in data
where n.ShipCountry == "Norway"
select new { OrderID = n.OrderID, EmployeeID = n.EmployeeID };
最後の「new { …… }」の記述が、匿名型を使用している個所です。これだけで、OrderIDとEmployeeIDというプロパティを持つクラスが自動的に定義され、それぞれのプロパティにn.OrderIDとn.EmpolyeeIDの値が代入されたオブジェクトが作成されます。
ただし、自動的に定義されたクラスのクラス名を知ることはできません。そのため、そのオブジェクトを変数で受け取るには、代入されるオブジェクトの型を自動的に設定してくれる「varキーワード」が不可欠となります。
匿名型は、クエリの結果の作成に必要な、一時的なオブジェクトを作成するために導入された機能であり、varキーワードはそのオブジェクトを受け取るために導入された機能であるというわけです*1。
*1 varキーワードはほかの場面でも便利に使えますが、匿名型はLINQのselect句以外には使い道はなさそうです。
クラス名も分からずにそのオブジェクトが扱えるのか? と少し心配になりますが、リスト7のクエリの結果へのアクセスは、再度varキーワードを使うことにより、次のようにして行えます。
foreach (var r in records)
{
Console.WriteLine("{0}, {1}", r.OrderID, r.EmployeeID);
}
ちなみに、リスト7の匿名型オブジェクト作成の部分は、次のようにプロパティ名を省略できます。
select new { n.OrderID, n.EmployeeID };
匿名型については、「連載:C# 3.0入門 第4回」で、varキーワードについては「同連載 第3回」で詳しく解説されていますので、そちらもぜひ参考にしてください。
■拡張メソッドであるWhereメソッドとSelectメソッド
ところで、リスト4では変数dataは単なる配列です。その配列に対してWhereメソッドを呼び出しているわけですが、配列(つまりはSystem名前空間のArrayクラス)にWhereメソッドなんかあったでしょうか?
実は、Whereメソッドは、「拡張メソッド」と呼ばれるC# 3.0の新機能により実現されています。拡張メソッドは、既存のクラスに対して、クラスの外部からメソッドを追加できる仕組みです。この場合では、配列(Arrayクラス)にWhereメソッドが追加される形となっています。
実際には、配列が実装しているIEnumerable<T>インターフェイスに対して、Whereメソッドが追加されています。これにより、IEnumerable<T>インターフェイスを実装しているすべてのオブジェクトに対して、Whereメソッドを呼び出すことができます。Selectメソッドも同様です。
なお、前編でも解説したように、WhereメソッドやSelectメソッドの本体自体はEnumerableクラス(System.Linq名前空間)で定義されています。Enumerableクラスには、グループ化のためのGroupByメソッドや、2つのデータソースを結合するためのJoinメソッドなど、LINQ用の標準クエリ演算子が拡張メソッドとして多数定義されています。
■クエリ構文とメソッド構文、どちらを使う?
以上のように、LINQでクエリは、クエリ構文(リスト1のパターン)でもメソッド構文(リスト2のパターン)でも記述することができます。気に入った方で記述すればよいでしょう。
ただし、where句やselect句などのようなキーワードがEnumerableクラスのメソッドすべてに対して用意されているわけではないので、両者を混在させるしかない場合もあります。
例えば、検索結果の件数をカウントするには、EnumerableクラスのCountメソッドを使って次のように記述します。
int num =
(from n in data
where n.ShipCountry == "Norway"
select n)
.Count();
こんなふうになるなら、最初から次のリスト10のように、すべてメソッド呼び出しで記述した方がよいという人も多いかもしれません。
int num =
data
.Where(n => n.ShipCountry == "Norway")
.Select(n => n)
.Count();
しかし例えば、2つのデータソースを結合して検索を行う場合には、Joinメソッドの呼び出しよりも、join句を使ったクエリ構文の方がすっきり記述できます。
次のリスト11は、join句を使用したサンプル・プログラムです。ここでは先ほどのリスト4に、従業員(=employee)のIDと名前からなるEmployeesクラスのインスタンスの配列を追加し、クエリの結果において、従業員IDの代わりに従業員の氏名が表示されるようにしています(いわゆる内部結合です)。
using System;
using System.Linq;
using System.Collections.Generic;
namespace ConsoleApplication1
{
class Orders
{
public int OrderID;
public int EmployeeID;
public DateTime OrderDate;
public string ShipCountry;
public Orders(int id, int emp, DateTime date, string ship) {
this.OrderID = id;
this.EmployeeID = emp;
this.OrderDate = date;
this.ShipCountry = ship;
}
}
class Employees
{
public int EmployeeID;
public string FirstName;
public Employees(int emp, string name) {
this.EmployeeID = emp;
this.FirstName = name;
}
}
class Program
{
static void Main(string[] args)
{
// Ordersオブジェクトの配列
Orders[] data = 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オブジェクトの配列
Employees[] emps = new Employees[]
{
new Employees(1, "ナンシー"),
new Employees(3, "ロバート"),
new Employees(7, "ジャネット"),
};
// join句を使用したクエリ
var records =
from n in data
join m in emps on n.EmployeeID equals m.EmployeeID
where n.ShipCountry == "Norway"
select new {
n.OrderID, m.FirstName, n.OrderDate, n.ShipCountry
};
foreach (var r in records)
{
Console.WriteLine("{0}, {1}, {2}, {3}",
r.OrderID, r.FirstName, r.OrderDate, r.ShipCountry);
}
Console.ReadLine();
}
}
}
長くなるため詳しくは説明しませんが、2つの配列に共通するEmployeeIDフィールドを軸に、Orders配列とEmployees配列を結合し、それに対してクエリを行っています。実行結果は次のようになります。
1000, ナンシー, 2008/07/18 3:09:37, Norway
1002, ジャネット, 2008/07/18 3:09:37, Norway
しかし、リスト11のクエリを、メソッド構文を使って記述すると次のようになってしまいます。
var records =
data
.Join(
emps,
n => n.EmployeeID,
m => m.EmployeeID,
(order, emp) => new { order = order, emp = emp }
)
.Where(n => n.order.ShipCountry == "Norway")
.Select(n =>
new { n.order.OrderID, n.emp.FirstName,
n.order.OrderDate, n.order.ShipCountry }
);
これを見るとjoin句の有り難みが分かります。
以上、LINQのクエリが処理されるカラクリについて少し触れましたが、実はここまでの話は、クエリのデータソースが配列やコレクションの場合、つまりLINQ to Objectでの話です。
クエリ自体の構文は同じですが、データソースがデータベースとなるLINQ to SQLではだいぶ中身が変わってきます。続いてはそんなLINQ to SQLについて説明します。
Copyright© Digital Advantage Corp. All Rights Reserved.