Dinesh Kulkarni, Luca Bolognese, Matt Warren, Anders Hejlsberg, Kit George
March 2007
日本語版最終更新日 2007 年 10 月 4 日
適用対象 :
Visual Studio 2008
.Net Framework 3.5
概要 : LINQ to SQL は、クエリ機能を維持したまま、リレーショナル データをオブジェクトとして管理するためのランタイム インフラストラクチャを提供します。LINQ to SQL がバックグラウンドで変更を自動的に追跡している間も、アプリケーションでは自由にオブジェクトを操作できます。
目次
はじめに
クイック ツアー
エンティティ クラスの作成
DataContext
リレーションシップの定義
リレーションシップ間のクエリ
エンティティの変更と保存
クエリの詳細
クエリの実行
オブジェクトの ID
リレーションシップ
結合
プロジェクション
コンパイルされたクエリ
SQL 変換
エンティティのライフサイクル
変更の追跡
変更の送信
同時変更
トランザクション
ストアド プロシージャ
エンティティ クラスの詳細
属性の使用
グラフの一貫性
変更通知
継承
高度なトピック
データベースの作成
ADO.NET との相互運用
変更の競合の解決
ストアド プロシージャの呼び出し
エンティティ クラス ジェネレータ ツール
DBML ジェネレータ ツール リファレンス
多層エンティティ
外部マッピング
NET Framework 関数のサポートと注意事項
デバッグのサポート
はじめに
現在作成されているほとんどのプログラムでは、さまざまな方法でデータを操作しており、操作対象となる多くのデータはリレーショナル データベースに格納されています。しかし、現在のプログラミング言語とデータベース間には、情報を表示および操作する方法に関して大きな相違があります。このインピーダンス不整合は、さまざまな形で現れます。最も顕著なのは、プログラミング言語では、クエリをテキスト文字列として指定する必要のある API を使用して、データベース内の情報にアクセスする点です。こうしたクエリは、プログラム ロジックの大半を占めます。しかし、このようなクエリは、プログラミング言語にとって不明瞭なため、コンパイル時の検証や IntelliSense などのデザイン時の機能を活用することはできません。
当然ながら、これ以上に根深い違いもあります。情報の表示方法 (データ モデル) は、両者の間でまったく異なります。現在のプログラミング言語では、情報をオブジェクトの形式で定義しますが、リレーショナル データベースでは、行を使用します。オブジェクトの各インスタンスは物理的に異なるので、オブジェクトには一意の ID がありますが、行は主キー値で識別されます。オブジェクトには、インスタンスを識別およびリンクする参照が含まれていますが、行は意図的に独立した状態になっているため、外部キーを使用して関連する行を緩やかに結びつける必要があります。オブジェクトはスタンドアロンで、別のオブジェクトによって参照されている間は存在しますが、行は、テーブルの要素として存在するので、削除されるとなくなります。
こうした違いを克服する必要があるアプリケーションの作成と管理が難しいのは当然です。たしかに、どちらか一方を排除すれば、この均一化の作業は簡略化されるでしょう。しかし、リレーショナル データベースは長期的なストレージおよびクエリ処理において重要なインフラストラクチャを提供しており、現在のプログラミング言語は迅速な開発や優れた計算にとって不可欠です。
これまで、こうした不整合は、アプリケーション開発者が、各アプリケーションで個別に解決する必要がありました。今までのところ、最も優れた解決策は、複雑なデータベース抽象層を使用することでした。この層では、アプリケーション ドメイン固有のオブジェクト モデルと表形式のデータベースの間で情報をやり取りし、それぞれに適した方法でデータの再作成や書式の再設定を行います。この解決策では、実際のデータ ソースがあいまいになるため、リレーショナル データベースの最も魅力的な機能である、データをクエリする機能を放棄することになります。
LINQ to SQL は、Visual Studio 2008 のコンポーネントで、クエリ機能を維持したまま、リレーショナル データをオブジェクトとして管理するためのランタイム インフラストラクチャを提供します。LINQ to SQL では、統合言語クエリをデータベースで実行できるように SQL に変換した後、表形式の結果を定義したオブジェクトの形式に変換します。これにより、LINQ to SQL がバックグラウンドで変更を自動追跡している間も、アプリケーションは自由にオブジェクトを操作できます。
- LINQ to SQL は、アプリケーションにとって煩わしくないようにデザインされています。
- LINQ to SQL は ADO.NET ファミリのコンポーネントであるため、(同じ接続やトランザクションを共有して) 現在の ADO.NET ソリューションを個別に LINQ to SQL に段階的に移行することができます。また、LINQ to SQL では、ストアド プロシージャも幅広くサポートしているため、既存の企業資産も再利用できます。
- LINQ to SQL アプリケーションは初めてでも簡単に使用できます。
- リレーショナル データにリンクされたオブジェクトは、プロパティが列にどのように対応しているかを示す属性で装飾する必要はありますが、それ以外については、通常のオブジェクトと同様に定義できます。当然ながら、これを手動で行う必要もありません。既存のリレーショナル データベース スキーマからオブジェクト定義への変換を自動化するデザイン時ツールが用意されています。
LINQ to SQL のランタイム インフラストラクチャとデザイン時ツールを併用すると、データベース アプリケーション開発者の作業負荷は大幅に軽減されます。以下の章では、LINQ to SQL を使用してデータベース関連の一般的な作業を行う方法の概要を説明します。このドキュメントでは、読者が統合言語クエリと標準クエリ演算子に関する詳しい知識があることを前提としています。
LINQ to SQL は言語に依存しません。統合言語クエリを提供するように開発された言語では、統合言語クエリを使用して、リレーショナル データベースに格納された情報にアクセスすることができます。このドキュメントのサンプルでは、C# と Visual Basic の両方を紹介しています。LINQ to SQL は、Visual Basic コンパイラの LINQ 対応バージョンでも使用できます。
クイック ツアー
LINQ to SQL アプリケーションを作成するには、まず、アプリケーション データを表すのに使用するオブジェクト クラスを宣言する必要があります。それでは例を見てみましょう。
エンティティ クラスの作成
まず、単純な Customer クラスを作成し、このクラスに Northwind サンプル データベースの Customers テーブルを関連付けます。これを行うのに必要な作業は、クラス宣言の先頭にカスタム属性を追加するだけです。これを実現するため、LINQ to SQL では Table 属性が定義されています。
C#
[Table(Name="Customers")]
public class Customer
{
public string CustomerID;
public string City;
} Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
Public CustomerID As String
Public City As String
End Class
Table 属性には Name プロパティがあります。このプロパティを使用して、データベース テーブルの正確な名前を指定することができます。Name プロパティが指定されていない場合、LINQ to SQL では、データベース テーブル名はクラスと同じ名前であると見なされます。データベースには、テーブルとして宣言されたクラスのインスタンスのみが格納されます。このようなクラスのインスタンスは、"エンティティ" と呼ばれます。クラス自体は、"エンティティ クラス" と呼ばれます。
テーブルへのクラスの関連付け以外に、データベース列に関連付ける各フィールドやプロパティを指定する必要があります。これを行うために、LINQ to SQL では Column 属性が定義されています。
C#
[Table(Name="Customers")]
public class Customer
{
[Column(IsPrimaryKey=true)]
public string CustomerID;
[Column]
public string City;
} Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
<Column(IsPrimaryKey:=true)> _
Public CustomerID As String
<Column> _
Public City As String
End Class
Column 属性には、カスタマイズして、フィールドとデータベース列間のマッピングを的確に行えるようにする、さまざまなプロパティがあります。注目すべきプロパティの 1 つが、Id プロパティです。このプロパティは、LINQ to SQL に、そのデータベース列がテーブルの主キーの一部であることを知らせます。
Table 属性と同様に、列名が、フィールドまたはプロパティの宣言から想定される列名と異なる場合にのみ、Column 属性に情報を設定する必要があります。この例では、CustomerID フィールドがテーブルの主キーの一部であることを LINQ to SQL に知らせる必要はありますが、具体的な名前や型を指定する必要はありません。
データベースで保存されたり、データベースから取得されたりするのは、列として宣言されたフィールドまたはプロパティのみです。それ以外は、アプリケーション ロジックの一時的な部分と見なされます。
DataContext
- DataContext は、データベースからオブジェクトを取得したり、変更を再送信したりする際に使用する主要ルートです。使用方法は、ADO.NET の Connection と同じです。その証拠に、DataContext は、指定した接続または接続文字列を使用して初期化されます。DataContext の目的は、オブジェクトに対する要求を、データベースに対して実行する SQL クエリに変換した後、結果からオブジェクトを組み立てることです。DataContext を使用すると、Where や Select などの標準クエリ演算子と同じパターンの演算子を実装することにより、統合言語クエリ を実現できます。
たとえば、次のように、DataContext を使用して、ロンドンに住んでいる顧客のオブジェクトを取得できます。
C#
// DataContext で接続文字列を取得します。
DataContext db = new DataContext("c:\\northwind\\northwnd.mdf");
// クエリを実行するために、型指定されたテーブルを取得します。
Table<Customer> Customers = db.GetTable<Customer>();
// ロンドンに住む顧客をクエリします。
var q =
from c in Customers
where c.City == "London"
select c;
foreach (var cust in q)
Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City); Visual Basic
' DataContext で接続文字列を取得します。
Dim db As DataContext = New DataContext("c:\northwind\northwnd.mdf")
' クエリを実行するために、型指定されたテーブルを取得します。
Dim Customers As Customers(Of Customer) = db.GetTable(Of Customer)()
' ロンドンに住む顧客をクエリします。
Dim londonCustomers = From customer in Customers _
Where customer.City = "London" _
Select customer
For Each cust in londonCustomers
Console.WriteLine("id = " & cust.CustomerID & ", City = " & cust.City)
Next 各データベース テーブルは Table コレクションとして表され、エンティティ クラスを使用してテーブルを識別する GetTable() メソッドを経由してアクセスできます。基本クラスである DataContext クラスと GetTable() メソッドを使用するのではなく、厳密に型指定された DataContext を宣言することをお勧めします。厳密に型指定された DataContext では、すべての Table コレクションがコンテキストのメンバとして宣言されます。
C#
public partial class Northwind : DataContext
{
public Table<Customer> Customers;
public Table<Order> Orders;
public Northwind(string connection): base(connection) {}
} Visual Basic
Partial Public Class Northwind
Inherits DataContext
Public Customers As Table(Of Customers)
Public Orders As Table(Of Orders)
Public Sub New(ByVal connection As String)
MyBase.New(connection)
End Sub
End Class ロンドンに住む顧客についてのクエリは、次のように、より簡潔に表現できます。
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (var cust in q)
Console.WriteLine("id = {0}, City = {1}",cust.CustomerID, cust.City); Visual Basic
Dim db = New Northwind("c:\northwind\northwnd.mdf")
Dim londonCustomers = From cust In db.Customers _
Where cust.City = "London" _
Select cust
For Each cust in londonCustomers
Console.WriteLine("id = {0}, City = {1}", cust.CustomerID, cust.City)
Next この概要ドキュメントの残りの部分でも、引き続き厳密に型指定された Northwind クラスを使用します。
リレーションシップの定義
通常、リレーショナル データベースのリレーションシップは、他のテーブルの主キーを参照している外部キー値としてモデル化されます。リレーションシップ間を移動するには、相関的な結合操作を使用して 2 つのテーブルを明示的に結合する必要があります。一方、オブジェクトでは、"ドット" 表記を使用して移動が行われるプロパティ参照または参照のコレクションを使用して、相互参照が行われます。移動のたびに明示的な結合条件を思い出す必要がないので、明らかに、結合よりもドットを使用する方が簡単です。
このような常に同じになるデータのリレーションシップについては、リレーションシップをエンティティ クラスのプロパティ参照としてコード化すると非常に便利です。LINQ to SQL では、メンバに適用してリレーションシップを表すことができる Association 属性が定義されています。関連付けリレーションシップは、テーブル間で列の値を照合して作成された外部キーと主キーのリレーションシップのようなものです。
C#
[Table(Name="Customers")]
public class Customer
{
[Column(Id=true)]
public string CustomerID;
...
private EntitySet<Order> _Orders;
[Association(Storage="_Orders", OtherKey="CustomerID")]
public EntitySet<Order> Orders {
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
} Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
<Column(Id:=true)> _
Public CustomerID As String
...
Private _Orders As EntitySet(Of Order)
<Association(Storage:="_Orders", OtherKey:="CustomerID")> _
Public Property Orders() As EntitySet(Of Order)
Get
Return Me._Orders
End Get
Set(ByVal value As EntitySet(Of Order))
End Set
End Property
End Class Customer クラスでは、顧客と注文との間のリレーションシップを宣言するプロパティを使用できるようになりました。リレーションシップには一対多の関係性があるので、Orders プロパティの型は EntitySet です。関連付け方法の説明には、Association 属性の OtherKey プロパティを使用しています。このプロパティは、比較対象となる関連クラスのプロパティ名を指定します。ここでは指定しませんでしたが、ThisKey というプロパティもあります。通常、こちら側のリレーションシップのメンバを一覧表示するには、このプロパティを使用します。ただし、このプロパティを省略しても、LINQ to SQL によって、主キーを構成するメンバがリレーションシップのメンバであることが想定されるようにしています。
次に示すように、Order クラスの定義では、これが逆に定義されています。
C#
[Table(Name="Orders")]
public class Order
{
[Column(Id=true)]
public int OrderID;
[Column]
public string CustomerID;
private EntityRef<Customer> _Customer;
[Association(Storage="_Customer", ThisKey="CustomerID")]
public Customer Customer {
get { return this._Customer.Entity; }
set { this._Customer.Entity = value; }
}
} Visual Basic
<Table(Name:="Orders")> _
Public Class Order
<Column(Id:=true)> _
Public OrderID As String
<Column> _
Public CustomerID As String
Private _Customer As EntityRef(Of Customer)
<Association(Storage:="_Customer", ThisKey:="CustomerID")> _
Public Property Customer() As Customer
Get
Return Me._Customer.Entity
End Get
Set(ByVal value As Customer)
Me._Customers.Entity = value
End Set
End Property
End Class Order クラスでは、EntityRef 型を使用して、Customer クラスにリレーションシップの情報を渡しています。"遅延読み込み" (後で説明します) をサポートするには、EntityRef クラスを使用する必要があります。この時点では、こちら側のリレーションシップには推測可能なメンバが存在しないので、Customer プロパティの Association 属性では、ThisKey プロパティが指定されます。
今度は、Storage プロパティも見てみましょう。このプロパティは、プロパティ値を保持するのに使用するプライベート メンバを LINQ to SQL に知らせます。これにより、LINQ to SQL では、プロパティ値を格納および取得する際に、パブリックなプロパティ アクセサを使用する必要がなくなります。このプロパティは、アクセサに組み込まれたカスタムのビジネス ロジックに LINQ to SQL がアクセスすることを回避する場合に必要です。この Storage プロパティを指定しない場合は、パブリック アクセサが使用されます。Column 属性でも Storage プロパティを使用できます。
エンティティ クラスにリレーションシップを追加すると、通知やグラフの一貫性のサポートを導入することになるので、記述する必要があるコードの量は増加します。さいわいなことに、必要なすべての定義を部分的なクラスとして生成するためのツールがあります (このツールについては、後で説明します)。このツールを使用すると、生成されたコードとカスタムのビジネス ロジックを一緒に使用できます。
このドキュメントの残りの部分では、このツールを使用して Northwind の完全なデータ コンテキストとすべてのエンティティ クラスが生成されていることを前提としています。
リレーションシップ間のクエリ
リレーションシップを定義したので、クラスで定義したリレーションシップ プロパティを参照するだけで、クエリを記述するときにリレーションシップを使用できるようになりました。
C#
var q =
from c in db.Customers
from o in c.Orders
where c.City == "London"
select new { c, o }; Visual Basic
Dim londonCustOrders = From cust In db.Customers, ord In cust.Orders _
Where cust.City = "London" _
Select Customer = cust, Order = ord 上記のクエリでは、Orders プロパティを使用して顧客と注文の外積を行い、Customer と Order のペアの新しいシーケンスを作成しています。
また、この逆の処理を行うこともできます。
C#
var q =
from o in db.Orders
where o.Customer.City == "London"
select new { c = o.Customer, o }; Visual Basic
Dim londonCustOrders = From ord In db.Orders _
Where ord.Customer.City = "London" _
Select Customer = ord.Customer, Order = ord この例では、注文をクエリし、Customer リレーションシップを使用して、関連付けられている Customer オブジェクトの情報にアクセスしています。
エンティティの変更と保存
クエリのみを考慮して構築されたアプリケーションはほとんどありません。データの作成や変更についても考慮する必要があります。LINQ to SQL は、オブジェクトに対して加えられた変更を操作したり、保存したりする際に最大限の柔軟性を発揮するようにデザインされています。エンティティ オブジェクトは、クエリを実行して取得するか新しく作成することによって使用できるようになれば、すぐに、値の変更、コレクションへの追加、およびコレクションからの削除を行うなど、アプリケーションでは、オブジェクトを通常のオブジェクトと同様に自由に操作できるようになります。LINQ to SQL により、すべての変更が追跡されており、操作の完了後、すぐにデータベースに変更が送信されます。
次の例では、ツールを使用して Northwind サンプル データベース全体のメタデータから生成した Customer クラスと Order クラスを使用しています。簡潔にするために、クラス定義は示していません。
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// 特定の顧客をクエリします。
string id = "ALFKI";
var cust = db.Customers.Single(c => c.CustomerID == id);
// 連絡先の名前を変更します。
cust.ContactName = "New Contact";
// 新しい Order オブジェクトを作成し、Orders コレクションに追加します。
Order ord = new Order { OrderDate = DateTime.Now };
cust.Orders.Add(ord);
// DataContext を使用してすべての変更を保存します。
db.SubmitChanges(); Visual Basic
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' 特定の顧客をクエリします。
Dim id As String = "ALFKI"
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = id).First
' 連絡先の名前を変更します。
targetCustomer.ContactName = "New Contact"
' 新しい Order オブジェクトを作成し、Orders コレクションに追加します。
Dim id = New Order With { .OrderDate = DateTime.Now }
targetCustomer.Orders.Add(ord)
' DataContext を使用してすべての変更を保存します。
db.SubmitChanges() SubmitChanges() が呼び出されると、LINQ to SQL によって自動的に SQL コマンドが生成および実行され、データベースに変更が送信されます。この動作は、カスタムのロジックでオーバーライドすることもできます。カスタムのロジックでは、データベース ストアド プロシージャを呼び出せます。
クエリの詳細
LINQ to SQL により、リレーショナル データベースのテーブルに関連付けられたオブジェクトに、標準クエリ演算子を実装できます。この章では、クエリにおける LINQ to SQL 固有の側面を説明します。
クエリの実行
高レベルのクエリ式としてクエリを記述する場合も個別の演算子を使用してクエリを作成する場合も、記述するクエリはすぐに実行しなくてもよいステートメントです。ステートメントは、説明です。たとえば、次の宣言では、ローカル変数 q は、クエリの実行結果ではなくクエリの説明を参照しています。
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
For Each cust In londonCustomers
Console.WriteLine(cust.CompanyName)
Next このインスタンスでの q の実際の型は、IQueryable<Customer> です。アプリケーションによってクエリの内容が列挙されるまで、クエリは実行されません。この例では、foreach ステートメントでクエリが実行されます。
IQueryable オブジェクトは、ADO.NET のコマンド オブジェクトに似ています。IQueryable オブジェクトを使用すれば、クエリが実行されるというわけではありません。コマンド オブジェクトは、クエリを説明する文字列を保持します。同様に IQueryable オブジェクトも、データ構造としてコード化された "式" と呼ばれるクエリの説明を保持します。コマンド オブジェクトには、クエリの実行を促す ExecuteReader() メソッドがあり、このメソッドの結果は DataReader として返されます。IQueryable には、クエリの実行を促す GetEnumerator() メソッドがあり、このメソッドの結果は IEnumerator<Customer> として返されます。
したがって、クエリが 2 回列挙された場合、クエリは 2 回実行されることになります。
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
// 最初の実行
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
// 2 回目の実行
foreach (Customer c in q)
Console.WriteLine(c.CompanyName);
Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
' 最初の実行
For Each cust In londonCustomers
Console.WriteLine(cust.CompanyName)
Next
' 2 回目の実行
For Each cust In londonCustomers
Console.WriteLine(cust.CustomerID)
Next この動作は "遅延実行" と呼ばれます。ADO.NET のコマンド オブジェクトと同様に、クエリを保持し、そのクエリを再実行することができます。
もちろん、多くの場合、アプリケーションの作成者はクエリを実行する場所とタイミングを非常に明確にしておく必要があります。アプリケーションで結果を複数回調べる必要があるという理由で、クエリが複数回実行されるということは想定されていない動作でしょう。たとえば、クエリの結果は DataGrid などにバインドできます。このコントロールでは、画面上で描画を行うたびに、結果が列挙されます。
- クエリが何回も実行されないようにするには、結果を任意の数の標準のコレクション クラスに変換します。標準クエリ演算子の ToList() または ToArray() を使用すると、結果を簡単にリストや配列に変換できます。
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
// ToList() または ToArray() を使用して 1 回実行します。
var list = q.ToList();
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
foreach (Customer c in list)
Console.WriteLine(c.CompanyName);
Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
' ToList() または ToArray() を実行して 1 回実行します。
Dim londonCustList = londonCustomers.ToList()
' どちらの反復処理でも、クエリは再実行されません。
For Each cust In londonCustList
Console.WriteLine(cust.CompanyName)
Next
For Each cust In londonCustList
Console.WriteLine(cust.CompanyName)
Next 遅延実行の利点の 1 つは、クエリを部分的に作成し、作成が完了したときにのみ実行できる点です。最初にクエリの一部を作成し、そのクエリをローカル変数に代入した後、さらに演算子を適用することができます。
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
if (orderByLocation) {
q =
from c in q
orderby c.Country, c.City
select c;
}
else if (orderByName) {
q =
from c in q
orderby c.ContactName
select c;
}
foreach (Customer c in q)
Console.WriteLine(c.CompanyName); Visual Basic
Dim londonCustomers = From cust In db.Customers _
where cust.City = "London"
if orderByLocation Then
londonCustomers = From cust in londonCustomers _
Order By cust.Country, cust.City
Else If orderByName Then
londonCustomers = From cust in londonCustomers _
Order By cust.ContactName
End If
For Each cust In londonCustList
Console.WriteLine(cust.CompanyName)
Next この例では、q を、ロンドンに住むすべての顧客についてのクエリとして始めています。その後この q は、アプリケーションの状態を基準にして並べ替えが行われるクエリに変化しています。遅延実行を使用することによって、リスクを伴う文字列操作を行わなくても、アプリケーションのニーズに適したクエリを作成できます。
オブジェクトの ID
ランタイムのオブジェクトには、一意の ID があります。2 つの変数が同じオブジェクトを参照する場合、これらの変数では、実際に同じオブジェクト インスタンスを参照しています。そのため、一方の変数によって行われた変更は、もう一方の変数でもすぐに認識されます。リレーショナル データベースのテーブルの行には一意の ID がありませんが、主キーがあります。主キーは、2 つの行で同じキーを共有できないので、一意であると言えます。ただし、これはデータベース テーブルの内容を制約しているにすぎません。したがって、リモート コマンドを使用してデータを操作するだけであれば、結果はほぼ同じです。
ただし、そのようなことはほとんどありません。ほとんどの場合、データは、データベースから、アプリケーションがデータを操作する別の層へと取り出されます。これは、まぎれもなく LINQ to SQL で意図しているサポート モデルです。データが行としてデータベースから取り出された場合、同じデータを表している 2 つの行が、同じ行のインスタンスに対応していることを想定する人はいないでしょう。特定の顧客について 2 回クエリすると、それぞれ同じ情報を含んだ 2 つのデータ行を取得することになります。
しかし、オブジェクトではまったく異なる状況が想定されます。たとえば、DataContext で同じ情報を 2 回要求した場合は、同じオブジェクト インスタンスが返されることを想定するでしょう。これは、オブジェクトがアプリケーションにとって特別な意味を持ち、通常のオブジェクトと同様に動作すると思われているからです。オブジェクトを階層またはグラフとしてデザインした場合は、当然、オブジェクトがデザインしたとおりの状態で取得できると考え、その際、同じものを 2 回要求しただけなので、インスタンスが多数レプリケートされるとは考えないでしょう。
このような想定に対応するため、DataContext では、オブジェクトの ID が管理されます。データベースから新しい行を取得するたびに、その行の主キーが ID テーブルに記録され、新しいオブジェクトが作成されます。同じ行が繰り返し取得されるたびに、アプリケーションには最初のオブジェクト インスタンスが渡されます。このようにして、DataContext では ID というデータベースの概念 (キー) を言語の概念 (インスタンス) に変換しています。アプリケーションでは、最初に取得したときの状態のオブジェクトしか認識されません。データが異なる場合は、新しいデータが破棄されます。
この点について戸惑うことがあるかもしれません。アプリケーションによってデータが破棄されるのはなぜでしょうか。結局のところ、これが LINQ to SQL によるローカル オブジェクトの整合性管理の方法であり、この方法でオプティミスティックな更新をサポートできるというのがその理由です。オブジェクトが最初に作成された後の変更はアプリケーションによって行われるので、その変更目的は明らかです。オブジェクトの作成後に、外部から変更が行われた場合は、その変更は SubmitChanges() が呼び出されたときに認識されます。詳細については、「同時変更」で説明します。
LINQ to SQL では、データベースに主キーが設定されていないテーブルが含まれている場合、テーブルに関するクエリを送信することはできますが、更新は行えないことに注意してください。これは、一意のキーがない場合には、更新する行をフレームワークで認識できないためです。
もちろん、そのオブジェクトの主キーから、クエリで要求したオブジェクトが既に取得したオブジェクトであることが簡単に識別できる場合、クエリは実行されません。ID テーブルは、以前に取得したすべてのオブジェクトを格納するキャッシュとして機能します。
リレーションシップ
「クイック ツアー」で説明したように、クラス定義での他のオブジェクトまたは他のオブジェクトのコレクションへの参照は、データベースの外部キー リレーションシップにそのまま対応しています。クエリで、ドット表記を使用してリレーションシップ プロパティにアクセスすることによって、このようなリレーションシップを使用してオブジェクト間を移動できます。このようなアクセス操作が SQL において同等のより複雑な結合または相関サブクエリに変換されることにより、クエリ中にオブジェクト グラフ内を移動することができます。たとえば、次のクエリでは、クエリの結果をロンドンに住む顧客の注文のみに制限するために、注文から顧客へと移動しています。
C#
var q =
from o in db.Orders
where o.Customer.City == "London"
select o;
Visual Basic
Dim londonOrders = From ord In db.Orders _
where ord.Customer.City = "London" リレーションシップ プロパティが存在しなかったとしたら、SQL クエリと同様に、結合を使用してリレーションシップ プロパティを完全に記述する必要があります。
C#
var q =
from c in db.Customers
join o in db.Orders on c.CustomerID equals o.CustomerID
where c.City == "London"
select o;
Visual Basic
Dim londonOrders = From cust In db.Customers _
Join ord In db.Orders _
On cust.CustomerID Equals ord.CustomerID _
Where ord.Customer.City = "London" _
Select ord リレーションシップ プロパティを使用すると、便利なドット表記を使用して、このようなリレーションシップを一度に定義できます。ただし、リレーションシップ プロパティが存在しているのはこのためではありません。リレーションシップ プロパティが存在するのは、ドメイン固有のオブジェクト モデルが、階層またはグラフとして定義される傾向があるためです。プログラムを組む対象として選択したオブジェクトには、他のオブジェクトへの参照が含まれています。オブジェクト対オブジェクトのリレーションシップがデータベースにおける外部キー形式のリレーションシップに対応していることから、プロパティでアクセスすることが結合を記述する際の便利な方法になるというのは、幸運な偶然にすぎません。
したがって、リレーションシップ プロパティの存在は、クエリの一部としてではなく、クエリの結果において重要です。特定の顧客を見てみると、クラス定義から、顧客には注文が付随していることがわかります。ですから、特定の顧客の Orders プロパティを調べると、実際に約束事としてクラス定義で宣言した動作から、その顧客のすべての注文が設定されたコレクションが表示されることを想定します。また、特に事前に注文を要求しなかった場合でも、注文が表示されることを想定するでしょう。オブジェクト モデルは、データベースのメモリ内拡張で、関連するオブジェクトがすぐに使用できるという期待を抱いているかもしれません。
LINQ to SQL には、この期待に応えるため、"遅延読み込み" と呼ばれる技法が実装されています。オブジェクトについてクエリする場合、実際には、要求したオブジェクトを取得しているだけです。関連オブジェクトも自動的に同時に取得されるわけではありません。ただし、関連オブジェクトにアクセスしようとすると、すぐに、その要求が送信されて関連オブジェクトが取得されるので、関連オブジェクトがまだ読み込まれていないという事実が表面化することはありません。
C#
var q =
from o in db.Orders
where o.ShipVia == 3
select o;
foreach (Order o in q) {
if (o.Freight > 200)
SendCustomerNotification(o.Customer);
ProcessOrder(o);
} Visual Basic
Dim shippedOrders = From ord In db.Orders _
where ord.ShipVia = 3
For Each ord In shippedOrders
If ord.Freight > 200 Then
SendCustomerNotification(ord.Customer)
ProcessOrder(ord)
End If
Next たとえば、特定の注文のセットについてクエリし、特定の顧客に電子メールの通知をたまに送信する場合があります。このような場合、注文ごとにすべての顧客データを事前に取得する必要はありません。遅延読み込みを使用すると、追加情報を取得する手間を、その追加情報が必要になるときまで延期することができます。
もちろん、その逆が適切な場合もあります。アプリケーションで、顧客データと注文データを同時に確認する必要があることもあるからです。この場合は、当然、両方のデータのセットが必要です。そしてアプリケーションでは、注文データを取得するとすぐに顧客ごとに注文をデータ掘り下げようとします。このような場合、各顧客のそれぞれの注文について個々にクエリを実行するのは実に不適切で、顧客データと共に注文データを取得する必要があります。
C#
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach (Customer c in q) {
foreach (Order o in c.Orders) {
ProcessCustomerOrder(o);
}
} Visual Basic
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London"
For Each cust In londonCustomers
For Each ord In cust.Orders
ProcessCustomerOrder(ord)
End If
Next もちろん、外積を行い、すべての関連データを 1 つの大規模なプロジェクションとして取得すれば、顧客と注文を 1 つのクエリで結合することはできます。しかし、この操作の結果はエンティティになりません。エンティティは、ID を持つ変更可能なオブジェクトですが、この操作の結果は、変更したり、保存したりすることができないプロジェクションです。さらに都合の悪いことに、フラットな結合の出力により、注文ごとに顧客データが繰り返し出力されるので、大量の重複データを取得することになります。
本当に必要としているのは、関連オブジェクトのセット (グラフの特定部分) を同時に取得し、必要な情報を過不足なく取得できる方法です。
このような理由から、LINQ to SQL では、オブジェクト モデルの一部の "即時読み込み" を要求することができます。これは、DataContext の代わりに DataShape を指定できるようにすることで実現しています。DataShape クラスは、特定の型の取得時に、どのオブジェクトを取得するのかをフレームワークに指示するのに使用します。これは、次のようにして LoadWith メソッドを使用して行います。
C#
DataShape ds = new DataShape();
ds.LoadWith<Customer>(c => c.Orders);
db.Shape = ds;
var q =
from c in db.Customers
where c.City == "London"
select c;
Visual Basic
Dim ds As DataShape = New DataShape()
ds.LoadWith(Of Customer)(Function(c As Customer) c.Orders)
db.Shape = ds
Dim londonCustomers = From cust In db.Customers _
Where cust.City = "London" _
Select cust 上記のクエリでは、ロンドンに住むすべての顧客のすべての注文がクエリの実行時に取得されるので、その後、Customer オブジェクトの Orders プロパティへのアクセスでは、データベース クエリは実行されません。
DataShape クラスを使用すると、リレーションシップの移動に適用されるサブクエリを指定することもできます。たとえば、今日出荷した注文を取得する場合は、DataShape で AssociateWith メソッドを次のように使用できます。
C#
DataShape ds = new DataShape();
ds.AssociateWith<Customer>(
c => c.Orders.Where(p => p.ShippedDate != DateTime.Today));
db.Shape = ds;
var q =
from c in db.Customers
where c.City == "London"
select c;
foreach(Customer c in q) {
foreach(Order o in c.Orders) {}
} Visual Basic
Dim ds As DataShape = New DataShape()
ds.AssociateWith(Of Customer)( _
Function(cust As Customer) From cust In db.Customers _
Where order.ShippedDate <> Today _
Select cust)
db.Shape = ds
Dim londonCustomers = From cust In db.Customers _
Where cust.City = "London" _
Select cust
For Each cust in londonCustomers
For Each ord In cust.Orders …
Next
Next 上記のコードの内側の foreach ステートメントでは、今日出荷された注文のみを繰り返し処理します。これは、そのような注文のみがデータベースから取得されているからです。
DataShape クラスには、次に示す 2 つの特筆事項があります。
- DataShape を DataContext に代入した後は、DataShape を変更できません。このような DataShapeで LoadWith メソッドまたは AssociateWith メソッドを呼び出すと、実行時にエラーが返されます。
- LoadWith や AssociateWith を使用して循環を作成することはできません。たとえば、次の例では、実行時にエラーが発生します。
C#
DataShape ds = new DataShape();
ds.AssociateWith<Customer>(
c=>c.Orders.Where(o=> o.Customer.Orders.Count() < 35); Visual Basic
Dim ds As DataShape = New DataShape()
ds.AssociateWith(Of Customer)( _
Function(cust As Customer) From ord In cust.Orders _
Where ord.Customer.Orders.Count() < 35)
結合
オブジェクト モデルに対する多くのクエリは、そのオブジェクト モデルのオブジェクト参照の移動に大きく依存します。ただし、エントリ間には、オブジェクト モデルの参照と見なされない、リレーションシップがあります。たとえば、Customer.Orders は、Northwind データベース内の外部キー リレーションシップに基づいた便利なリレーションシップです。ただし、City や Country が同じである Supplier と Customer は、外部キー リレーションシップに基づかない一時的なリレーションシップであり、オブジェクト モデルではキャプチャされないことがあります。結合を使用すると、このようなリレーションシップを処理するメカニズムが提供されます。LINQ to SQL では、LINQ に導入された新しい結合演算子がサポートされます。
同じ市内の業者と顧客を見つけるという問題を考えてみましょう。次のクエリでは、業者の会社名と顧客の会社名および両者に共通の都市名がフラットな結果として返されます。これは、リレーショナル データベースの内部等結合に相当します。
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City
select new {
Supplier = s.CompanyName,
Customer = c.CompanyName,
City = c.City
}; Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Join cust In db.Customers _
On sup.City Equals cust.City _
Select Supplier = sup.CompanyName, _
CustomerName = cust.CompanyName, _
City = cust.City 上記のクエリでは、特定の顧客と異なる市内に拠点を構える業者を除外しています。ただし、一時的なリレーションシップでは、エントリのいずれかを除外することが好ましくない場合もあります。次のクエリでは、業者ごとに顧客をグループ化して、すべての業者を一覧表示しています。同じ市内に顧客を持たない業者の場合、クエリ結果では、空の顧客のコレクションがその業者に関連付けられます。各業者にコレクションが関連付けられるため、結果はフラットではありません。事実上、これはグループ結合となります。つまり、2 つのシーケンスを結合し、最初のシーケンスの要素によって、2 番目のシーケンスの要素をグループ化しています。
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into scusts
select new { s, scusts }; Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Group Join cust In db.Customers _
On sup.City Equals cust.City _
Into supCusts _
Select Supplier = sup, _
Customers = supCusts グループ結合も、複数のコレクションへと拡張できます。次のクエリは、上記のクエリを拡張して、業者の拠点と同じ市内に住む社員を表示するようにしています。このクエリでは、顧客と社員のコレクション (空の場合があります) と共に業者が表示されます。
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into scusts
join e in db.Employees on s.City equals e.City into semps
select new { s, scusts, semps }; Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Group Join cust In db.Customers _
On sup.City Equals cust.City _
Into supCusts _
Group Join emp In db.Employees _
On sup.City Equals emp.City _
Into supEmps _
Select Supplier = sup, _
Customers = supCusts, Employees = supEmps グループ結合の結果も、フラット化できます。業者と顧客間のグループ結合の結果をフラット化すると、複数の業者のエントリが各業者と同じ市内に住む複数の顧客と共に、顧客ごとに 1 エントリずつ表示されます。空のコレクションは、NULL で置き換えられます。これは、リレーショナル データベースの左外部等結合に相当します。
C#
var q =
from s in db.Suppliers
join c in db.Customers on s.City equals c.City into sc
from x in sc.DefaultIfEmpty()
select new {
Supplier = s.CompanyName,
Customer = x.CompanyName,
City = x.City
}; Visual Basic
Dim customerSuppliers = From sup In db.Suppliers _
Group Join cust In db.Customers _
On sup.City Equals cust.City _
Into supCusts _
Select Supplier = sup, _
CustomerName = supCusts.CompanyName, sup.City 基になる結合演算子のシグネチャは、標準クエリ演算子のドキュメントで定義されています。等結合のみがサポートされるので、等価演算子の 2 つのオペランドの型は同じである必要があります。
プロジェクション
ここまでは、データベース テーブルに直接関連付けられたオブジェクトであるエンティティを取得するクエリだけを取り上げてきました。しかし、これに固執する必要はありません。クエリ言語のすばらしさは、どのような形式でも情報を取得できるという点にあります。クエリ言語では、自動的な変更の追跡や ID 管理の優れた点を利用することはできませんが、必要なデータのみを入手することができます。
たとえば、単に、ロンドン市内に住んでいる顧客の会社名を知りたい場合があります。この場合、会社名を取得するために、顧客オブジェクト全体を取得する必要はありません。プロジェクションを行って、クエリの一部として名前を取得することができます。
C#
var q =
from c in db.Customers
where c.City == "London"
select c.CompanyName;
Visual Basic
Dim londonCustomerNames = From cust In db.Customer _
Where cust.City = "London" _
Select cust.CompanyName この場合、q が文字列シーケンスを取得するクエリになります。
クエリ結果で、名前以外のデータを取得するのに、顧客オブジェクト全体を取得する必要がないと判断する場合は、クエリの一部として結果を作成することで、必要なサブセットを指定できます。
C#
var q =
from c in db.Customers
where c.City == "London"
select new { c.CompanyName, c.Phone }; Visual Basic
Dim londonCustomerInfo = From cust In db.Customer _
Where cust.City = "London" _
Select cust.CompanyName, cust.Phone この例では、"匿名のオブジェクト初期化子" を使用して、会社名と電話番号の両方を保持する構造体を作成しています。この初期化子の型がわからないと思うかもしれませんが、この言語には "暗黙に型指定されたローカル変数宣言" があるので必ずしも型を指定する必要はありません。
C#
var q =
from c in db.Customers
where c.City == "London"
select new { c.CompanyName, c.Phone };
foreach(var c in q)
Console.WriteLine("{0}, {1}", c.CompanyName, c.Phone); Visual Basic
Dim londonCustomerInfo = From cust In db.Customer _
Where cust.City = "London" _
Select cust.CompanyName, cust.Phone
For Each cust In londonCustomerInfo
Console.WriteLine(cust.CompanyName & ", " & cust.Phone)
Next データをすぐに使用する場合は、クエリの結果を保持するクラスを明示的に定義するのではなく、匿名型を使用するのが得策です。
オブジェクト全体の外積を行うこともできますが、その必要が生じることはほとんどありません。
C#
var q =
from c in db.Customers
from o in c.Orders
where c.City == "London"
select new { c, o }; Visual Basic
Dim londonOrders = From cust In db.Customer, _
ord In db.Orders _
Where cust.City = "London" _
Select Customer = cust, Order = ord このクエリでは、顧客オブジェクトと注文オブジェクトのペアのシーケンスを作成しています。
クエリの任意の段階で、プロジェクションを行うこともできます。新しく作成したオブジェクトにデータのプロジェクションを行って、それらのオブジェクトのメンバをその後のクエリ操作で参照することができます。
C#
var q =
from c in db.Customers
where c.City == "London"
select new {Name = c.ContactName, c.Phone} into x
orderby x.Name
select x; Visual Basic
Dim londonItems = From cust In db.Customer _
Where cust.City = "London" _
Select Name = cust.ContactName, cust.Phone _
Order By Name ただし、この段階でパラメータ化されたコンストラクタを使用するには注意が必要です。技術的には妥当ですが、現在の LINQ to SQL では、コンストラクタの内部で使用されている実際のコードがわからない限り、コンストラクタの使用がメンバの状態に与える影響を追跡することはできません。
C#
var q =
from c in db.Customers
where c.City == "London"
select new MyType(c.ContactName, c.Phone) into x
orderby x.Name
select x;
Visual Basic
Dim londonItems = From cust In db.Customer _
Where cust.City = "London" _
Select MyType = New MyType(cust.ContactName, cust.Phone) _
Order By MyType.Name LINQ to SQL では、クエリを純粋なリレーショナル SQL に変換するので、ローカルに定義されたオブジェクト型をサーバー上に実際に作成することはできません。実際には、データがデータベースから返されるまで、どのオブジェクトの作成も保留されます。生成された SQL では、実際のコンストラクタの代わりに通常の SQL の列のプロジェクションが使用されます。クエリ トランスレータではコンストラクタの呼び出し中に何が起きているのかを把握できないので、MyType の Name フィールドの意味を確定することはできません。
最良の方法は、常に "オブジェクト初期化子" を使用してプロジェクションをエンコードすることです。
C#
var q =
from c in db.Customers
where c.City == "London"
select new MyType { Name = c.ContactName, HomePhone = c.Phone } into x
orderby x.Name
select x; Visual Basic
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London" _
Select Contact = New With {.Name = cust.ContactName, _
.Phone = cust.Phone} _
Order By Contact.Name パラメータ化されたコンストラクタを安全に使用できるのは、クエリの最後のプロジェクションのみです。
C#
var e =
new XElement("results",
from c in db.Customers
where c.City == "London"
select new XElement("customer",
new XElement("name", c.ContactName),
new XElement("phone", c.Phone)
)
); Visual Basic
Dim x = <results>
<%= From cust In db.Customers _
Where cust.City = "London" _
Select <customer>
<name><%= cust.ContactName %></name>
<phone><%= cust.Phone %></phone>
</customer>
%>
</results> 必要であれば、次の例のように、オブジェクト コンストラクタを複雑な入れ子にして使用することもできます。次の例では、クエリの結果から直接 XML を作成しています。この方法は、クエリの最後のプロジェクションであれば成功します。
コンストラクタの呼び出しが認識されても、ローカル メソッドに対する呼び出しは認識されない場合があります。最後のプロジェクションでローカル メソッドを呼び出す必要がある場合は、LINQ to SQL でそれを行うことは難しいでしょう。SQL への既知の変換がないメソッド呼び出しは、クエリの一部として使用することはできません。例外として、クエリ変数に依存する引数を持たないメソッド呼び出しを使用することができます。このようなメソッド呼び出しは、変換されたクエリの一部とは見なされず、パラメータとして扱われます。
それでも複雑なプロジェクション (変換) に、ローカルの手続き型のロジックを実装する必要が生じることがあります。独自のローカル メソッドを最後のプロジェクションで使用するには、プロジェクションを 2 回行う必要があります。最初のプロジェクションでは参照に必要なすべてのデータの値を抽出し、2 回目のプロジェクションでは変換を行います。これら 2 つのプロジェクションを仲介するのは、LINQ to SQL クエリからローカルに実行されるクエリへと、呼び出し時点で処理を切り替える AsEnumerable() 演算子の呼び出しです。
C#
var q =
from c in db.Customers
where c.City == "London"
select new { c.ContactName, c.Phone };
var q2 =
from c in q.AsEnumerable()
select new MyType {
Name = DoNameProcessing(c.ContactName),
Phone = DoPhoneProcessing(c.Phone)
}; Visual Basic
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London" _
Select cust.ContactName, cust.Phone
Dim processedCustomers = From cust In londonCustomers.AsEnumerable() _
Select Contact = New With { _
.Name = DoNameProcessing(cust.ContactName), _
.Phone = DoPhoneProcessing(cust.Phone)} 注 ToList() や ToArray() と異なり、AsEnumerable() 演算子を呼び出してもクエリは実行されません。クエリの実行は保留されます。AsEnumerable() 演算子はクエリの静的な型指定を単に変更するものですが、IQueryable<T> (Visual Basic の IQueryable (ofT)) を IEnumerable<T> (Visual Basic の IEnumerable (ofT)) に変換し、コンパイラを巧みに動作させて残りのクエリがローカルで実行されるようにします。
コンパイルされたクエリ
構造が類似したクエリを何度も実行することは、多くのアプリケーションでは一般的です。そのような場合、1 度クエリをコンパイルしてから、別のパラメータを使用してそのクエリをアプリケーションで何度も実行することによって、パフォーマンスを向上できます。LINQ to SQL では、この結果を取得するのに、CompiledQuery クラスを使用します。コンパイルされたクエリの定義方法を次のコードに示します。
C#
static class Queries
{
public static Func<Northwind, string, IQueryable<Customer>>
CustomersByCity = CompiledQuery.Compile((Northwind db, string city) =>
from c in db.Customers where c.City == city select c);
} Visual Basic
Class Queries
public Shared Function(Of Northwind, String, IQueryable(Of Customer)) _ CustomersByCity = CompiledQuery.Compile( _
Function(db As Northwind, city As String) _
From cust In db.Customers Where cust.City = city)
End Class Compile メソッドは、入力パラメータを変更するだけで、その後、何度も実行できる、キャッシュ可能なデリゲート型を返します。その例を次のコードに示します。
C#
public IEnumerable<Customer> GetCustomersByCity(string city) {
Northwind db = new Northwind();
return Queries.CustomersByCity(myDb, city);
} Visual Basic
Public Function GetCustomersByCity(city As String) _
As IEnumerable(Of Customer)
Dim db As Northwind = New Northwind()
Return Queries.CustomersByCity(myDb, city)
End Function SQL 変換
実際にクエリを実行するのは、LINQ to SQL ではなくリレーショナル データベースです。LINQ to SQL によって行われるのは、記述したクエリをそれと同等の SQL クエリに変換し、サーバーに送信して、サーバーで処理されるようにすることです。LINQ to SQL では、実行が先送りされるので、クエリが複数のパートから構成されている場合でもクエリ全体を確認することができます。
リレーショナル データベース サーバーでは実際には IL が実行されていないので (SQL Server 2005 の CLR 統合は除きます)、クエリは IL としてサーバーに送信されるわけではありません。クエリは、パラメータ化された SQL クエリとしてテキスト形式で送信されます。
もちろん、SQL では、CLR 統合を使用した T-SQL でも、ローカルにプログラムで使用できるさまざまなメソッドを実行することはできません。したがって、SQL の環境で使用できるものと同等の操作および関数に変換できるクエリを記述する必要があります。
.NET Framework の組み込み型の多くのメソッドと演算子は、直接 SQL に変換できます。使用可能な関数から生成できるものもあります。変換できないものは使用できません。それらを使用しようとすると、ランタイム例外が生成されます。SQL への変換を行うために実装されているフレームワーク メソッドの詳細については、このドキュメントの後半で説明します。
エンティティのライフサイクル
LINQ to SQL は、単なるリレーショナル データベース用の標準クエリ演算子の実装ではありません。クエリを変換するだけではなく、オブジェクトをそのライフサイクル全体にわたって管理するサービスで、データの整合性の管理や変更をストアに反映するプロセスの自動化を支援します。
一般的なシナリオでは、オブジェクトは、1 つ以上のクエリによって取得され、その後、アプリケーションにより、なんらかの方法で操作され、サーバーに変更が送信されます。このプロセスは、アプリケーションでこの情報を使用しなくなるまで、何回も繰り返される場合があります。その時点では、オブジェクトは通常のオブジェクトと同様にランタイムによって再利用されます。ただし、データはデータベースに残ります。オブジェクトがランタイムから消去された後でも、同じデータを表すオブジェクトを取得できます。つまり、実際のオブジェクトのライフサイクルは、単一のランタイムの期間を超えて存在します。
この章では、"エンティティ ライフサイクル" に重点を置いて説明します。エンティティ ライフサイクルでは、1 つのサイクルが、特定のランタイムのコンテキスト内のエンティティ オブジェクトの 1 つの期間になります。このサイクルは、DataContext が新しいインスタンスを認識したときに開始し、オブジェクトまたは DataContext が必要なくなったときに終了します。
変更の追跡
- エンティティをデータベースから取得したら、取得したエンティティは好きなように操作できます。取得したエンティティはユーザーのオブジェクトなので、自由に使用できます。LINQ to SQL では、SubmitChanges() が呼び出されたらデータベースに変更内容を反映できるように、エンティティの操作による変更を追跡します。
LINQ to SQL は、ユーザーがエンティティを操作する前、データベースから取得した瞬間に、エンティティの追跡を開始します。実は、前に説明した "ID 管理サービス" も既に開始されています。実際に変更を開始するまでは、変更の追跡による追加のオーバヘッドはほとんどありません。
C#
Customer cust = db.Customers.Single(c => c.CustomerID == "ALFKI");
cust.CompanyName = "Dr. Frogg's Croakers";
Visual Basic
' 特定の顧客をクエリします。
Dim id As String = "ALFKI"
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = id).First
targetCustomer.CompanyName = "Dr. Frogg's Croakers" 上記の例で CompanyName が割り当てられると、LINQ to SQL は、その変更をすぐに認識し、変更内容を記録することができます。すべてのデータ メンバの元の値は、"変更追跡サービス" によって保持されます。
変更追跡サービスでは、すべてのリレーションシップ プロパティの操作も記録します。エンティティはデータベースのキー値によってリンクされている可能性がありますが、リレーションシップ プロパティを使用して、エンティティ間のリンクを確立します。キー列に関連付けられたメンバを直接変更する必要はありません。変更が送信される前に、LINQ to SQL によって、変更が自動的に同期されます。
C#
Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);
foreach (Order o in db.Orders.Where(o => o.CustomerID == custId2)) {
o.Customer = cust1;
} Visual Basic
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
For Each ord In (From o In db.Orders _
Where o.CustomerID = custId2)
o.Customer = targetCustomer
Next Customer プロパティへの割り当てを作成するだけで、顧客間で注文を移動できます。リレーションシップは顧客と注文の間に存在するため、どちらかを変更すると、リレーションシップを変更できます。次に示すように、簡単に cust2 の Orders コレクションから注文を削除して、cust1 の Orders コレクションに注文を追加できます。
C#
Customer cust1 = db.Customers.Single(c => c.CustomerID == custId1);
Customer cust2 = db.Customers.Single(c => c.CustomerID == custId2);
// 注文を取り出します。
Order o = cust2.Orders[0];
// ある顧客から注文を削除し、その注文を別の顧客に追加します。
cust2.Orders.Remove(o);
cust1.Orders.Add(o);
// "true" を表示します。
Console.WriteLine(o.Customer == cust1);
Visual Basic
Dim targetCustomer1 = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
Dim targetCustomer2 = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
' 注文を取り出します。
Dim o As Order = targetCustomer2.Orders(0)
' ある顧客から注文を削除し、その注文を別の顧客に追加します。
targetCustomer2.Orders.Remove(o)
targetCustomer1.Orders.Add(o)
' "True" を表示します。
MsgBox(o.Customer = targetCustomer1) もちろん、リレーションシップに NULL 値を割り当てると、リレーションシップが完全に消去されます。注文の Customer プロパティに NULL を割り当てると、注文が顧客の一覧から削除されます。
C#
Customer cust = db.Customers.Single(c => c.CustomerID == custId1);
// 注文を取り出します。
Order o = cust.Orders[0];
// NULL 値を割り当てます。
o.Customer = null;
// "false" を表示します。
Console.WriteLine(cust.Orders.Contains(o));
Visual Basic
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
' 注文を取り出します。
Dim o As Order = targetCustomer.Orders(0)
' NULL 値を割り当てます。
o.Customer = Nothing
' "False" を表示します。
Msgbox(targetCustomer.Orders.Contains(o)) オブジェクト グラフの一貫性を保持するためには、リレーションシップを構成する両側を自動更新することが不可欠です。通常のオブジェクトとは異なり、データ間のリレーションシップは、多くの場合、双方向になっています。LINQ to SQL では、プロパティを使用してリレーションシップを表すことができます。ただし、これらの双方向のプロパティの状態を自動的に同期するサービスは提供されていません。これは、クラスの定義で直接実装される必要があるレベルのサービスです。コード生成ツールを使用して生成されたエンティティのクラスには、この機能が含まれます。次の章で、手動でゼロから記述したクラスに、この処理を実装する方法について説明します。
ただし、リレーションシップを削除しても、データベースからオブジェクトが削除されるわけではない点に注意する必要があります。基になるデータのライフサイクルは、行がテーブルから削除されるまでは、データベースで続いていることを忘れないでください。実際にオブジェクトを削除する唯一の方法は、Table コレクションからオブジェクトを削除することです。
C#
Customer cust = db.Customers.Single(c => c.CustomerID == custId1);
// 注文を取り出します。
Order o = cust.Orders[0];
// テーブルから直接削除します (このデータは必要ない、ということです)。
db.Orders.Remove(o);
// "false" を表示します。つまり、顧客の Orders コレクションから削除します。
Console.WriteLine(cust.Orders.Contains(o));
// "true" を表示します。つまり、注文がその顧客からデタッチされます。
Console.WriteLine(o.Customer == null);
Visual Basic
Dim targetCustomer = (From cust In db.Customers _
Where cust.CustomerID = custId1).First
' 注文を取り出します。
Dim o As Order = targetCustomer.Orders(0)
' テーブルから直接削除します (このデータは必要ない、ということです)。
db.Orders.Remove(o)
' "False" を表示します。つまり、顧客の Orders コレクションから削除します。
Msgbox(targetCustomer.Orders.Contains(o))
' "True" を表示します。つまり、注文がその顧客からデタッチされます。
Msgbox(o.Customer = Nothing) 他のすべての変更と同様に、注文は実際には削除されていません。注文が削除され、残りのオブジェクトからデタッチされたため、削除されたように見えるだけです。order オブジェクトが Orders テーブルから削除されると、変更追跡サービスによって、その order オブジェクトは削除する対象としてマークされますが、実際にデータベースから削除されるのは、SubmitChanges() の呼び出しで変更が送信されたときです。オブジェクト自体が削除されることはありません。ランタイムによってオブジェクト インスタンスのライフサイクルが管理されるため、オブジェクトは、参照されている限り存続します。ただし、オブジェクトがテーブルから削除され、変更が送信されると、それ以降、そのオブジェクトは、変更追跡サービスによって追跡されなくなります。
これ以外に、エンティティが追跡されないのは、DataContext によって認識される前にエンティティが存在している場合だけです。この現象は、コードで新しいオブジェクトを作成するときに必ず発生します。アプリケーションでは、データベースからエンティティ クラスのインスタンスを取得することなく、これらのインスタンスを自由に使用できます。変更の追跡と ID 管理は、DataContext で認識されているオブジェクトのみが対象となります。したがって、新規に作成されたインスタンスを DataContext に追加するまでは、どちらのサービスも新規インスタンスに対して有効になりません。
新しいインスタンスは、次の 2 つのうちどちらかの方法で追加できます。1 つは、関連付けられた Table コレクションで、Add() メソッドを手動で呼び出す方法です。
C#
Customer cust =
new Customer {
CustomerID = "ABCDE",
ContactName = "Frond Smooty",
CompanyTitle = "Eggbert's Eduware",
Phone = "888-925-6000"
};
// Customers テーブルに新しい顧客を追加します。
db.Customers.Add(cust); Visual Basic
Dim targetCustomer = New Customer With { _
.CustomerID = “ABCDE”, _
.ContactName = “Frond Smooty”, _
.CompanyTitle = “Eggbert’s Eduware”, _
.Phone = “888-925-6000”}
' Customers テーブルに新しい顧客を追加します。
db.Customers.Add(cust) もう 1 つは、DataContext によって既に認識されているオブジェクトに、新しいインスタンスをアタッチする方法です。
C#
// 顧客の Orders コレクションに注文を追加します。
cust.Orders.Add(
new Order { OrderDate = DateTime.Now }
); Visual Basic
' 顧客の Orders コレクションに注文を追加します。
targetCustomer.Orders.Add( _
New Order With { .OrderDate = DateTime.Now } ) 新しいオブジェクト インスタンスは、他の新しいインスタンスにアタッチされている場合でも、DataContext によって検出されます。
C#
// 顧客の Orders コレクションに注文と詳細情報を追加します。
Cust.Orders.Add(
new Order {
OrderDate = DateTime.Now,
OrderDetails = {
new OrderDetail {
Quantity = 1,
UnitPrice = 1.25M,
Product = someProduct
}
}
}
); Visual Basic
' 顧客の Orders コレクションに注文と詳細情報を追加します。
targetCustomer.Orders.Add( _
New Order With { _
.OrderDate = DateTime.Now, _
.OrderDetails = New OrderDetail With { _
.Quantity = 1,
.UnitPrice = 1.25M,
.Product = someProduct
}
} ) 基本的に、Add() メソッドを呼び出したかどうかに関係なく、DataContext では、新しいインスタンスとして現在追跡されていない、オブジェクト グラフ内のすべてのエンティティを認識します。
読み取り専用の DataContext の使用
多くのシナリオでは、データベースから取得したエンティティを更新する必要はありません。Customers のテーブルを Web ページに表示することが、その好例です。そのようなすべての場合、DataContext に、エンティティへの変更を追跡しないように指示することで、パフォーマンスを向上することができます。この操作は、次のコードに示すように、DataContext の ObjectTracking プロパティを false に指定して実行できます。
C#
db.ObjectTracking = false;
var q = db.Customers.Where( c => c.City = "London");
foreach(Customer c in q)
Display(c); Visual Basic
db.ObjectTracking = False
Dim londonCustomers = From cust In db.Customer _
Where cust.City = "London"
For Each c in londonCustomers
Display(c)
Next 変更の送信
オブジェクトに対して行う変更の数に関係なく、オブジェクトへの変更は、メモリ内のレプリカのみに対して行われます。データベース内の実際のデータには、まだ何も行われていません。この情報は、DataContext で SubmitChanges() を呼び出して、明示的に要求するまで、サーバーに転送されません。
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// ここで、変更を行います。
db.SubmitChanges(); Visual Basic
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' ここで、変更を行います。
db.SubmitChanges() SubmitChanges() を呼び出すと、DataContext は、すべての変更を同等の SQL コマンドに変換し、対応するテーブルで挿入、更新、または削除を試みます。この操作は、必要であれば独自のカスタム ロジックによってオーバーライドすることができます。ただし、送信の順序は "変更プロセッサ" と呼ばれる DataContext のサービスによって調整されます。
SubmitChanges() を呼び出すと、まず、既存のオブジェクトのセットが調査され、新しいインスタンスがオブジェクトにアタッチされているかどうかが判断されます。この新しいインスタンスは、追跡対象のオブジェクトのセットに追加されます。次に、変更が保留中になっているすべてのオブジェクトは、オブジェクト間の依存関係に基づいて、順序付けされます。変更が他のオブジェクトに依存しているオブジェクトは、依存関係に従って順序付けされます。データベースの外部キー制約と一意性制約は、変更の正しい順序を決定する上で大きな役割を果たします。その後、実際に変更が転送される直前に、トランザクションが既にスコープ内にある場合を除いては、トランザクションが開始され、一連の個々のコマンドをカプセル化します。最後に、オブジェクトへの変更が SQL コマンドに 1 つずつ変換され、サーバーに送信されます。
この時点で、データベースでエラーが検出されると、送信プロセスが中断し、例外が発生します。データベースへのすべての変更はロールバックされ、変更の送信は行われなかったことになります。DataContext では、すべての変更の完全な記録が保持されるため、再度 SubmitChanges() を呼び出すことで、問題を修正して、変更を再送信することができます。
C#
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// ここで、変更を行います。
try {
db.SubmitChanges();
}
catch (Exception e) {
// 調整を行います。
...
// 再試行します。
db.SubmitChanges();
} Visual Basic
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' ここで、変更を行います。
Try
db.SubmitChanges()
Catch e As Exception
' 調整を行います。
...
' 再試行します。
db.SubmitChanges()
End Try 送信のトランザクションが正常に完了したら、DataContext では、変更の追跡情報を消去することで、オブジェクトへの変更を受け付けます。
同時変更
SubmitChanges() の呼び出しの失敗には、さまざまな理由が考えられます。既に使用されているか、データベースの CHECK 制約に違反している値を指定した、無効な主キーを使用してオブジェクトを作成した可能性があります。このようなチェックでは、多くの場合、全体的なデータベースの状態を完全に把握する必要があるため、ビジネス ロジックに組み込むことは困難です。ただし、ほとんどの場合の失敗理由は、単純に、他のユーザーによって事前にオブジェクトが変更された、ということです。
データベースで各オブジェクトをロックしていて、完全にシリアル化されたトランザクションを使用している場合には、オブジェクトを変更することはできません。ただし、このようなプログラミング (ペシミスティック同時実行制御) は、コストがかかるうえに実際のクラッシュはめったに発生しないため、ほとんど使用されていません。同時変更を管理する最も一般的な方法は、"オプティミスティック同時実行制御" を使用する方法です。このモデルでは、データベースの行に対するロックがまったく行われません。つまり、最初にオブジェクトを取得してから変更を送信するまでの間に、データベースが何度も変更されている可能性があります。
したがって、最後の更新が受け付けられて、それ以前の他の変更が取り消されるというポリシーに従う必要がない限り、他のユーザーによってデータが変更されたことを通知するのが適切な処理でしょう。
DataContext には、オプティミスティック同時実行制御に対するサポートが組み込まれていて、変更の競合が自動的に検出されます。個別の更新は、データベースの現在の状態が、最初にオブジェクトを取得したときのデータの状態と一致する場合にのみ、正常に行われます。更新はオブジェクトごとに行われ、更新対象のオブジェクトが、更新されている場合にのみ違反が通知されます。
エンティティ クラスを定義するときに、DataContext によって変更の競合が検出される程度を制御できます。各 Column 属性には、UpdateCheck というプロパティがあり、Always、Never、WhenChanged という 3 つの値のいずれかを割り当てることができます。このプロパティ値が設定されていない場合、Column 属性には既定値の Always が設定されます。つまり、バージョン スタンプなどの明らかな情報がない限り、そのメンバによって示されるデータ値では常に競合がチェックされます。Column 属性には IsVersion というプロパティがあります。このプロパティを使用すると、データ値が、データベースで保持されるバージョン スタンプを構成するかどうかを指定できます。バージョンが存在する場合には、競合が発生しているかどうかを判断するのに、バージョン情報が単独で使用されます。
変更の競合が発生している場合、その他のエラーの発生時と同じように例外がスローされます。送信に関するトランザクションは中止されますが、DataContext はそのままの状態が維持されるので、問題を修正して、変更を再送信することができます。
C#
while (retries < maxRetries) {
Northwind db = new Northwind("c:\\northwind\\northwnd.mdf");
// ここで、オブジェクトをフェッチして変更を行います。
try {
db.SubmitChanges();
break;
}
catch (ChangeConflictException e) {
retries++;
}
} Visual Basic
Do While retries < maxRetries
Dim db As New Northwind("c:\northwind\northwnd.mdf")
' ここで、オブジェクトをフェッチして変更を行います。
Try
db.SubmitChanges()
Exit Do
catch cce As ChangeConflictException
retrie