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
retries += 1
End Try
Loop
中間層またはサーバーで変更を行っている場合、変更の競合を修正するために実行できる最も簡単な方法は、単純に最初からやり直して再試行し、コンテキストを再作成して変更を再適用することです。他のオプションについては、これ以降のセクションで説明します。
トランザクション
トランザクションとは、データベースやその他のリソース マネージャによって提供されるサービスで、一連の個別の操作が自動的に行われることを保証するのに使用されます。つまり、それら一連の操作は、すべて成功するかすべて失敗するかのいずれかになります。すべて失敗する場合、その操作は、その他の操作が行われる前にすべて自動的に取り消されます。トランザクションがスコープ内に存在していない場合、SubmitChanges() の呼び出し時に、更新を保護するために DataContext によってデータベース トランザクションが自動的に開始されます。
トランザクションを自分で開始して、使用するトランザクションの種類、分離レベル、実際に含まれる処理を制御することもできます。DataContext によって使用されるトランザクション分離レベルは、ReadCommitted と呼ばれます。
C#
Product prod = db.Products.Single(p => p.ProductID == 15);
if (prod.UnitsInStock > 0)
prod.UnitsInStock--;
using(TransactionScope ts = new TransactionScope()) {
db.SubmitChanges();
ts.Complete();
}
Visual Basic
Dim product = (From prod In db.Products _
Where prod.ProductID = 15).First
If product.UnitsInStock > 0) Then
product.UnitsInStock -= 1
End If
Using ts As TransactionScope = New TransactionScope())
db.SubmitChanges()
ts.Complete()
End Using
上記の例では、新しいトランザクション スコープ オブジェクトを作成することで、完全にシリアル化されたトランザクションが開始されます。トランザクションのスコープ内で実行されるすべてのデータベース コマンドは、トランザクションによって保護されます。
C#
Product prod = db.Products.Single(p => p.ProductId == 15);
if (prod.UnitsInStock > 0)
prod.UnitsInStock--;
using(TransactionScope ts = new TransactionScope()) {
db.ExecuteCommand("exec sp_BeforeSubmit");
db.SubmitChanges();
ts.Complete();
}
Visual Basic
Dim product = (From prod In db.Products _
Where prod.ProductID = 15).First
If product.UnitsInStock > 0) Then
product.UnitsInStock -= 1
End If
Using ts As TransactionScope = New TransactionScope())
db.ExecuteCommand(“exec sp_BeforeSubmit”)
db.SubmitChanges()
ts.Complete()
End Using
同じ例を少し変更した上記のコードでは、DataContext で ExecuteCommand() メソッドを使用し、変更が送信される直前にデータベースでストアド プロシージャを実行します。ストアド プロシージャによるデータベースの処理に関係なく、その操作が同じトランザクションの一部であることを確信できます。
トランザクションが正常に完了すると、DataContext によって、蓄積された追跡情報がすべて破棄され、エンティティの新しい状態が変更されていないものとして扱われます。ただし、トランザクションが失敗しても、オブジェクトへの変更はロールバックされません。これにより、変更の送信中に発生した問題に対処する際の柔軟性を最大限に引き出すことができます。
また、新しい TransactionScope の代わりに、SQL のローカル トランザクションを使用することもできます。LINQ to SQL では、LINQ to SQL 機能を既存の ADO.NET アプリケーションに統合するのを支援するために、この機能を提供しています。ただし、この方法を使用する場合には、より多くの操作を手動で行う必要があります。
C#
Product prod = q.Single(p => p.ProductId == 15);
if (prod.UnitsInStock > 0)
prod.UnitsInStock--;
db.Transaction = db.Connection.BeginTransaction();
try {
db.SubmitChanges();
db.Transaction.Commit();
}
catch {
db.Transaction.Rollback();
throw;
}
finally {
db.Transaction = null;
}
Visual Basic
Dim product = (From prod In db.Products _
Where prod.ProductID = 15).First
If product.UnitsInStock > 0) Then
product.UnitsInStock -= 1
End If
db.Transaction = db.Connection.BeginTransaction()
Try
db.SubmitChanges()
db.Transaction.Commit()
catch e As Exception
db.Transaction.Rollback()
Throw e
Finally
db.Transaction = Nothing
End Try
ご覧のとおり、手動で制御されたデータベース トランザクションを使用するのは少し複雑です。ユーザーがトランザクションを開始する必要があるだけでなく、Transaction プロパティにトランザクションを割り当てることで、DataContext によって、そのトランザクションが使用されるように明示的に指定する必要があります。その後、try-catch ブロックを使用して送信ロジックを組み込み、トランザクションがコミットされ、DataContext によって変更が受け付けられるように明示的に指定し、トランザクションが失敗した場合には、トランザクションが中止されるようにする必要があります。また、終了時には必ず、Transaction プロパティの値を NULL に戻す必要があります。
ストアド プロシージャ
SubmitChanges() が呼び出されると、LINQ to SQL によって SQL コマンドが生成および実行され、データベースで行の挿入、更新、削除が行われます。これらの操作は、アプリケーション開発者によってオーバーライドすることが可能で、必要な操作を実行するためにカスタム コードを使用することができます。このように、データベース ストアド プロシージャなどの代替機能は、"プロセッサの変更" によって自動的に呼び出すことができます。
Northwind サンプル データベースの Products テーブルの在庫数を更新するストアド プロシージャについて考えてみましょう。プロシージャの SQL 宣言は、次のとおりです。
SQL
create proc UpdateProductStock
@id int,
@originalUnits int,
@decrement int
as
厳密に型指定された DataContext でメソッドを定義することで、通常の自動生成の更新コマンドの代わりにストアド プロシージャを使用できます。DataContext クラスが LINQ to SQL のコード生成ツールによって自動生成されている場合でも、ユーザー独自の部分クラスでこれらのメソッドを指定できます。
C#
public partial class Northwind : DataContext
{
...
public void UpdateProduct(Product original, Product current) {
// UnitsInStock を更新するストアド プロシージャを実行します。
if (original.UnitsInStock != current.UnitsInStock) {
int rowCount = this.ExecuteCommand(
"exec UpdateProductStock " +
"@id={0}, @originalUnits={1}, @decrement={2}",
original.ProductID,
original.UnitsInStock,
(original.UnitsInStock - current.UnitsInStock)
);
if (rowCount < 1)
throw new Exception("Error updating");
}
...
}
}
Visual Basic
Partial Public Class Northwind
Inherits DataContext
...
Public Sub UpdateProduct(original As Product, current As Product)
‘ UnitsInStock を更新するストアド プロシージャを実行します。
If original.UnitsInStock <> current.UnitsInStock Then
Dim rowCount As Integer = ExecuteCommand( _
"exec UpdateProductStock " & _
"@id={0}, @originalUnits={1}, @decrement={2}", _
original.ProductID, _
original.UnitsInStock, _
(original.UnitsInStock - current.UnitsInStock) )
If rowCount < 1 Then
Throw New Exception(“Error updating”)
End If
End If
...
End Sub
End Class
メソッドのシグネチャとジェネリック パラメータによって、DataContext では、生成された更新ステートメントではなく、このメソッドを使用するように指定されます。LINQ to SQL では、指定された型のオブジェクトの元のコピーと現在のコピーを渡すために、元のパラメータと現在のパラメータを使用しています。この 2 つのパラメータは、オプティミスティック同時実行制御の競合の検出に使用できます。
注 既定の更新ロジックをオーバーライドする場合、競合の検出は自身の責任で行う必要があります。
ストアド プロシージャ UpdateProductStock は、DataContext の ExecuteCommand() メソッドを使用して呼び出されます。これにより、処理された行数が返され、次のシグネチャが含まれます。
C#
public int ExecuteCommand(string command, params object[] parameters);
Visual Basic
Public Function ExecuteCommand(command As String, _
ParamArray parameters() As Object) As Integer
オブジェクト配列は、command の実行に必要なパラメータを渡すために使用されます。
更新のメソッドと同様に、挿入と削除のメソッドが指定される場合があります。挿入と削除のメソッドは、更新されるエンティティ型のパラメータを 1 つだけ受け取ります。たとえば、Product インスタンスを挿入および削除するメソッドは、次のように指定できます。
C#
public void InsertProduct(Product prod) { ... }
public void DeleteProudct(Product prod) { ...}
Visual Basic
Public Sub InsertProduct(prod As Product) ...
Public Sub DeleteProudct(prod As Product) ...
エンティティ クラスの詳細
属性の使用
エンティティ クラスは、アプリケーションの一部として定義する通常のオブジェクト クラスと同様です。ただし、特定のデータベース テーブルに関連付ける特別な情報で注釈が付けられているという点が異なります。このような注釈は、クラスの宣言でカスタム属性として付けられます。このカスタム属性は、クラスを LINQ to SQL と共に使用するときにのみ有効です。このカスタム属性は、.NET Framework の XML シリアル化属性に似ています。この "データ" 属性によって、オブジェクトのクエリをデータベースに対する SQL クエリに変換し、オブジェクトへの変更を SQL の挿入コマンド、更新コマンド、および削除コマンドに変換するのに十分な情報が、LINQ to SQL に提供されます。
また、属性の代わりに XML マッピング ファイルを使用して、マッピング情報を示すこともできます。このシナリオの詳細については、「外部マッピング」を参照してください。
Database 属性
Database 属性は、データベースの既定の名前が接続によって指定されていない場合に指定するのに使用します。Database 属性は、厳密に型指定された DataContext 宣言に適用できます。この属性は、省略可能です。
Database 属性
| プロパティ | データ型 | 説明 |
| Name | String | データベースの名前を指定します。この情報は、接続によってデータベース名が指定されない場合にのみ使用されます。この Database 属性がコンテキストの宣言に存在せず、データベース名が接続によって指定されない場合、データベースの名前はコンテキストのクラスと同じ名前であると見なされます。 |
C#
[Database(Name="Database#5")]
public class Database5 : DataContext {
...
}
Visual Basic
<Database(Name:="Database#5")> _
Public Class Database5
Inherits DataContext
...
End Class
Table 属性
Table 属性は、クラスを、データベース テーブルに関連付けられたエンティティ クラスとして指定するのに使用します。Table 属性が適用されたクラスは、LINQ to SQL で扱われます。
Table 属性
| プロパティ | データ型 | 説明 |
| Name | String | テーブルの名前を指定します。この情報が指定されない場合、テーブルの名前はエンティティ クラスと同じ名前であると見なされます。 |
C#
[Table(Name="Customers")]
public class Customer {
...
}
Visual Basic
<Table(Name:="Customers")> _
Public Class Customer
...
End Class
Column 属性
Column 属性は、データベース テーブルの列を表すエンティティ クラスのメンバを指定するのに使用します。この属性は、任意のフィールドまたはプロパティ (パブリック、プライベート、または内部) に適用できます。LINQ to SQL によってデータベースに変更が保存されるときに、列として識別されたメンバのみが保存されます。
Column 属性
| プロパティ | データ型 | 説明 |
| Name | String | テーブルまたはビューの列の名前です。指定されない場合、その列の名前はクラス メンバと同じ名前であると見なされます。 |
| Storage | String | 基になるストレージの名前です。このプロパティでは、LINQ to SQL に対して、データ メンバのパブリックなプロパティ アクセサを使用せずに、未処理の値自体を操作する方法を指定します。指定されない場合、LINQ to SQL は、パブリック アクセサを使用して値を取得および設定します。 |
| DBType | String | データベースの種類と修飾子を使用して指定されたデータベース列の種類です。これは、T-SQL テーブルの宣言のコマンドで列を定義するのに使用する文字列と同一です。指定されない場合、データベース列の種類はメンバの種類から推論されます。特定のデータベースの種類は、データベースのインスタンスの作成に CreateDatabase() メソッドが使用される場合にのみ必要となります。 |
| IsPrimaryKey | Bool | true が設定された場合、クラス メンバは、テーブルの主キーの一部である列を表します。クラスのメンバが 1 つ以上 ID として指定されると、主キーは、関連付けられた列の複合として指定されます。 |
| IsDbGenerated | Boolean | メンバの列値がデータベースによって自動生成されることを識別します。IsDbGenerated=true が指定された主キーは、DBType が IDENTITY 修飾子に指定されている必要があります。IsDbGenerated メンバは、データ行の挿入後すぐに同期され、SubmitChanges() が完了すると使用可能になります。 |
| IsVersion | Boolean | メンバの列の種類を、データベースのタイムスタンプまたはバージョン番号として識別します。関連付けられた行が更新されるたびに、バージョン番号がインクリメントされ、データベースによってタイムスタンプ列が更新されます。IsVersion=true が指定されたメンバは、データ行の更新後すぐに同期されます。新しい値は、SubmitChanges() が完了すると表示されます。 |
| UpdateCheck | UpdateCheck | LINQ to SQL によって、"オプティミスティック同時実行制御" の競合の検出がどのように実装されるかを示します。IsVersion=true が指定されているメンバがない場合、検出は、元のメンバ値を現在のデータベース状態と比較することで実行されます。各メンバに UpdateCheck 列挙値を指定することで、競合の検出時に、LINQ to SQL によってどのメンバが使用されるかを制御できます。
-
Always: 競合の検出には常にこの列を使用します。
-
Never: 競合の検出にはこの列を使用しません。
-
WhenChanged: メンバがアプリケーションによって変更されたときにのみ、この列を使用します。
|
| IsDiscriminator | Boolean | クラス メンバで、継承階層の識別子の値が保持されるかどうかを特定します。 |
| Expression | String | LINQ to SQL の操作には影響しません。ただし、CreateDatabase() の実行中に、計算列式を表す未処理の SQL 式として使用されます。 |
| CanBeNull | Boolean | 値に NULL 値を含められることを示します。これは、通常、エンティティのメンバの CLR 型から推論されます。この属性を使用して、文字列値が、データベースで NULL 非許容の列として表されることを示します。 |
| AutoSync | AutoSync | 挿入コマンドまたは更新コマンドの実行時に、データベースによって生成された値で列が自動的に同期されるかどうかを指定します。有効な値は、OnInsert、Always、および Never です。 |
一般的なエンティティ クラスは、パブリック プロパティで Column 属性を使用し、実際の値をプライベート フィールドに格納します。
C#
private string _city;
[Column(Storage="_city", DBType="NVarChar(15)")]
public string City {
get { ... }
set { ... }
}
Visual Basic
Private _city As String
<Column(Storage:="_city", DBType:="NVarChar(15)")> _
public Property City As String
Get
set
End Property
DBType は、CreateDatabase() メソッドで最も明確な型のテーブルを構築できるようにするという目的のためだけに指定されています。それ以外の場合、基になる列が 15 文字に制限されているという情報は使用されません。
あるデータベースの種類の主キーを表すメンバは、多くの場合、自動生成された値に関連付けられます。
C#
private string _orderId;
[Column(Storage="_orderId", IsPrimaryKey=true, IsDbGenerated = true,
DBType="int NOT NULL IDENTITY")]
public string OrderId {
get { ... }
set { ... }
}
Visual Basic
Private _orderId As String
<Column(Storage:="_orderId", IsPrimaryKey:=true, _
IsDbGenerated:= true, DBType:="int NOT NULL IDENTITY")> _
public Property OrderId As String
Get
Set
End Property
DBType を指定する場合には、IDENTITY 修飾子を必ず含めるようにします。LINQ to SQL では、ユーザーによって指定された DBType は拡大されません。ただし、DBType が指定されない場合、CreateDatabase() メソッドを使用してデータベースを作成するときに IDENTITY 修飾子が必要であることが、LINQ to SQL によって推論されます。
同様に、IsVersion プロパティが true の場合、DBType では、バージョン番号またはタイムスタンプ列を指定するための適切な修飾子を指定する必要があります。DBType が指定されていない場合、LINQ to SQL によって適切な修飾子が推論されます。
自動生成の列、バージョン スタンプ、その他の非表示にする必要がある列に関連付けられたメンバへのアクセスは、メンバのアクセス レベルを指定するか、アクセサ自体を制限することで制御できます。
C#
private string _customerId;
[Column(Storage="_customerId", DBType="NCHAR(5) ")]
public string CustomerID {
get { ... }
}
Visual Basic
Private _customerId As String
<Column(Storage:="_customerId", DBType:="NCHAR(5)")> _
Public Property CustomerID As String
Get
End Property
Order の CustomerID プロパティは、set アクセサを定義しないことで、読み取り専用にできます。LINQ to SQL では、基になる値をストレージのメンバから取得および設定することができます。
また、Column 属性をプライベート メンバに配置することで、アプリケーションの他の部分から、完全にアクセスできないようにメンバを設定することもできます。これにより、エンティティ クラスでは、クラスのビジネス ロジックに関連する情報を全体に公開することなく含めることができます。プライベート メンバは変換されたデータの一部ですが、プライベートであるため、統合言語クエリで参照することはできません。
既定では、オプティミスティック同時実行制御の競合の検出を実行するためにすべてのメンバが使用されます。UpdateCheck 値を指定することで、特定のメンバを使用するかどうかを制御できます。
C#
[Column(Storage="_city", UpdateCheck=UpdateCheck.WhenChanged)]
public string City {
get { ... }
set { ... }
}
Visual Basic
<Column(Storage:="_city", UpdateCheck:=UpdateCheck.WhenChanged)> _
Public Property City As String
Get
Set
End Property
次の表では、データベースの種類と対応する CLR 型の間の許容されるマッピングを示します。この表は、特定のデータベース列を表すために使用する CLR 型を特定する際のガイドとして使用してください。
データベースの種類と対応する CLR 型の許容されるマッピング
| データベースの種類 | .NET CLR 型 | コメント |
| bit、tinyint、smallint、int、bigint | Bye、Int16、Uint16、Int32、Uint32、Int64、Uint64 | 損失を伴う変換となる可能性があります。値がラウンド トリップしない場合があります。 |
| bit | Boolean | |
| decimal、numeric、smallmoney、money | Decimal | 小数部桁数の違いにより、損失を伴う変換となる可能性があります。ラウンド トリップしない場合があります。 |
| real、float | Single、Double | 精度の違いがあります。 |
| char、varchar、text、nchar、nvarchar、ntext | String | ロケールが異なる可能性があります。 |
| datetime、smalldatetime | DateTime | 精度の違いにより、損失を伴う変換およびラウンド トリップの問題が発生する場合があります。 |
| uniqueidentifier | Guid | 照合規則が異なります。並べ替えが正常に機能しない場合があります。 |
| timestamp | Byte[] (Visual Basic の Byte())、Binary | バイト配列がスカラ型として扱われます。ユーザーは、コンストラクタが呼び出されたときに十分な記憶域を割り当てる必要があります。これは変更不可と見なされ、変更の追跡が行われません。 |
| binary、varbinary | Byte[] (Visual Basic の Byte())、Binary | |
Association 属性
Association 属性は、主キーと外部キーのリレーションシップのような、データベースの関連付けを表すプロパティを指定するのに使用します。
Association 属性
| プロパティ | データ型 | 説明 |
| Name | String | 関連付けの名前です。多くの場合、データベースの外部キー制約の名前と同じです。これは、関連する制約を生成するため、データベースのインスタンスの作成に CreateDatabase() が使用されるときに使用します。また、同じターゲットのエンティティ クラスを参照する、単一のエンティティ クラスの複数のリレーションシップを区別する際にも使用します。この場合、リレーションシップを構成する双方のリレーションシップ プロパティ (共に定義されている場合) は、同じ名前である必要があります。 |
| Storage | String | 基になるストレージのメンバの名前です。このプロパティでは、LINQ to SQL に対して、データ メンバのパブリックなプロパティ アクセサを使用せずに、未処理の値自体を操作する方法を指定します。指定されない場合、LINQ to SQL は、パブリック アクセサを使用して値を取得および設定します。すべての関連付けのメンバは、識別された個別のストレージのメンバのプロパティにすることをお勧めします。 |
| ThisKey | String | 関連付けのこちら側のキー値を表す、このエンティティ クラスの 1 つ以上のメンバの名前が示される、コンマで区切られたリストです。指定されない場合、そのメンバは、主キーを構成するメンバであると見なされます。 |
| OtherKey | String | 関連付けのもう一方の側のキー値を表す、ターゲットのエンティティ クラスの 1 つ以上のメンバの名前が示される、コンマで区切られたリストです。指定されない場合、そのメンバは、もう一方のエンティティ クラスの主キーを構成するメンバであると見なされます。 |
| IsUnique | Boolean | True が設定された場合、外部キーに一意性制約があり、一対一のリレーションシップであることを示します。一対一リレーションシップはデータベース内で管理することがほぼ不可能なため、このプロパティはほとんど使用されません。通常、エンティティ モデルは、アプリケーション開発者によって一対一として扱われる場合でも、一対多リレーションシップを使用して定義されます。 |
| IsForeignKey | Boolean | True が設定された場合、ターゲットの関連付けのもう一方の型がソース型の親であることを示します。主キーと外部キーのリレーションシップでは、外部キーを保持する側が子で、主キーを保持する側が親です。 |
| DeleteRule | String | この関連付けに削除の動作を追加するのに使用します。たとえば、CASCADE は外部キー (FK) リレーションシップに ON DELETE CASCADE を追加します。NULL が設定されると、削除の動作は追加されません。 |
関連付けのプロパティは、別のエンティティ クラスのインスタンスへの単一の参照、または参照のコレクションのいずれかを表します。単一の参照は、実際の参照を保存するため、EntityRef<T> (Visual Basic の EntityRef (OfT)) 値型を使用してエンティティ クラスにエンコードする必要があります。EntityRef 型は、LINQ to SQL で参照の "遅延読み込み" を実行できるようにする方法です。
C#
class Order
{
...
private EntityRef<Customer> _Customer;
[Association(Name="FK_Orders_Customers", Storage="_Customer",
ThisKey="CustomerID")]
public Customer Customer {
get { return this._Customer.Entity; }
set { this._Customer.Entity = value;
// 変更を管理するための追加のコードを記述します。
}
}
Visual Basic
Class Order
...
Private _customer As EntityRef(Of Customer)
<Association(Name:="FK_Orders_Customers", _
Storage:="_Customer", ThisKey:="CustomerID")> _
public Property Customer() As Customer
Get
Return _customer.Entity
End Get
Set (value As Customer)
_customer.Entity = value
‘ 変更を管理するための追加のコードを記述します。
End Set
End Class
パブリック プロパティは、EntityRef<Customer> ではなく、Customer として型指定されます。クエリでの EntityRef 型への参照は SQL に変換されないため、この型をパブリック API の一部として公開しないことが重要です。
同様に、コレクションを表す関連付けのプロパティでは、リレーションシップを保存するため、EntitySet<T> (Visual Basic の EntitySet(OfT)) コレクション型を使用する必要があります。
C#
class Customer
{
...
private EntitySet<Order> _Orders;
[Association(Name="FK_Orders_Customers", Storage="_Orders",
OtherKey="CustomerID")]
public EntitySet<Order> Orders {
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
}
Visual Basic
Class Customer
...
Private _Orders As EntitySet(Of Order)
<Association(Name:="FK_Orders_Customers", _
Storage:="_Orders", OtherKey:="CustomerID")> _
public Property Orders() As EntitySet(Of Order)
Get
Return _Orders
End Get
Set (value As EntitySet(Of Order))
_Orders.Assign(value)
End Property
End Class
ただし、EntitySet<T> (Visual Basic の EntitySet(OfT)) はコレクションであるため、EntitySet を戻り値の型として使用することができます。また、代わりに ICollection<T> (Visual Basic の ICollection(OfT)) インターフェイスを使用して、コレクションの実際の型を示さないようにすることもできます。
C#
class Customer
{
...
private EntitySet<Order> _Orders;
[Association(Name="FK_Orders_Customers", Storage="_Orders",
OtherKey="CustomerID")]
public ICollection<Order> Orders {
get { return this._Orders; }
set { this._Orders.Assign(value); }
}
}
Visual Basic
Class Customer
...
Private _orders As EntitySet(Of Order)
<Association(Name:="FK_Orders_Customers", _
Storage:="_Orders", OtherKey:="CustomerID")> _
public Property Orders() As ICollection (Of Order)
Get
Return _orders
End Get
Set (value As ICollection (Of Order))
_orders.Assign(value)
End Property
End Class
プロパティのパブリックな Set アクセス操作子を公開する場合、EntitySet で Assign() メソッドを使用するようにします。このメソッドを使用すると、エンティティ クラスは、既に "変更追跡サービス" に関連付けられている可能性があるため、同じコレクション インスタンスを継続して使用することができます。
ResultType 属性
この属性は、IMultipleResults インターフェイスを返すように宣言されている関数から返される、列挙可能なシーケンスの要素型を指定するのに使用します。この属性は、何度も指定できます。
ResultType 属性
| プロパティ | データ型 | 説明 |
| Type | Type | 返される結果の型です。 |
StoredProcedure 属性
StoredProcedure 属性は、DataContext または Schema 型で定義されたメソッドへの呼び出しが、データベース ストアド プロシージャへの呼び出しとして変換されることを宣言するのに使用します。
StoredProcedure 属性
| プロパティ | データ型 | 説明 |
| Name | String | データベース内のストアド プロシージャの名前です。指定されない場合、ストアド プロシージャの名前はメソッドと同じ名前であると見なされます。 |
Function 属性
Function 属性は、DataContext または Schema で定義されたメソッドへの呼び出しが、データベースのユーザー定義のスカラ値関数またはテーブル値関数として変換されることを宣言するのに使用します。
Function 属性
| プロパティ | データ型 | 説明 |
| Name | String | データベースの関数の名前です。指定されない場合、関数の名前はメソッドと同じ名前であると見なされます。 |
Parameter 属性
Parameter 属性は、メソッドと、データベース ストアド プロシージャまたはユーザー定義関数のパラメータ間のマッピングを宣言するのに使用します。
Parameter 属性
| プロパティ | データ型 | 説明 |
| Name | String | データベースのパラメータの名前です。指定されない場合、パラメータはメソッド パラメータ名から推論されます。 |
| DBType | String | データベースの種類と修飾子を使用して指定されたパラメータの種類です。 |
InheritanceMapping 属性
InheritanceMapping 属性は、特定の識別子のコードと継承のサブタイプ間の対応関係を説明するのに使用します。継承階層に使用されるすべての InheritanceMapping 属性は、階層のルート型で宣言される必要があります。
InheritanceMapping 属性
| プロパティ | データ型 | 説明 |
| Code | Object | 識別子のコードの値です。 |
| Type | Type | 継承のサブタイプです。ルート型を含む、任意の継承階層の非抽象型を指定できます。 |
| IsDefault | Boolean | 指定された継承のサブタイプが、LINQ to SQL によって InheritanceMapping 属性で定義されていない識別子のコードが特定されたときに構築された、既定の型であるかどうかを示します。InheritanceMapping 属性のうち 1 つだけを、IsDefault に true を設定して宣言する必要があります。 |
グラフの一貫性
グラフとは、参照で相互参照しているすべてのオブジェクトのデータ構造を表す一般的な用語です。階層 (またはツリー) は、グラフの変型です。ドメイン固有のオフジェクト モデルは、多くの場合、オブジェクトのグラフとして示すのが最も適している参照のネットワークを表します。オブジェクト グラフの正常性は、アプリケーションを安定させるうえで非常に重要です。そのため、グラフ内の参照と、ビジネス ルールまたは データベース、あるいはその両方で定義された制約との一貫性が維持することが重要になります。
LINQ to SQL では、リレーションシップの参照の一貫性が自動的に管理されません。リレーションシップが双方向の場合、リレーションシップの一方が変更されたときに、もう一方が自動的に更新されます。通常のオブジェクトがこのように動作することは珍しいため、オブジェクトをこのようにデザインすることはほとんどないことに注意してください。
LINQ to SQL では、この操作を簡単にするためのいくつかのメカニズムと、参照を適切に管理できるようにするパターンが提供されます。コード生成ツールによって生成されたエンティティ クラスでは、適切なパターンが自動的に実装されます。
C#
public class Customer() {
this._Orders =
new EntitySet<Order>(
new Action<Order>(this.attach_Orders),
new Action<Order>(this.detach_Orders));
);}
Visual Basic
Public Class Customer()
_Orders = New EntitySet(Of Order)( _
New Action(Of Order)(attach_Orders), _
New Action(Of Order)(detach_Orders))
End Class
);}
EntitySet<T> (Visual Basic の EntitySet(OfT)) 型には、コールバックとして使用する 2 つのデリゲートを指定できるコンストラクタがあります。1 つ目は項目がコレクションに追加されるとき、2 つ目は項目が削除されるときに使用します。上記の例からおわかりのように、これらのデリゲートに指定するコードは、逆のリレーションシップ プロパティを更新するように記述する必要があります。この方法により、注文が顧客の Orders コレクションに追加されるときに、Order インスタンスの Customer プロパティが自動的に変更されます。
もう一方でのリレーションシップの実装は、これほど容易ではありません。EntityRef<T> (Visual Basic の EntityRef(OfT)) は、できるだけ実際のオブジェクト参照によるオーバーヘッドを軽減するように定義された値型です。2 つのデリゲートを指定することはできませんが、単一の参照のグラフの一貫性を管理するコードが、プロパティ アクセサ自体に組み込まれています。
C#
[Association(Name="FK_Orders_Customers", Storage="_Customer",
ThisKey="CustomerID")]
public Customer Customer {
get {
return this._Customer.Entity;
}
set {
Customer v = this._Customer.Entity;
if (v != value) {
if (v != null) {
this._Customer.Entity = null;
v.Orders.Remove(this);
}
this._Customer.Entity = value;
if (value != null) {
value.Orders.Add(this);
}
}
}
}
Visual Basic
<Association(Name:="FK_Orders_Customers", _
Storage:="_Customer", ThisKey:="CustomerID")> _
Public Property Customer As Customer
Get
Return _Customer.Entity
End Get
Set (value As Customer)
Dim cust As Customer v = _customer.Entity
if cust IsNot value Then
If cust IsNot Nothing Then
_Customer.Entity = Nothing
cust.Orders.Remove(Me)
End If
_customer.Entity = value
if value IsNot Nothing Then
value.Orders.Add(Me)
End If
End If
End Set
End Property
Set アクセス操作子に注目してください。Customer プロパティが変更されるとき、注文のインスタンスが最初に現在の顧客の Orders コレクションから削除され、その後、新しい顧客のコレクションに追加されます。Remove() への呼び出しが行われる前に、実際のエンティティ参照を NULL に設定しています。これは、Remove() メソッドが呼び出されるときの再帰を防ぐために行われます。既に説明しましたが、EntitySet では、コールバック デリゲートを使用して、このオブジェクトの Customer プロパティを NULL に設定します。同じことが、Add() への呼び出しの直前にも行われます。実際のエンティティ参照は新しい値に更新されます。これにより再び再帰が発生する可能性が削減され、そもそもの Set アクセス操作子のタスクが達成されます。
単一の参照に関しては、一対一のリレーションシップの定義は、一対多のリレーションシップに非常に似ています。Add() や Remove() が呼び出される代わりに、新しいオブジェクトが割り当てられるか、リレーションシップを削除する場合は NULL が割り当てられます。
繰り返しになりますが、リレーションシップ プロパティでオブジェクト グラフの一貫性が維持されることが不可欠です。メモリ内のオブジェクト グラフとデータベース データに一貫性がない場合、SubmitChanges() メソッドの呼び出し時に実行時例外が生成されます。一貫性を維持するため、コード生成ツールの使用を検討してください。
変更通知
オブジェクトは、変更の追跡プロセスに参加することができます。参加する必要はありませんが、参加すると潜在的なオブジェクトの変更を追跡するためのオーバーヘッドを大幅に削減できます。アプリケーションは、クエリで、最終的に変更されるオブジェクトよりも多くのオブジェクトを取得する可能性があります。オブジェクトからのプロアクティブ サポートがない場合、変更追跡サービスでは、変更を追跡する方法が制限されます。
ランタイムには本格的なインターセプト サービスがないため、正式な追跡は実際には発生しません。代わりに、オブジェクトを最初に取得したときに、そのオブジェクトの重複するコピーが保存されます。その後、SubmitChanges() を呼び出したときに、取得したコピーとの比較にこのコピーが使用されます。これらの値が異なる場合、オブジェクトは変更されています。つまり、オブジェクトを変更しない場合でも、すべてのオブジェクトについてメモリ内に 2 つのコピーが必要です。
さらに優れたソリューションは、オブジェクトが実際に変更されたときに、変更追跡サービスに、通知するようにすることです。これは、コールバック イベントを公開するインターフェイスを、オブジェクトに実装することで実現できます。変更追跡サービスでは、その後、各オブジェクトを接続し、変更されたときに通知を受けることができます。
C#
[Table(Name="Customers")]
public partial class Customer: INotifyPropertyChanging {
public event PropertyChangingEventHandler PropertyChanging;
private void OnPropertyChanging() {
if (this.PropertyChanging != null) {
this.PropertyChanging(this, emptyEventArgs);
}
}
private string _CustomerID;
[Column(Storage="_CustomerID", IsPrimaryKey=true)]
public string CustomerID {
get {
return this._CustomerID;
}
set {
if ((this._CustomerID != value)) {
this.OnPropertyChanging("CustomerID");
this._CustomerID = value;
}
}
}
}
Visual Basic
<Table(Name:="Customers")> _
Partial Public Class Customer
Inherits INotifyPropertyChanging
Public Event PropertyChanging As PropertyChangingEventHandler _
Implements INotifyPropertyChanging.PropertyChanging
Private Sub OnPropertyChanging()
RaiseEvent PropertyChanging(Me, emptyEventArgs)
End Sub
private _customerID As String
<Column(Storage:="_CustomerID", IsPrimaryKey:=True)>
public Property CustomerID() As String
Get
Return_customerID
End Get
Set (value As Customer)
If _customerID IsNot value Then
OnPropertyChanging(“CustomerID”)
_CustomerID = value
End IF
End Set
End Function
End Class
強化された変更の追跡をサポートするには、エンティティ クラスで INotifyPropertyChanging インターフェイスを実装する必要があります。必要な操作は、PropertyChanging というイベントを定義するだけです。変更追跡サービスでは、その後、オブジェクトが取得されたときにイベントを登録します。必要な操作は、プロパティの値を変更する直前に、このイベントを発生させることだけです。
また、リレーションシップ プロパティの Set アクセス操作子にも、イベントを発生させる同一のロジックを組み込む必要があります。EntitySets では、指定したデリゲートでイベントを発生させます。
C#
public Customer() {
this._Orders =
new EntitySet<Order>(
delegate(Order entity) {
this.OnPropertyChanging("Orders");
entity.Customer = this;
},
delegate(Order entity) {
this.onPropertyChanging("Orders");
entity.Customer = null;
}
);
}
Visual Basic
Dim _orders As EntitySet(Of Order)
Public Sub New()
_orders = New EntitySet(Of Order)( _
AddressOf OrderAdding, AddressOf OrderRemoving)
End Sub
Sub OrderAdding(ByVal o As Order)
OnPropertyChanging()
o.Customer = Me
End Sub
Sub OrderRemoving(ByVal o As Order)
OnPropertyChanging()
o.Customer = Nothing
End Sub
継承
LINQ to SQL では、単一テーブルのマッピングがサポートされます。このマッピングにより、継承階層全体が単一のデータベース テーブル内に保存されます。このテーブルには、階層全体の全データ列のフラットな共用体が含まれ、行によって示されるインスタンスの型に適用できない各行の列には NULL が含まれます。単一テーブルのマッピングは、最も簡単な継承の表現で、多数のさまざまな分類のクエリに対して優れたパフォーマンス特性を提供します。
マッピング
LINQ to SQL を使用してこのマッピングを実装するには、継承階層のルート クラスで次の属性と属性プロパティを指定する必要があります。
- [Table] (Visual Basic の <Table>) 属性。
- 階層構造の各クラスの [InheritanceMapping] (Visual Basic の <InheritanceMapping>) 属性。非抽象クラスでは、この属性は、Code プロパティ (このデータの行が属するクラスまたはサブクラスを示す、Inheritance Discriminator 列のデータベース テーブルに表示される値) および Type プロパティ (キー値が示すクラスまたはサブクラスを指定する) を定義する必要があります。
- 単一の [InheritanceMapping] (Visual Basic の <InheritanceMapping>) 属性の IsDefault プロパティ。このプロパティでは、データベース テーブルの識別子の値が継承のマッピングの Code 値のいずれとも一致しない場合に、"フォールバック" マッピングを指定します。
- [Column] (Visual Basic の <Column>) 属性の IsDiscriminator プロパティ。このプロパティは、継承のマッピングの Code 値を保持する列であることを指定します。
サブクラスでは、特殊な属性やプロパティは必要ありません。サブクラスには [Table] (Visual Basic の <Table>) 属性がないことに、特に注意してください。
次の例では、Car サブクラスと Truck サブクラスに含まれるデータを、単一のデータベース テーブル Vehicle にマップします (例を簡略化するため、サンプル コードでは列のマッピングにプロパティではなくフィールドを使用します)。
C#
[Table]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle),
IsDefault = true)]
public class Vehicle
{
[Column(IsDiscriminator = true)]
public string Key;
[Column(IsPrimaryKey = true)]
public string VIN;
[Column]
public string MfgPlant;
}
public class Car : Vehicle
{
[Column]
public int TrimCode;
[Column]
public string ModelName;
}
public class Truck : Vehicle
{
[Column]
public int Tonnage;
[Column]
public int Axles;
}
Visual Basic
<Table> _
<InheritanceMapping(Code:="C", Type:=Typeof(Car))> _
<InheritanceMapping(Code:="T", Type:=Typeof(Truck))> _
<InheritanceMapping(Code:="V", Type:=Typeof(Vehicle), _
IsDefault:=true)> _
Public Class Vehicle
<Column(IsDiscriminator:=True)> _
Public Key As String
<Column(IsPrimaryKey:=True)> _
Public VIN As String
<Column> _
Public MfgPlant As String
End Class
Public Class Car
Inherits Vehicle
<Column> _
Public TrimCode As Integer
<Column> _
Public ModelName As String
End Class
Public class Truck
Inherits Vehicle
<Column> _
public Tonnage As Integer
<Column> _
public Axles As Integer
End Class
クラス ダイアグラムは次のようになります。
図 1. Vehicle クラス ダイアグラム
結果のデータベース ダイアグラムをサーバー エクスプローラで表示すると、次のように、すべての列が単一のテーブルにマップされていることを確認できます。
図 2. 単一のテーブルにマップされた列
サブタイプのフィールドを示す列の型は、NULL 許容であるか、既定値が指定されている必要があります。これは、挿入コマンドが正常に実行されるために必要です。
クエリ
次のコードでは、クエリで派生型を使用する方法を示します。
C#
var q = db.Vehicle.Where(p => p is Truck);
// または
var q = db.Vehicle.OfType<Truck>();
// または
var q = db.Vehicle.Select(p => p as Truck).Where(p => p != null);
foreach (Truck p in q)
Console.WriteLine(p.Axles);
Visual Basic
Dim trucks = From veh In db.Vehicle _
Where TypeOf(veh) Is Truck
For Each truck In trucks
Console.WriteLine(p.Axles)
Next
詳細
階層は、上記の簡単なサンプルをさらに展開できます。
例 1
より深い階層および複雑なクエリを次に示します。
C#
[Table]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle), IsDefault = true)]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "S", Type = typeof(Semi))]
[InheritanceMapping(Code = "D", Type = typeof(DumpTruck))]
public class Truck: Vehicle { ... }
public class Semi: Truck { ... }
public class DumpTruck: Truck { ... }
...
// 産業用途であることを示すフラグが設定されたすべての Truck を取得します。
db.Vehicles.OfType<Truck>.Select(t =>
new {Truck=t, IsIndustrial=t is Semi || t is DumpTruck }
);
Visual Basic
<Table> _
<InheritanceMapping(Code:="V", Type:=Typeof(Vehicle), IsDefault:=True)> _
<InheritanceMapping(Code:="C", Type:=Typeof(Car))> _
<InheritanceMapping(Code:="T", Type:=Typeof(Truck))> _
<InheritanceMapping(Code:="S", Type:=Typeof(Semi))> _
<InheritanceMapping(Code:="D", Type:=Typeof(DumpTruck))> _
Public Class Truck
InheritsVehicle
Public Class Semi
Inherits Truck
Public Class DumpTruck
InheritsTruck
...
' 産業用途であることを示すフラグが設定されたすべての Truck を取得します。
Dim trucks = From veh In db.Vehicle _
Where Typeof(veh) Is Truck And _
IsIndustrial = (Typeof(veh) Is Semi _
Or Typeof(veh) Is DumpTruck)
例 2
次の階層にはインターフェイスが含まれます。
C#
[Table]
[InheritanceMapping(Code = "V", Type = typeof(Vehicle),
IsDefault = true)]
[InheritanceMapping(Code = "C", Type = typeof(Car))]
[InheritanceMapping(Code = "T", Type = typeof(Truck))]
[InheritanceMapping(Code = "S", Type = typeof(Semi))]
[InheritanceMapping(Code = "H", Type = typeof(Helicopter))]
public class Truck: Vehicle
public class Semi: Truck, IRentableVehicle
public class Helicopter: Vehicle, IRentableVehicle
Visual Basic
<Table> _
<InheritanceMapping(Code:="V", Type:=TypeOf(Vehicle),
IsDefault:=True) > _
<InheritanceMapping(Code:="C", Type:=TypeOf(Car)) > _
<InheritanceMapping(Code:="T", Type:=TypeOf(Truck)) > _
<InheritanceMapping(Code:="S", Type:=TypeOf(Semi)) > _
<InheritanceMapping(Code:="H", Type:=TypeOf(Helicopter)) > _
Public Class Truck
Inherits Vehicle
Public Class Semi
InheritsTruck, IRentableVehicle
Public Class Helicopter
InheritsVehicle, IRentableVehicle
次のようなクエリが含まれる可能性があります。
C#
// 商用車を取得し、レンタル料金順に並べ替えます。
db.Vehicles.OfType<IRentableVehicle>.OrderBy(cv => cv.RentalRate);
// レンタルできない車をすべて取得します。
db.Vehicles.Where(v => !(v is IRentableVehicle));
Visual Basic
' 商用車を取得し、レンタル料金順に並べ替えます。
Dim rentableVehicles = From veh In _
db.Vehicles.OfType(Of IRentableVehicle).OrderBy( _
Function(cv) cv.RentalRate)
' レンタルできない車をすべて取得します。
Dim unrentableVehicles = From veh In _
db.Vehicles.OfType(Of Vehicle).Where( _
Function(uv) Not (TypeOf(uv) Is IRentableVehicle))
高度なトピック
データベースの作成
エンティティ クラスには、リレーショナル データベースのテーブルと列の構造を記述する属性があるので、この情報を使用してデータベースの新しいインスタンスを作成することができます。DataContext で CreateDatabase() メソッドを呼び出して、オブジェクトで定義された構造を使用して新しいデータベースのインスタンスを LINQ to SQL で作成することができます。この処理は、さまざまな状況で必要になります。ユーザーのシステムに自動的に自己インストールするアプリケーションを作成したり、オフラインの状態を保存するローカル データベースが必要なクライアント アプリケーションを作成したりする場合があります。このようなシナリオ、特に SQL Server Express 2005 などの既知のデータ プロバイダが利用できる場合には、CreateDatabase() が最適です。
ただし、データ属性は、既存のデータベース構造に関するすべての情報をエンコードするとは限りません。ユーザー定義関数、ストアド プロシージャ、トリガ、および CHECK 制約の内容は、属性では表されません。CreateDatabase() 関数で行う処理は、データベースの構造や各テーブルの列の型など、既知の情報を使用してデータベースのレプリカを作成するだけです。しかし、多くのデータベースではこの関数で十分です。
次に、MyDVDs.mdf というデータベースを作成する方法の例を示します。
C#
[Table(Name="DVDTable")]
public class DVD
{
[Column(Id = true)]
public string Title;
[Column]
public string Rating;
}
public class MyDVDs : DataContext
{
public Table<DVD> DVDs;
public MyDVDs(string connection) : base(connection) {}
}
Visual Basic
<Table(Name:="DVDTable")> _
Public Class DVD
<Column(Id:=True)> _
public Title As String
<Column> _
Public Rating As String
End Class
Public Class MyDVDs
Inherits DataContext
Public DVDs As Table(Of DVD)
Public Sub New(connection As String)
End Class
オブジェクト モデルは、次のように SQL Server Express 2005 を使用したデータベースの作成に使用できます。
C#
MyDVDs db = new MyDVDs("c:\\mydvds.mdf");
db.CreateDatabase();
Visual Basic
Dim db As MyDVDs = new MyDVDs("c:\mydvds.mdf")
db.CreateDatabase()
LINQ to SQL では、既存のデータベースを削除してから新しくデータベースを作成する API も提供します。上記のデータベース作成コードは、まず、DatabaseExists() で既存のバージョンのデータベースがあるかどうかを確認してから、DeleteDatabase() で確認したデータベースを削除するように変更できます。
C#
MyDVDs db = new MyDVDs("c:\\mydvds.mdf");
if (db.DatabaseExists()) {
Console.WriteLine("Deleting old database...");
db.DeleteDatabase();
}
db.CreateDatabase();
Visual Basic
Dim db As MyDVDs = New MyDVDs("c:\mydvds.mdf")
If (db.DatabaseExists()) Then
Console.WriteLine("Deleting old database...")
db.DeleteDatabase()
End If
db.CreateDatabase()
CreateDatabase() を呼び出したら、新しいデータベースでは、SubmitChanges() などのクエリやコマンドを受け入れて、MDF ファイルにオブジェクトを追加することができます。
SQL Server Express 以外のエディションで、MDF ファイルまたはカタログ名のいずれかを使用して、CreateDatabase() を使用することもできます。どちらを使用するかは、接続文字列に使用する情報によって異なります。接続文字列の情報は、今後、存在する予定のデータベースを定義するのにも使用されます。必ずしも既存のデータベースを定義するために使用されるとは限りません。LINQ to SQL では関連する情報を検出し、その情報を利用して作成するデータベース、およびデータベースの作成先となるサーバーを決定します。もちろん、この処理には、サーバーに対するデータベースの管理者権限、またはそれと同等の権限が必要です。
ADO.NET との相互運用
LINQ to SQL は ADO.NET ファミリ テクノロジの一部です。LINQ to SQL は ADO.NET プロバイダ モデルで提供されるサービスに基づいているので、LINQ to SQL コードを既存の ADO.NET アプリケーションと混在させることができます。
LINQ to SQL の DataContext を作成するときには、既存の ADO.NET 接続を指定できます。クエリを含め、DataContext に対するすべての操作では、指定した接続が使用されます。既に接続が開かれている場合、LINQ to SQL ではユーザーの接続に関する権限を受け入れ、接続を使用し終えたときも、接続はそのままにしておきます。通常、トランザクションがスコープ内にない場合は、操作が終了し次第 LINQ to SQL は接続を閉じます。
C#
SqlConnection con = new SqlConnection( ... );
con.Open();
...
// DataContext で接続を取得します。
Northwind db = new Northwind(con);
...
var q =
from c in db.Customers
where c.City == "London"
select c;
Visual Basic
Dim con As SqlConnection = New SqlConnection( ... )
con.Open()
...
' DataContext で接続を取得します。
Dim db As Northwind = new Northwind(con)
...
Dim q = From c In db.Customers _
Where c.City = "London" _
Select c
ユーザーは、いつでも Connection プロパティを使用して DataContext で使用している接続にアクセスし、自分で接続を閉じることができます。
C#
Visual Basic
また、アプリケーションで既に開始されたデータベース トランザクションがあり、DataContext で操作する場合は、DataContext に独自のデータベース トランザクションを指定することもできます。
C#
IDbTransaction = con.BeginTransaction();
...
db.Transaction = myTransaction;
db.SubmitChanges();
db.Transaction = null;
Visual Basic
Dim db As IDbTransaction = con.BeginTransaction()
...
db.Transaction = myTransaction
db.SubmitChanges()
db.Transaction = Nothing
Transaction が設定されているときは必ず、DataContext ではクエリを発行したり、コマンドを実行したりするたびに、Transaction を使用します。終了時には必ず、プロパティに NULL を設定する必要があります。
ただし、.NET Framework でトランザクションを行う場合は、TransactionScope オブジェクトを使用する方法が適しています。この方法を使用すると、データベースやメモリ常駐型のリソース マネージャで機能する分散トランザクションを作成できます。これは、トランザクションのスコープは軽量な状態で開始し、実際にトランザクションのスコープ内の複数のデータベースや複数の接続を参照するときにのみ、完全な分散トランザクションに昇格するという考え方に基づいています。
C#
using(TransactionScope ts = new TransactionScope()) {
db.SubmitChanges();
ts.Complete();
}
Visual Basic
Using ts As TransactionScope= New TransactionScope()
db.SubmitChanges()
ts.Complete()
End Using
SQL ステートメントの直接実行
ADO.NET と相互運用する方法は、接続とトランザクションだけではありません。DataContext のクエリや変更の送信機能は、実行する特殊なタスクには不十分なことがあります。このような状況では、DataContext を使用して、データベースに直接、未処理の SQL コマンドを発行できます。
ExecuteQuery() メソッドを使用すると、未処理の SQL クエリを実行し、実行したクエリの結果を直接オブジェクトに変換できます。たとえば、Customer クラスのデータが customer1 と customer2 という 2 つのテーブルにまたがって存在していると仮定します。次のクエリを実行すると、一連の Customer オブジェクトを返します。
C#
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
@"select c1.custid as CustomerID, c2.custName as ContactName
from customer1 as c1, customer2 as c2
where c1.custid = c2.custid"
);
Visual Basic
Dim results As IEnumerable(Of Customer) = _
db.ExecuteQuery(Of Customer)( _
"select c1.custid as CustomerID, " & _
"c2.custName as ContactName " & _
"from customer1 as c1, customer2 as c2 "& _
"where c1.custid = c2.custid" )
表形式の結果の列名が、エンティティ クラスの列のプロパティと一致する限り、LINQ to SQL では SQL クエリからオブジェクトを具体化します。
ExecuteQuery() メソッドではパラメータを使用することもできます。次のコードでは、パラメータ化したクエリが実行されます。
C#
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
"select contactname from customers where city = {0}",
"London"
);
Visual Basic
Dim results As IEnumerable(Of Customer) = _
db.ExecuteQuery(Of Customer)( _
"select contactname from customers where city = {0}", _
"London" )
クエリ テキストでは、パラメータは Console.WriteLine() や String.Format() で使用されているものと同じ中かっこで表現されます。実際、String.Format() では指定したクエリ文字列に対して呼び出され、中かっこで囲まれたパラメータを @p0、@p1...、@p(n) などの生成されたパラメータ名に置き換えます。
変更の競合の解決
説明
変更の競合は、クライアントがあるオブジェクトの変更を送信したときに、更新チェックで使用する 1 つ以上の値が、クライアントがその値を最後に読み取ったときから、データベースで更新されている場合に発生します。
注 UpdateCheck.Always または UpdateCheck.WhenChanged としてマップされているメンバのみがオプティミスティック同時実行制御のチェックの対象になります。UpdateCheck.Never としてマークされているメンバに対してチェックは行われません。
変更の競合を解決するには、オブジェクトのどのメンバで競合が発生しているのかを検出し、それについてどうするかを決める必要があります。状況によっては、オプティミスティック同時実行制御が最適な解決策ではないことがあります。最後に行われた更新を優先させることが適切な場合もあります。
LINQ to SQL における競合の検出、報告、および解決
競合の解決とは、データベースを再クエリし、相違点を調整することで、競合が発生している項目を更新する処理です。オブジェクトが更新されると、変更追跡サービスにより、古い元の値と新しいデータベースの値が保持されます。このデータを基に、LINQ to SQL では、そのオブジェクトで競合が発生しているかどうかを判断します。競合が発生している場合、LINQ to SQL では、関連のあるメンバを特定します。あるメンバの新しいデータベースの値が、エラーになった更新チェックで使用した元の値と異なる場合、そのメンバで競合が発生していることになります。メンバの競合は、競合一覧に追加されます。
たとえば、次のシナリオでは、User1 が、データベースのある行をクエリして、更新処理の準備を進めているとします。User1 で変更を送信する準備が整う前に、User2 がデータベースを変更したとします。User1 が変更を送信すると、Col B と Col C の値が変更されているため更新に失敗します。
データベース更新の競合
| ユーザー | Col A | Col B | Col C |
| 元の状態 | Alfreds | Maria | Sales |
| User 1 | Alfred | | Marketing |
| User 2 | | Mary | Service |
オプティミスティック同時実行制御の競合が原因で更新に失敗した LINQ to SQL オブジェクトでは、例外 (ChangeConflictException) がスローされます。例外を最初の失敗のときにのみスローするのか、失敗したすべての更新についての情報を収集して例外で報告するのかを指定することができます。
// [C#]
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
db.SubmitChanges(ConflictMode.ContinueOnConflict);
' [Visual Basic]
db.SubmitChanges(ConflictMode.FailOnFirstConflict)
db.SubmitChanges(ConflictMode.ContinueOnConflict)
例外がスローされると、ObjectChangeConflict コレクションへのアクセスが提供されます。競合ごとに、MemberConflict 一覧へのアクセスを含む詳細情報が提供されます (この詳細情報は、失敗した更新の試行ごとにマップされます)。各メンバの競合は、オプティミスティック同時実行制御のチェックに失敗した更新に含まれる該当メンバにマップされます。
競合の処理
上記のシナリオでは、User1 は、再送信を行う前に相違点を調整するのに使用する RefreshMode オプションを持っています (このオプションについては、後で説明します)。どのような場合でも、クライアント側のレコードは、まず、データベースから更新されたデータを取得することによって最新の状態に更新されます。この処理により、次回の更新時には、同じオプティミスティック同時実行制御のチェックで失敗しないことが保証されます。
ここでは、User1 は、データベースの値を現在のクライアント側の値とマージして、現在の変更セットでも変更されている値については、データベースの値を上書きされるようにしました (詳細については、このセクションの「例 1」を参照してください)。
上記の例では、競合を解決すると、データベースの状態は次のようになります。
KeepChanges
| | Col A | Col B | Col C |
| KeepChanges | Alfred (User 1) | Mary (User 2) | Marketing (User 1) |
- Col A: User1 の変更 (Alfred) が反映されます。
- Col B: User2 の変更 (Mary) が反映されます。User1 は、この値を変更していないため、User2 の変更がマージされました。
- Col C: User1 の変更 (Marketing) が反映されます。User 1 もこのアイテムを変更したため、User2 の変更 (Service) はマージされませんでした。
今度は、User1 はすべてのデータベースの値を最新の値で上書きするようにしました (詳細については、このセクションの「例 2」を参照してください)。
更新処理を行うと、User1 の変更が送信されます。データベースの状態は、次のようになります。
KeepCurrentValues
| | Col A | Col B | Col C |
| KeepCurrentValues | Alfred (User 1) | Maria (元の値) | Marketing (User 1) |
- Col A: User1 の変更 (Alfred) が反映されます。
- Col B: 元の値 Maria が維持され、User2 の変更は破棄されます。
- Col C: User1 の変更 (Marketing) が反映されます。User2 の変更 (Service) は破棄されます。
次のシナリオでは、User1 はデータベースの値でクライアント側の最新の値を上書きするようにしました (詳細については、このセクションの「例 3」を参照してください)。
上記の例では、競合を解決すると、データベースの状態は次のようになります。
OverwriteCurrentValues
| | Col A | Col B | Col C |
| OverwriteCurrentValues | Alfreds (元の値) | Mary (User 2) | Service (User 2) |
- Col A: 元の値 (Alfreds) が維持されされ、User1 の値 (Alfred) は破棄されます。
- Col B: User2 の変更 (Mary) が反映されます。
- Col C: User2 の変更 (Service) が反映されます。User1 の変更 (Marketing) は破棄されます。
競合を解決したら、変更を再送できます。この 2 回目の更新も失敗する可能性があるので、更新の試行にはループ処理を使用することを検討してください。
例
以下のコードの抜粋では、メンバの競合を検出して解決するのに自由に使用できるメンバや技法を紹介しています。
例 1
この例では、競合は自動的に解決されます。つまり、クライアント側でも値 (KeepChanges) を変更していない限り、データベースの値は、クライアントが保持している最新の値とマージされます。個別メンバの競合に関する検査やカスタム処理は行われません。
C#
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
// クライアント側で変更されていないメンバについては、データベースの値を
// 最新の値と自動的にマージします。
context.ChangeConflicts.Resolve(RefreshMode.KeepChanges);
}
// 2 回目の試行で変更の送信が成功します。
context.SubmitChanges(ConflictMode.FailOnFirstConflict);
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
' クライアント側で変更されていないメンバについては、データベースの値を
' 最新の値と自動的にマージします。
context.ChangeConflicts.Resolve(RefreshMode.KeepChanges)
End Try
' 2 回目の試行で変更の送信が成功します。
context.SubmitChanges(ConflictMode.FailOnFirstConflict)
例 2
この例では、カスタム処理を使用せずに競合を解決します。ただし、この例では、データベースの値は、クライアント側で保持している最新の値とマージされません。
C#
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
foreach (ObjectChangeConflict cc in context.ChangeConflicts) {
// データベースの値は最新の値とマージされません。
cc.Resolve(RefreshMode.KeepCurrentValues);
}
}
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
For Each cc As ObjectChangeConflict In context.ChangeConflicts
' データベースの値は最新の値とマージされません。
cc.Resolve(RefreshMode.KeepCurrentValues)
Next
End Try
例 3
この例でも、カスタム処理は使用しません。ただし、この例では、クライアント側の値は、すべて最新のデータベースの値で更新されます。
C#
try {
context.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
foreach (ObjectChangeConflict cc in context.ChangeConflicts) {
// データベースの値は最新の値とマージされません。
cc.Resolve(RefreshMode.OverwriteCurrentValues);
}
}
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
For Each cc As ObjectChangeConflict In context.ChangeConflicts
' データベースの値は最新の値とマージされません。
cc.Resolve(RefreshMode. OverwriteCurrentValues)
Next
End Try
例 4
この例では、競合が発生しているエンティティに関する情報にアクセスする方法を紹介します。
C#
try {
user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
Console.WriteLine("Optimistic concurrency error");
Console.ReadLine();
foreach (ObjectChangeConflict cc in user1.ChangeConflicts) {
ITable table = cc.Table;
Customers entityInConflict = (Customers)cc.Object;
Console.WriteLine("Table name: {0}", table.Name);
Console.Write("Customer ID: ");
Console.WriteLine(entityInConflict.CustomerID);
}
}
Visual Basic
Try
context.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
Console.WriteLine("Optimistic concurrency error")
Console.ReadLine()
For Each cc As ObjectChangeConflict In context.ChangeConflicts
Dim table As ITable = cc.Table
Dim entityInConflict As Customers = CType(cc.Object, Customers)
Console.WriteLine("Table name: {0}", table.Name)
Console.Write("Customer ID: ")
Console.WriteLine(entityInConflict.CustomerID)
Next
End Try
例 5
この例では、各メンバを調べるループ処理を追加します。この例では、任意のメンバについてカスタム処理を使用することができます。
Note MemberInfo を提供するには using System.Reflection; を追加します。
C#
try {
user1.SubmitChanges(ConflictMode.ContinueOnConflict);
}
catch (ChangeConflictException e) {
Console.WriteLine("Optimistic concurrency error");
Console.ReadLine();
foreach (ObjectChangeConflict cc in user1.ChangeConflicts) {
ITable table = cc.Table;
Customers entityInConflict = (Customers)cc.Object;
Console.WriteLine("Table name: {0}", table.Name);
Console.Write("Customer ID: ");
Console.WriteLine(entityInConflict.CustomerID);
foreach (MemberChangeConflict mc in cc.MemberConflicts) {
object currVal = mc.CurrentValue;
object origVal = mc.OriginalValue;
object databaseVal = mc.DatabaseValue;
MemberInfo mi = mc. Member;
Console.WriteLine("Member: {0}", mi.Name);
Console.WriteLine("current value: {0}", currVal);
Console.WriteLine("original value: {0}", origVal);
Console.WriteLine("database value: {0}", databaseVal);
Console.ReadLine();
}
}
}
Visual Basic
Try
user1.SubmitChanges(ConflictMode.ContinueOnConflict)
Catch e As ChangeConflictException
Console.WriteLine("Optimistic concurrency error")
Console.ReadLine()
For Each cc As ObjectChangeConflict In context.ChangeConflicts
Dim table As ITable = cc.Table
Dim entityInConflict As Customers = CType(cc.Object, Customers)
Console.WriteLine("Table name: {0}", table.Name)
Console.Write("Customer ID: ")
Console.WriteLine(entityInConflict.CustomerID)
For Each mc As MemberChangeConflict In cc.MemberConflicts
Dim currVal As Object = mc.CurrentValue
Dim origVal As Object = mc.OriginalValue
Dim databaseVal As Object = mc.DatabaseValue
Dim mi As MemberInfo = mc.Member
Console.WriteLine("Member: {0}", mi.Name)
Console.WriteLine("current value: {0}", currVal)
Console.WriteLine("original value: {0}", origVal)
Console.WriteLine("database value: {0}", databaseVal)
Console.ReadLine()
Next
Next
End Try
ストアド プロシージャの呼び出し
LINQ to SQL では、ストアド プロシージャとユーザー定義関数がサポートされます。LINQ to SQL では、このようなデータベースで定義された抽象オブジェクトをコードで生成されたクライアント オブジェクトにマップするので、クライアント コードからは、厳密に型指定された方法でストアド プロシージャやユーザー定義関数にアクセスできます。これらのメソッドは IntelliSense で簡単に見つけることができ、メソッド シグネチャは、データベースで定義されているプロシージャや関数のシグネチャに可能な限り近い状態になっています。マップされたプロシージャの呼び出しで返される結果セットは、厳密に型指定されたコレクションです。LINQ to SQL では、マップされたメソッドを自動的に生成できるだけでなく、コード生成を使用しない場合は、手動によるマッピングもサポートしています。
LINQ to SQL では、属性を利用して、ストアド プロシージャや関数をメソッドにマップします。StoredProcedure、Parameter、および Function 属性のすべての属性では、Name プロパティがサポートされます。Parameter 属性では、DBType プロパティもサポートされます。2 つの例を次に示します。
C#
[StoredProcedure()]
public IEnumerable<CustOrderHistResult> CustOrderHist(
[Parameter(Name="CustomerID", DBType="NChar(5)")] string customerID) {
IExecuteResult result = this.ExecuteMethodCall(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
return ((IEnumerable<CustOrderHistResult>)(result.ReturnValue));
}
[Function(Name="[dbo].[ConvertTemp]")]
public string ConvertTemp(string string) { ...}
Visual Basic
<StoredProcedure()> _
Public Function CustOrderHist( _
<Parameter(Name:="CustomerID", DBType:="NChar(5)")> _
customerID As String) As IEnumerable(Of CustOrderHistResult)
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID)
Return CType(result.ReturnValue, IEnumerable(Of CustOrderHistResult))
End Function
<Function(Name:="[dbo].[ConvertTemp]")> _
Public Function ConvertTemp(str As String) As String
次の例では、さまざまな種類のストアド プロシージャのマッピングを示します。
例 1
次のストアド プロシージャは、入力パラメータを 1 つ受け取り、整数を返します。
CREATE PROCEDURE GetCustomerOrderCount(@CustomerID nchar(5))
AS
Declare @count int
SELECT @count = COUNT(*) FROM ORDERS WHERE CustomerID = @CustomerID
RETURN @count
The mapped method would be as follows:
C#
[StoredProcedure(Name = "GetCustomerOrderCount")]
public int GetCustomerOrderCount(
[Parameter(Name = "CustomerID")] string customerID) {
IExecuteResult result = this.ExecuteMethodCall(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);
return (int) result.ReturnValue;
}
Visual Basic
<StoredProcedure (Name:="GetCustomerOrderCount")> _
public Function GetCustomerOrderCount( _
<Parameter(Name:= "CustomerID")> customerID As String) As Integer
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID)
return CInt(result.ReturnValue)
End Function
例 2
ストアド プロシージャで複数の結果形式を返すことができる場合は、戻り値の型を単一のプロジェクション形式に厳密に型指定することはできません。次の例では、結果形式は入力パラメータによって異なります。
CREATE PROCEDURE VariableResultShapes(@shape int)
AS
if(@shape = 1)
select CustomerID, ContactTitle, CompanyName from customers
else if(@shape = 2)
select OrderID, ShipName from orders
マップされたメソッドは次のとおりです。
C#
[StoredProcedure(Name = "VariableResultShapes")]
[ResultType(typeof(Customer))]
[ResultType(typeof(Order))]
public IMultipleResults VariableResultShapes(System.Nullable<int> shape) {
IExecuteResult result = this.ExecuteMethodCallWithMultipleResults(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())), shape);
return (IMultipleResults) result.ReturnValue;
}
Visual Basic
<StoredProcedure(Name:= "VariableResultShapes")> _
<ResultType(typeof(Customer))> _
<ResultType(typeof(Order))> _
public VariableResultShapes(shape As Integer?) As IMultipleResults
Dim result As IExecuteResult =
ExecuteMethodCallWithMultipleResults(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), shape)
return CType(result.ReturnValue, IMultipleResults)
End Function
このストアド プロシージャは次のように使用できます。
C#
IMultipleResults result = db.VariableResultShapes(1);
foreach (Customer c in result.GetResult<Customer>()) {
Console.WriteLine(c.CompanyName);
}
result = db.VariableResultShapes(2);
foreach (Order o in result.GetResult<Order>()) {
Console.WriteLine(o.OrderID);
}
Visual Basic
Dim result As IMultipleResults = db.VariableResultShapes(1)
For Each c As Customer In result.GetResult(Of Customer)()
Console.WriteLine(c.CompanyName)
Next
result = db.VariableResultShapes(2);
For Each o As Order In result.GetResult(Of Order)()
Console.WriteLine(o.OrderID)
Next
}
ここでは、ストアド プロシージャに関する知識に基づいて GetResult パターンを使用し、正しい型の列挙子を取得する必要があります。LINQ to SQL では、すべての可能なプロジェクション形式を生成できますが、LINQ to SQL にはプロジェクションが返される順番を認識する方法はありません。マップされたメソッドに対応する生成されたプロジェクション形式を特定するには、メソッドに生成されたコードのコメントを使用する必要があります。
例 3
複数の結果形式を "順次" 返すストアド プロシージャの T-SQL を、次に示します。
CREATE PROCEDURE MultipleResultTypesSequentially
AS
select * from products
select * from customers
LINQ to SQL ではこのプロシージャを上記の例 2 と同じようにマップします。ただし、この例では、2 つの結果セットが順次返されます。
C#
[StoredProcedure(Name="MultipleResultTypesSequentially")]
[ResultType(typeof(Product))]
[ResultType(typeof(Customer))]
public IMultipleResults MultipleResultTypesSequentially() {
return ((IMultipleResults)(
this.ExecuteMethodCallWithMultipleResults (this,
((MethodInfo)(MethodInfo.GetCurrentMethod()))).ReturnValue
)
);
}
Visual Basic
<StoredProcedure(Name:="MultipleResultTypesSequentially")> _
<ResultType(typeof(Customer))> _
<ResultType(typeof(Order))> _
public Function MultipleResultTypesSequentially() As IMultipleResults
Return CType( ExecuteMethodCallWithMultipleResults (Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo)), _
IMultipleResults).ReturnValue
End Function
このストアド プロシージャは次のように使用できます。
C#
IMultipleResults sprocResults = db.MultipleResultTypesSequentially();
// まず、製品データを読み取ります。
foreach (Product p in sprocResults.GetResult<Product>()) {
Console.WriteLine(p.ProductID);
}
// 次に、顧客データを読み取ります。
foreach (Customer c in sprocResults.GetResult<Customer>()){
Console.WriteLine(c.CustomerID);
}
Visual Basic
Dim sprocResults As IMultipleResults = db.MultipleResultTypesSequentially()
' まず、製品データを読み取ります。
For Each P As Product In sprocResults.GetResult(Of Product)()
Console.WriteLine(p.ProductID)
Next
' 次に、顧客データを読み取ります。
For Each c As Customer c In sprocResults.GetResult(Of Customer)()
Console.WriteLine(c.CustomerID)
Next
例 4
LINQ to SQL では、出力パラメータを参照パラメータ (ref キーワード) にマップし、値の型については、NULL を許容するパラメータ (例、int?) を宣言します。次の例のプロシージャは、入力パラメータを 1 つ受け取り、出力パラメータを 1 つ返します。
CREATE PROCEDURE GetCustomerCompanyName(
@customerID nchar(5),
@companyName nvarchar(40) output
)
AS
SELECT @companyName = CompanyName FROM Customers
WHERE CustomerID=@CustomerID
マップされたメソッドは次のとおりです。
C#
[StoredProcedure(Name = "GetCustomerCompanyName")]
public int GetCustomerCompanyName(
string customerID, ref string companyName) {
IExecuteResult result =
this.ExecuteMethodCall(this,
((MethodInfo)(MethodInfo.GetCurrentMethod())),
customerID, companyName);
companyName = (string)result.GetParameterValue(1);
return (int)result.ReturnValue;
}
Visual Basic
<StoredProcedure(Name:="GetCustomerCompanyName")> _
Public Function GetCustomerCompanyName( _
customerID As String, ByRef companyName As String) As Integer
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), customerID, _
companyName)
companyName = CStr(result.GetParameterValue(1))
return CInt(result.ReturnValue)
End Function
この場合、メソッドには明示的な戻り値はありませんが、既定の戻り値がマップされます。出力パラメータについては、対応する出力パラメータが予想通りに使用されます。
次のように、上記のストアド プロシージャを呼び出すことができます。
C#
string CompanyName = "";
string customerID = "ALFKI";
db.GetCustomerCompanyName(customerID, ref CompanyName);
Console.WriteLine(CompanyName);
Visual Basic
Dim CompanyName As String = ""
Dim customerID As String = "ALFKI"
db.GetCustomerCompanyName(customerID, CompanyName)
Console.WriteLine(CompanyName)
ユーザー定義関数
LINQ to SQL では、スカラ関数とテーブル値関数の両方がサポートされ、両方に対応するインライン関数もサポートされます。
LINQ to SQL では、システム定義関数の呼び出しと同じように、インライン スカラ関数の呼び出しを処理します。次のクエリを考えてみましょう。
C#
var q =
from p in db.Products
select
new {
pid = p.ProductID,
unitp = Math.Floor(p.UnitPrice.Value)
};
Visual Basic
Dim productInfos = From prod In db.Products _
Select p.ProductID, price = Math.Floor(p.UnitPrice.Value)
ここでは、Math.Floor メソッドの呼び出しは、FLOOR というシステム関数の呼び出しに変換されます。同じように、ユーザ定義関数 (UDF) にマップされた関数の呼び出しは、SQL の UDF の呼び出しに変換されます。
例 1
次に、スカラ ユーザー定義関数 (UDF) である、ReverseCustName() 関数を示します。SQL Server では、この関数は次のように定義できます。
CREATE FUNCTION ReverseCustName(@string varchar(100))
RETURNS varchar(100)
AS
BEGIN
DECLARE @custName varchar(100)
-- 読者の実習用に、この部分は実装されていません。
RETURN @custName
END
次のコードを使用して、スキーマ クラスで定義されたクライアント メソッドをこの UDF にマップできます。メソッドの本体では、メソッド呼び出しの目的を表す式を作成し、作成した式を DataContext に渡して、変換および処理を行います (このような直接処理は、関数が呼び出されたときのみ実行されます)。
C#
[Function(Name = "[dbo].[ReverseCustName]")]
public string ReverseCustName(string string1) {
IExecuteResult result = this.ExecuteMethodCall(this,
(MethodInfo)(MethodInfo.GetCurrentMethod())), string1);
return (string) result.ReturnValue;
}
Visual Basic
Function(Name:= "[dbo].[ReverseCustName]")> _
Public Function ReverseCustName(string1 As String) As String
Dim result As IExecuteResult = ExecuteMethodCall(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), string1)
return CStr(result.ReturnValue)
例 2
次のクエリでは、生成された UDF メソッドである ReverseCustName メソッドのインライン呼び出しを示しています。この場合、関数はすぐには実行されません。このクエリ用に構築された SQL により、データベースで定義された UDF の呼び出しに変換されます (クエリの次の SQL コードを参照してください)。
C#
var q =
from c in db.Customers
select
new {
c.ContactName,
Title = db.ReverseCustName(c.ContactTitle)
};
Visual Basic
Dim customerInfos = From cust In db.Customers _
Select c.ContactName, _
Title = db.ReverseCustName(c.ContactTitle)
SELECT [t0].[ContactName],
dbo.ReverseCustName([t0].[ContactTitle]) AS [Title]
FROM [Customers] AS [t0]
同じ関数をクエリの外側で呼び出すと、LINQ to SQL では (パラメータ @p0 が渡される定数にバインドされている) 次の SQL 構文を持つメソッド呼び出し式から、単純なクエリを作成します。
LINQ to SQL では、次のような式になります。
C#
string str = db.ReverseCustName("LINQ to SQL");
Visual Basic
Dim str As String = db.ReverseCustName("LINQ to SQL")
変換後は次のようになります。
SELECT dbo.ReverseCustName(@p0)
例 3
テーブル値関数 (TVF) では、1 つの結果セットを返します (これに対して、ストアド プロシージャでは複数の結果形式を返すことができます)。TVF の戻り値の型はテーブルなので、テーブルを使用できる任意の SQL で TVF を使用でき、テーブルと同じように TVF を操作できます。
SQL Server で定義されている次のテーブル値関数を考えてみましょう。
CREATE FUNCTION ProductsCostingMoreThan(@cost money)
RETURNS TABLE
AS
RETURN
SELECT ProductID, UnitPrice
FROM Products
WHERE UnitPrice > @cost
この関数では、テーブルを返すことが明示的に指定されているので、返される結果セットの構造は暗黙的に定義されています。LINQ to SQL では、このテーブル値関数を次のようにマップします。
C#
[Function(Name = "[dbo].[ProductsCostingMoreThan]")]
public IQueryable<Product> ProductsCostingMoreThan(
System.Nullable<decimal> cost) {
return this.CreateMethodCallQuery<Product>(this,
(MethodInfo)MethodInfo.GetCurrentMethod(),
cost);
}
Visual Basic
<Function(Name:="[dbo].[ProductsCostingMoreThan]")> _
Public Function ProductsCostingMoreThan(
cost As System.Nullable(Of Decimal)) As IQueryable(Of Product)
Return CreateMethodCallQuery(Of Product)(Me, _
CType(MethodInfo.GetCurrentMethod(), MethodInfo), cost)
次の SQL コードでは、関数から返されたテーブルを結合できること、関数から返されたテーブルであるということを除き、他のテーブルと同じように操作できることを示しています。
SELECT p2.ProductName, p1.UnitPrice
FROM dbo.ProductsCostingMoreThan(80.50)
AS p1 INNER JOIN Products AS p2 ON p1.ProductID = p2.ProductID
LINQ to SQL では、(新しい join 構文を使用して) クエリは次のように記述されます。
C#
var q =
from p in db.ProductsCostingMoreThan(80.50m)
join s in db.Products on p.ProductID equals s.ProductID
select new {p.ProductID, s.UnitPrice};
Visual Basic
Dim productInfos = From costlyProd In db.ProductsCostingMoreThan(80.50m) _
Join prod In db.Products _
On costlyProd.ProductID Equals prod.ProductID _
Select costlyProd.ProductID, prod.UnitPrice
ストアド プロシージャに関する LINQ to SQL の制限事項
LINQ to SQL では、静的に決定された結果セットを返すストアド プロシージャの、コード生成がサポートされています。そのため、LINQ to SQL のコード ジェネレータでは、次のストアド プロシージャはサポートされません。
- 動的な SQL を使用して結果セットを返すストアド プロシージャ。ストアド プロシージャに動的な SQL ステートメントを作成する条件ロジックが含まれる場合、結果セットの生成に使用するクエリは実行時まで特定できないので、LINQ to SQL では結果セットのメタデータを取得できません。
- 一時テーブルに基づいて結果を生成するストアド プロシージャ。
エンティティ クラス ジェネレータ ツール
既存のデータベースがある場合、ただデータベースを表すために手動で完全なオブジェクト モデルを作成する必要はありません。LINQ to SQL には、SQLMetal というツールが付属しています。これは、データベースのメタデータから適切なクラスを推論して、エンティティ クラスを作成する作業を自動化するコマンド ライン ユーティリティです。
SQLMetal を使用して、データベースから SQL メタデータを抽出し、エンティティ クラスの宣言を含むソース ファイルを生成できます。また、このプロセスは、まず SQL メタデータを表す XML ファイルを生成してから、生成した XML ファイルをクラス宣言を含むソース ファイルに変換するという、2 段階に分けることができます。この分割されたプロセスを使用すると、メタデータをファイルとして保持できるので、メタデータを編集できます。XML ファイルを作成する抽出プロセスでは、データベースのテーブル名と列名から、適切なクラス名とプロパティ名に関するいくつかの推論を行います。ジェネレータで、目的に合った結果を作成したり、オブジェクト内に存在する必要のないデータベースの情報を表示させないようにするには、XML ファイルの編集が必要になることがあります。
SQLMetal を使用する最も簡単なシナリオは、既存のデータベースからクラスを直接生成することです。ツールを起動する方法を次に示します。
C#
SqlMetal /server:.\SQLExpress /database:Northwind /pluralize /namespace:nwind /code:Northwind.cs
Visual Basic
SqlMetal /server:.\SQLExpress /database:Northwind /pluralize /namespace:nwind /code:Northwind.vb /language:vb
このツールを実行すると、データベースのメタデータを読み取って生成されたオブジェクト モデルを含む、Northwind.cs ファイルまたは Northwind.vb ファイルが作成されます。データベースのテーブル名が生成するオブジェクトの名前と似ている場合、この方法は効果的です。これらの名前が似ていない場合、2 段階のアプローチを使用する必要があります。
SQLMetal に DBML ファイルを生成するよう指示するには、次のようにツールを使用します。
SqlMetal /server:.\SQLExpress /database:Northwind /pluralize
/xml:Northwind.dbml
DBML ファイルが生成されたら、DBML ファイルに class 属性や property 属性を使用して注釈を付け、テーブルや列をクラスやプロパティにマップする方法を記述することができます。DBML ファイルに注釈を付け終えたら、次のコマンドを実行してオブジェクト モデルを生成できます。
C#
SqlMetal /namespace:nwind /code:Northwind.cs Northwind.dbml
Visual Basic
SqlMetal /namespace:nwind /code:Northwind.vb Northwind.dbml /language:vb
The SQLMetal usage signature is as follows:
SqlMetal [options] [filename]
次の表では、SQLMetal で使用できるコマンド ライン オプションを示しています。
SQLMetal のコマンド ライン オプション
| オプション | 説明 |
| /server:<name> | データベースにアクセスするための、接続先のサーバーを指定します。 |
| /database:<name>
| メタデータの読み取り元になるデータベースの名前を指定します。 |
| /user:<name>
| サーバーのログイン ユーザー ID を指定します。 |
| /password:<name> | サーバーのログイン パスワードを指定します。 |
| /views | データベース ビューを抽出します。 |
| /functions | データベース関数を抽出します。 |
| /sprocs | ストアド プロシージャを抽出します。 |
| /code[:<filename>] | ツールの出力先をエンティティ クラスの宣言のソースファイルに指定します。 |
| /language:<language>
| Visual Basic または C# (既定値) を使用します。 |
| /xml[:<filename>] | ツールの出力先を、データベースのメタデータ、およびクラス名とプロパティ名を簡単に推測する近似値を記述する DBML ファイルに指定します。 |
| /map[:<filename>] | 属性の代わりに外部マッピング ファイルを使用する必要があることを指定します。 |
| /pluralize | 適切なクラス名、およびプロパティ名を作成するために、このツールでテーブル名を英語の複数形または単数形にするヒューリスティックを実行することを指定します。 |
| /namespace:<name> | エンティティ クラスを生成する名前空間を指定します。 |
| /timeout:<seconds> | データベース コマンドに使用するタイムアウト値 (秒単位) を指定します。 |
注 MDF ファイルからメタデータを抽出するには、他のすべてのオプションを指定した後で MDF ファイル名を指定する必要があります。/server を指定しない場合、localhost を指定したと見なされます。
DBML ジェネレータ ツール リファレンス
DBML (Database Mapping Language) ファイルの最も大きな役割は、既存のデータベースの SQL メタデータを説明することです。このファイルは、SQLMetal により、データベースのメタデータが調査されて抽出されます。SQLMetal では、同じファイルを使用して、既定のオブジェクト モデルを生成してデータベースを表現しています。
次に、DBML 構文の典型例を示します。
<?xml version="1.0" encoding="utf-16"?>
<Database Name="Northwind" EntityNamespace="Mappings.FunctionMapping"
ContextNamespace="Mappings.FunctionMapping"
Provider="System.Data.Linq.SqlClient.Sql2005Provider"
xmlns="http://schemas.microsoft.com/dsltools/LINQ to SQLML">
<Table Name="Categories">
<Type Name="Category">
<Column Name="CategoryID" Type="System.Int32"
DbType="Int NOT NULL IDENTITY" IsReadOnly="False"
IsPrimaryKey="True" IsDbGenerated="True" CanBeNull="False" />
<Column Name="CategoryName" Type="System.String"
DbType="NVarChar(15) NOT NULL" CanBeNull="False" />
<Column Name="Description" Type="System.String"
DbType="NText" CanBeNull="True" UpdateCheck="Never" />
<Column Name="Picture" Type="System.Byte[]"
DbType="Image" CanBeNull="True" UpdateCheck="Never" />
<Association Name="FK_Products_Categories" Member="Products"
ThisKey="CategoryID" OtherKey="CategoryID"
OtherTable="Products" DeleteRule="NO ACTION" />
</Type>
</Table>
<Function Name="GetCustomerOrders">
<Parameter Name="customerID" Type="System.String" DbType="NChar(5)" />
<ElementType Name="GetCustomerOrdersResult">
<Column Name="OrderID" Type="System.Int32"
DbType="Int" CanBeNull="True" />
<Column Name="ShipName" Type="System.String"
DbType="NVarChar(40)" CanBeNull="True" />
<Column Name="OrderDate" Type="System.DateTime"
DbType="DateTime" CanBeNull="True" />
<Column Name="Freight" Type="System.Decimal"
DbType="Money" CanBeNull="True" />
</ElementType>
</Function>
</Database>
要素と各要素の属性を以下の表で説明します。
Database
この要素は、XML 形式ファイルの最も外部の要素です。この要素では、生成された DataContext に Database 属性を緩やかにマップします。
Database 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (なし) | データベースの名前です。この属性が存在し、DataContext を生成している場合、Database 属性がこの名前で付与されます。DataContext のクラス属性が存在しない場合、DataContext クラスの名前としても使用されます。 |
| EntityNamespace | String | (なし) | Table 要素内の Type 要素から生成されたクラスの既定の名前空間です。この属性で名前空間を指定しない場合、エンティティ クラスはルートの名前空間に生成されます。 |
| ContextNamespace | String | (なし) | 生成された DataContext クラスの既定の名前空間です。この属性で名前空間を指定しない場合、DataContext クラスはルートの名前空間に生成されます。 |
| Class | String | Database.Name | 生成された DataContext クラスの名前です。この属性が存在しない場合、Database 要素の Name 属性を使用します。 |
| AccessModifier | AccessModifier | Public | 生成された DataContext クラスのアクセシビリティ レベルです。有効な値は、Public、Protected、Internal、および Private です。 |
| BaseType | String | “System.Data.Linq.DataContext” | DataContext クラスの基本型です。 |
| Provider | String | “System.Data.Linq.SqlClient.Sql2005Provider” | DataContext のプロバイダです。既定のプロバイダは Sql2005 プロバイダです。 |
| ExternalMapping | Boolean | False | 外部マッピング ファイルの生成に DBML を使用するかどうかを指定します。 |
| Serialization | SerializationMode | SerializationMode.None | 生成された DataContext とエンティティ クラスがシリアル化可能かどうかを指定します。 |
Database 要素のサブ要素の属性
| サブ要素 | 要素の型 | 出現回数 | 説明 |
| <Table> | Table | 0 ~ 無制限 | 単独の型または継承階層のいずれかにマップされる SQL Server テーブルまたは SQL Server ビューを表します。 |
| <Function> | Function | 0 ~ 無制限 | 生成された DataContext クラスのメソッドにマップされる SQL Server ストアド プロシージャまたはデータベース関数を表します。 |
| <Connection> | Connection | 0 ~ 1 | この DataContext で使用するデータベース接続を表します。 |
Table
この要素は、単独の型または継承階層のいずれかにマップされるデータベース テーブル (またはビュー) を表します。この要素は、生成されたエンティティ クラスの Table 属性に緩やかにマップされます。
Table 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | データベース内のテーブルの名前です。テーブル アダプタの既定の名前は、必要に応じてこの名前を基にして作成されます。 |
| Member | String | Table.Name | このテーブル用に DataContext クラス内に生成されたメンバ フィールドの名前です。 |
| AccessModifier | AccessModifier | Public | DataContext 内の Table<T> 参照のアクセシビリティ レベルです。有効な値は、Public、Protected、Internal、および Private です。 |
Table 要素のサブ要素の属性
| サブ要素 | 要素の型 | 出現回数 | 説明 |
| <Type> | Type | 1 ~ 1 | このテーブルにマップされる型または継承階層を表します。 |
| <InsertFunction> | TableFunction | 0 ~ 1 | 挿入用のメソッドです。この要素が存在する場合、InsertT メソッドが生成されます。 |
| <UpdateFunction> | TableFunction | 0 ~ 1 | 更新用のメソッドです。この要素が存在する場合、UpdateT メソッドが生成されます。 |
| <DeleteFunction> | TableFunction | 0 ~ 1 | 削除用のメソッドです。この要素が存在する場合、DeleteT メソッドが生成されます。 |
Type
この要素は、Table またはストアド プロシージャの結果形式のいずれかの型定義を表します。列と関連付けが指定された新しい CLR 型としてコードで生成されます。
また、複数の型が同じテーブルにマップされている継承階層の構成要素を表すこともできます。その場合、Type 要素は入れ子になって親子の継承リレーションシップを表し、指定された InheritanceCode によってデータベース内で区別されます。
Type 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | 生成される CLR 型の名前です。 |
| InheritanceCode | String | (なし) | この型が継承に参加している場合、関連のある継承コードを設定することによって、テーブルから行を読み込むときに各 CLR 型を区別することができます。InheritanceCode の値が IsDiscriminator 列の値と一致する Type 要素は、読み込まれたオブジェクトのインスタンス化に使用されます。継承コードがない場合は、生成されるエンティティ クラスは抽象クラスです。 |
| IsInheritanceDefault | Boolean | False | 継承階層内の Type 要素のこの属性値が true の場合、定義された継承コードと一致しない行の読み込み時にこの型が使用されます。 |
| AccessModifier | AccessModifier | Public | 作成される CLR 型のアクセシビリティ レベルです。有効な値は、Public、Protected、Internal、および Private です。 |
| Id | String | (なし) | 型には一意の Id を設定できます。型の Id は、他のテーブルや関数で使用できます。Id は、DBML ファイル内のみで使用され、オブジェクト モデルでは使用されません。 |
| IdRef | String | (なし) | IdRef は、他の型の Id を参照するのに使用します。ある Type 要素に IdRef 属性が存在する場合、その Type 要素に含めることができるのは IdRef 情報のみです。IdRef は DBML ファイル内のみで使用され、オブジェクト モデルでは使用されません。 |
Type 要素のサブ要素の属性
| サブ要素 | 要素の型 | 出現回数 | 説明 |
| <Column> | Column | 0 ~ 無制限 | この型のプロパティを表します。これらのプロパティは、この型のテーブル内のフィールドにバインドされます。 |
| <Association> | Association | 0 ~ 無制限 | この型のプロパティを表します。このプロパティは、テーブル間の外部キー リレーションシップの一方にバインドされます。 |
| <Type> | SubType | 0 ~ 無制限 | 継承階層内のこの型のサブタイプを表します。 |
SubType
この要素は、継承階層内の派生型を表します。この要素は、この型で指定された列と関連付けを持つ新しい CLR 型として生成されます。サブタイプには継承属性は生成されません。
Type 要素と違って、SubType 要素には AccessModifier 属性がありません。これは、すべての派生型を public にする必要があるためです。SubTypes 要素は他のテーブルや関数で再利用できないので、Id と IdRef はありません。
SubType 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | 生成される CLR 型の名前です。 |
| InheritanceCode | String | (なし) | この型が継承に参加している場合、関連のある継承コードを設定することによって、テーブルから行を読み込むときに各 CLR 型を区別することができます。InheritanceCode の値が IsDiscriminator 列の値と一致する Type 要素は、読み込まれたオブジェクトをインスタンス化するのに使用します。継承コードがない場合は、生成されるエンティティ クラスは抽象クラスです。 |
| IsInheritanceDefault | Boolean | False | 継承階層内の Type 要素のこの属性値が True の場合、定義された継承コードと一致しない行が読み込まれたときにこの型が使用されます。 |
SubType 要素のサブ要素の属性
| サブ要素 | 要素の型 | 出現回数 | 説明 |
| <Column> | Column | 0 ~ 無制限 | この型のプロパティを表します。これらのプロパティは、この型のテーブル内のフィールドにバインドされます。 |
| <Association> | Association | 0 ~ 無制限 | この型のプロパティを表します。このプロパティは、テーブル間の外部キー リレーションシップの一方にバインドされます。 |
| <Type> | SubType | 0 ~ 無制限 | 継承階層内のこの型のサブタイプを表します。 |
Column
この要素は、クラス内のプロパティ (およびバッキング フィールド) にマップされたテーブル内の列を表します。外部キー リレーションシップには、どちら側にも Column 要素はありませんが、これは Association 要素によって完全に (どちら側でも) 表現されているからです。
Column 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (なし) | この列をマップするデータベース フィールドの名前です。 |
| Member | String |
Name
| この属性を含んでいる型に生成される CLR プロパティの名前です。 |
| Storage | String |
_Member
| この列の値を格納するプライベート CLR バッキング フィールドの名前です。シリアル化する場合は、既定値の場合でも Storage を削除しないでください。 |
| AccessModifier | AccessModifier | Public | 生成される CLR プロパティのアクセシビリティ レベルです。有効な値は、Public、Protected、Internal、および Private です。 |
| Type | String | (必須) | 作成される CLR プロパティとバッキング フィールドの両方の型の名前です。この名前は、生成されるコードがコンパイルされるときに最終的にスコープに含まれていれば、完全修飾名やクラス自体の名前など、どのような名前でもかまいません。 |
| DbType | String | (なし) | この列の完全な SQL Server 型です (NOT NULL などの注釈を含みます)。生成されたクエリを最適化したり、CreateDatabase() を実行する際に詳細を指定したりするために、この属性を指定した場合、この属性は LINQ to SQL によって使用されます。DbType は、常にシリアル化してください。 |
| IsReadOnly | Boolean | False | IsReadOnly が設定されている場合、プロパティの Set アクセス操作子は作成されません。つまり、オブジェクトを使用してこの列の値を変更することはできません。 |
| IsPrimaryKey | Boolean | False | この列がテーブルの主キーに含まれることを示します。LINQ to SQL が正常に動作するにはこの情報が必要です。 |
| IsDbGenerated | Boolean | False | このフィールドのデータがデータベースによって生成されることを示します。主に AutoNumber フィールドと集計フィールドに使用します。これらのフィールドに値を代入することに意味はないので、これらのフィールドは自動的に IsReadOnly になります。 |
| CanBeNull | Boolean | (なし) | 値に NULL 値を含められることを示します。CLR で実際に NULL 値を使用するには、ClrType を Nullable<T> として指定する必要があります。 |
| UpdateCheck | UpdateCheck | Always (他のメンバで IsVersion が設定されている場合は Never) | オプティミスティック同時実行制御の競合検出中に LINQ to SQL でこの列を使用するかどうかを示します。通常、IsVersion 列がない限り、既定によりすべての列が参加します。IsVersion 列は自動的に参加します。値には、Always、Never、または WhenChanged (保持する値が変更された場合に参加します) を指定できます。 |
| IsDiscriminator | Boolean | False | このフィールドに、継承階層内で型を選択する際に使用する識別コードが含まれるかどうかを示します。 |
| Expression | String | (なし) | LINQ to SQL の操作には影響しません。ただし、CreateDatabase() の実行中に、計算列式を表す未処理の SQL 式として使用されます。 |
| IsVersion | Boolean | False | このフィールドが、SQL Server の TIMESTAMP フィールドを表すことを示します。TIMESTAMP フィールドは、行が変更されるたびに自動的に更新されます。このフィールドを使用して、オプティミスティック同時実行制御の競合検出をより効果的に行うことができます。 |
| IsDelayLoaded | Boolean | False | オブジェクトの具体化時にすぐに読み込まれるのではなく、まず関連プロパティがアクセスされてから、この列を読み込む必要があることを示します。サイズの大きな記録用フィールドや必ずしも必要ではない行に格納されたバイナリ データに使用すると便利です。 |
| AutoSync | AutoSync | IsDbGenerated および IsPrimaryKey が True の場合は OnInsert。
IsDbGenerated が True の場合は Always。
それ以外の場合は Never。
| データベースによって生成された値で列が自動的に同期されるかどうかを指定します。有効な値は、OnInsert、Always、および Never です。 |
Association
この要素は、外部キー リレーションシップの一方を表します。一対多のリレーションシップでは、"一" の側が EntitySet<T> になり、"多" の側が EntityRef<T> になります。一対一のリレーションシップでは、両方とも EntityRef<T> になります。
1 つの関連付けで両方に Association エントリを含める必要はありません。一方にのみ Association エントリを含めた場合、Association エントリのある側にのみプロパティが生成され、一方向のリレーションシップが作成されます。
Association 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | リレーションの名前です (通常は外部キー制約名です)。この属性は、技術的には省略可能ですが、同じ 2 つのテーブル間に複数のリレーションシップが存在する場合にあいまいさを避けるために、コードによって常に生成されるようにすることをお勧めします。 |
| Member | String |
Name
| 関連付けのこちら側に生成される CLR プロパティの名前です。 |
| Storage | String | OneToMany が True かつ IsForeignKey が False の場合は
_OtherTable。
それ以外の場合は
_TypeName(OtherTable)。
| この列の値を格納するプライベート CLR バッキング フィールドの名前です。 |
| AccessModifier | AccessModifier | Public | 生成される CLR プロパティのアクセシビリティ レベルです。有効な値は、Public、Protected、Internal、および Private です。 |
| ThisKey | String | この属性を含んでいるクラスの IsIdentity プロパティ | 関連付けのこちら側のキーのコンマ区切りのリストです。 |
| OtherTable | String | (説明を参照) | リレーションシップのもう一方のテーブルです。通常、LINQ to SQL ランタイムによって、一致するリレーションシップ名から判断されますが、一方向のアソシエーションまたは匿名のアソシエーションの場合は LINQ to SQL ランタイムでは判断できません。 |
| OtherKey | String | 外部クラスの主キー | 関連付けのもう一方のキーのコンマ区切りのリストです。 |
| IsForeignKey | Boolean | False | リレーションシップの "子" の側 (一対多の "多" の側) かどうかを示します。 |
| RelationshipType | RelationshipType | OneToMany | この関連付けによって関連付けられているデータを、ユーザーが、一対一の基準を満たしていると見なしているのか、またはより一般的な一対多のケースに適合していると見なしているのかを示します。一対一のリレーションシップでは、ユーザーは、主キー ("一") 側の行に対して、外部キー ("多") 側に対応する行が 1 行しかないと見なしています。そのため、"一" の側に EntitySet<T> ではなく EntityRef<T> が生成されます。有効な値は、OneToOne と OneToMany です。 |
| DeleteRule | String | (なし) | この関連付けに削除の動作を追加するのに使用します。たとえば、CASCADE は外部キー (FK) リレーションシップに ON DELETE CASCADE を追加します。NULL が設定されると、削除の動作は追加されません。 |
Function
この要素は、ストアド プロシージャまたはデータベース関数を表します。各 Function ノードの DataContext クラスにメソッドが生成されます。
Function 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | データベース内のストアド プロシージャの名前です。 |
| Method | String |
Method
| 生成する CLR メソッドの名前です。この CLR メソッドを使用してストアド プロシージャを呼び出すことができます。Method の既定の名前は、Name から [dbo]. などの文字列が取り除かれたものです。 |
| AccessModifier | AccessModifier | Public | ストアド プロシージャ メソッドのアクセシビリティ レベルです。有効な値は、Public、Protected、Internal、および Private です。 |
| HasMultipleResults | Boolean | 型の番号 (1 より大きな値) | Function ノードによって表されているストアド プロシージャから複数の結果セットが返されるかどうかを指定します。結果セットはテーブル形式です。結果セットは、既存の Type か列セットのいずれかです。列セットの場合、列セット用に Type ノードが作成されます。 |
| IsComposable | Boolean | False | 関数またはストアド プロシージャを LINQ to SQL クエリで作成できるかどうかを指定します。作成できるのは、void を返さないデータベース関数のみです。 |
Function 要素のサブ要素の属性
| サブ要素 | 要素の型 | 出現回数 | 説明 |
| <Parameter> | Parameter | 0 ~ 無制限 | このストアド プロシージャの入力パラメータと出力パラメータを表します。 |
| <ElementType> | Type | 0 ~ 無制限 | 対応するストアド プロシージャが返せる表形式を表します。 |
| <Return> | Return | 0 ~ 1 | このデータベース関数またはストアド プロシージャから返されるスカラ型です。Return が NULL の場合、関数によって void が返されます。Function 要素に Return と ElementType の両方のサブ要素を設定することはできません。 |
TableFunction
この要素は、テーブルの CUD オーバーライド関数を表します。LINQ to SQL デザイナでは、LINQ to SQL 用の Insert、Update、および Delete オーバーライド メソッドを作成し、エンティティのプロパティ名をストアド プロシージャ名にマップすることができます。
CUD 関数のメソッド名は固定されているので、DBML の TableFunction 要素には Method 属性はありません。たとえば、Customer テーブルの場合、CUD メソッドは InsertCustomer、UpdateCustomer、および DeleteCustomer という名前になります。
テーブル関数では表形式を返せないので、TableFunction 要素には ElementType 属性はありません。
TableFunction 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | データベース内のストアド プロシージャの名前です。 |
| AccessModifier | AccessModifier | Private | ストアド プロシージャ メソッドのアクセシビリティ レベルです。有効な値は、Public、Protected、Internal、および Private です。 |
| HasMultipleResults | Boolean | 型の番号 (1 より大きな値) | Function ノードによって表されているストアド プロシージャから複数の結果セットが返されるかどうかを示します。結果セットはテーブル形式です。結果セットは、既存の Type か列セットのいずれかです。列セットの場合、列セット用に Type ノードが作成されます。 |
| IsComposable | Boolean | False | 関数またはストアド プロシージャを LINQ to SQL クエリで作成できるかどうかを指定します。作成できるのは、void を返さないデータベース関数のみです。 |
TableFunction 要素のサブ要素の属性
| サブ要素 | 要素の型 | 出現回数 | 説明 |
| <Parameter> | TableFunctionParameter | 0 ~ 無制限 | このテーブル関数の入力パラメータと出力パラメータを表します。 |
| <Return> | TableFunctionReturn | 0 ~ 1 | このテーブル関数から返されるスカラ型です。Return が NULL の場合、関数によって void が返されます。 |
Parameter
この要素は、ストアド プロシージャまたは関数のパラメータを表します。パラメータでは、データの入力や出力を行えます。
Parameter 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | ストアド プロシージャまたは関数のパラメータのデータベース名です。 |
| Parameter | String |
Name
| メソッドのパラメータの CLR 名です。 |
| | String | (必須) | メソッドのパラメータの CLR 名です。 |
| DbType | String | (なし) | ストアド プロシージャまたは関数のパラメータのデータベースの種類です。 |
| Direction | ParameterDirection | In | パラメータが渡される方向です。有効な値は、In、Out、および InOut です。 |
Return
この要素は、ストアド プロシージャまたは関数の戻り値の型を表します。
Return 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Type | String | (必須) | ストアド プロシージャまたは関数の結果の CLR 型です。 |
| DbType | String | (なし) | ストアド プロシージャまたは関数の結果のデータベースの種類です。 |
TableFunctionParameter
この要素は、CUD 関数のパラメータを表します。パラメータでは、データの入力や出力を行えます。すべてのパラメータは、CUD 関数が所属する Table 列にマップされています。型情報は、パラメータがマップされている列から取得できるので、この要素には Type 属性や DbType 属性はありません。
TableFunctionParameter 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Name | String | (必須) | CUD 関数のパラメータのデータベース名です。 |
| Parameter | String |
Name
| メソッドのパラメータの CLR 名です。 |
| Column | String |
Name
| このパラメータがマップされている列名です。 |
| Direction | ParameterDirection | In | パラメータが渡される方向です。有効な値は、In、Out、および InOut です。 |
| Version | Version | Current | PropertyName で参照している特定の列のバージョンが、現在と最初のどちらのバージョンであるかを指定します。Update オーバーライドにのみ適用されます。有効な値は、Current と Original です。 |
TableFunctionReturn
この要素は、CUD 関数の戻り値の型を表します。この要素には、CUD 関数の結果にマップされた列名のみが含まれています。戻り値の型情報は列から取得できます。
TableFunctionReturn 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| Column | String | (なし) | 戻り値がマップされる列名です。 |
Connection
この要素は、データベースの既定の接続パラメータを表します。この要素を使用すると、データベースへの接続方法が指定された DataContext 型の既定のコンストラクタを作成できます。
既定で使用できる接続方法は 2 種類あります。1 つは ConnectionString 自体を使用する方法で、もう 1 つは App.Settings から読み取る方法です。
Connection 要素の属性
| 属性 | データ型 | 既定値 | 説明 |
| UseApplicationSettings | Boolean | False | App.Settings ファイルを使用するのか、ConnectionString からアプリケーションの設定を直接取得するのかを示します。 |
| ConnectionString | String | (なし) | SQL データ プロバイダに送信する接続文字列です。 |
| SettingsObjectName | String | Settings | プロパティの取得先の App.Settings Object です。 |
| SettingsPropertyName | String | ConnectionString | ConnectionString を含んだ App.Settings プロパティです。 |
多層エンティティ
2 層アプリケーションでは、単一の DataContext によって照会と更新が行われますが、多層アプリケーションでは、多くの場合、照会と更新に個別の DataContext インスタンスを使用する必要があります。たとえば、ASP.NET アプリケーションの場合、照会と更新は、Web サーバーへの個別の要求に対して行われます。そのため、複数の要求にまたがって同じ DataContext インスタンスを使用することは現実的ではありません。このような場合、DataContext インスタンスでは、取得していないオブジェクトを更新できる必要があります。LINQ to SQL での多層エンティティのサポートにより、Attach() メソッドを介してこの機能が提供されます。
次の例では、個別の DataContext インスタンスを使用して Customer オブジェクトを変更する方法を紹介します。
C#
// Customer エンティティが、別の層で (たとえばブラウザから) 変更されたとします。
// 中間層では、新しいコンテキストが使用される必要があります。
Northwind db2 = new Northwind(…);
// 変更を適用するための新しいエンティティを作成します。
Customer C2 = new Customer();
C2.CustomerID ="NewCustID";
// オプティミスティック同時実行制御のチェックに必要な他のプロパティを設定します。
C2.CompanyName = "New Company Name Co.";
...
// LINQ to SQL で、挿入のためではなく、更新のためにこのオブジェクトを追跡するように指定します。
db2.Customers.Attach(C2);
// ここで変更を適用します。
C2.ContactName = "Mary Anders";
// これで、DataContext によって Customer エンティティの更新方法が認識されます。
db2.SubmitChanges();
Visual Basic
' Customer エンティティが、別の層で (たとえばブラウザから) 変更されたとします。
' 中間層では、新しいコンテキストが使用される必要があります。
Dim db2 As Northwind = New Northwind(…)
' 変更を適用するための新しいエンティティを作成します。
Dim C2 As New Customer()
C2.CustomerID =”NewCustID”
' オプティミスティック同時実行制御のチェックに必要な他のプロパティを設定します。
C2.CompanyName = ”New Company Name Co.”
...
' LINQ to SQL で、挿入のためではなく、更新のためにこのオブジェクトを追跡するように指定します。
db2.Customers.Attach(C2)
' ここで変更を適用します。
C2.ContactName = "Mary Anders"
' これで、DataContext によって Customer エンティティの更新方法が認識されます。
db2.SubmitChanges()
多層アプリケーションでは、容易性、相互運用性、またはプライバシーを確保するために、エンティティ全体が層を越えて送信されることはほとんどありません。たとえば、業者が、中間層で使用される Order エンティティとは異なる Web サービスのデータ コントラクトを定義する場合があります。同様に、ある Web ページでは、Employee エンティティのメンバのサブセットしか表示されない場合があります。そのため、多層エンティティのサポートは、このような状況に対応するようにデザインされています。次の 1 つ以上の分類に属するメンバのみが、層の間で転送され、Attach() の呼び出し前に設定される必要があります。
- エンティティの ID の一部であるメンバ
- 変更されているメンバ
- オプティミスティック同時実行制御のチェックに参加するメンバ
タイムスタンプ列またはバージョン番号列がオプティミスティック同時実行制御のチェックで使用される場合、対応するメンバは、Attach() の呼び出し前に設定される必要があります。その他のメンバの値は、Attach() の呼び出し前に設定される必要はありません。LINQ to SQL では、最小限の更新がオプティミスティック同時実行制御のチェックで使用されます。つまり、オプティミスティック同時実行制御の対象として設定またはチェックされていないメンバは無視されます。
オプティミスティック同時実行制御のチェックに必要な元の値は、LINQ to SQL の API の範囲外のさまざまなメカニズムを使用して保持することができます。ASP.NET アプリケーションでは、ビュー ステート (またはビュー ステートを使用するコントロール) を使用することができます。Web サービスは、更新処理で元の値を使用できるように、更新のメソッドで DataContract を使用することができます。相互運用性と汎用性を実現するため、LINQ to SQL では、層の間でやり取りされるデータの形式や、元の値をラウンド トリップするためのメカニズムは指定されません。
挿入または削除のためのエンティティは、Attach() メソッドを必要としません。挿入や削除には、2 層アプリケーションで使用される Table.Add() メソッドと Table.Remove() メソッドを使用できます。2 層間の更新の場合のように、ユーザーは外部キー制約を処理する必要があります。注文のある顧客を削除しないようにするための外部キー制約がデータベースにある場合、注文を処理せずに注文のある顧客を削除することはできません。
LINQ to SQL では、更新のためのエンティティのアタッチも遷移的に処理されます。ユーザーは、基本的に、希望どおりに更新前のオブジェクト グラフを作成し、Attach() を呼び出します。その後、次のように、アタッチされたグラフですべての変更が "再生" され、必要な更新が行われます。
C#
Northwind db1 = new Northwind(…);
// Customer c1 および関連する Order o1、Order o2 が取得されているとします。
// 中間層では、新しいコンテキストが使用される必要があります。
Northwind db2 = new Northwind(…);
// 変更を適用するための新しいエンティティを作成します。
Customer c2 = new Customer();
c2.CustomerID = c.CustomerID;
Order o2 = new Order();
o2.OrderID = ...;
c2.Orders.Add(o2);
// 更新に必要なその他の関連オブジェクトを追加します。
// オプティミスティック同時実行制御のチェックに必要なプロパティを設定します。
...
// 削除される Order o1 です。
Order o1 = new Order();
o1.OrderID = ...;
// LINQ to SQL で、グラフが遷移的に追跡されるように指定します。
db2.Customers.Attach(c2);
// ここですべての変更を "再生" します。
// 更新します。
c2.ContactName = ...;
o2.ShipAddress = ...;
// 挿入する新しいオブジェクトです。
Order o3 = new Order();
o3.OrderID = ...;
c2.Orders.Add(o3);
// Order o1 を削除します。
db2.Orders.Remove(o1);
// これで、DataContext によって更新、挿入、削除の方法が認識されます。
db2.SubmitChanges();
Visual Basic
Dim db1 As Northwind = New Northwind(…)
' Customer c1 および関連する Order o1、Order o2 が取得されているとします。
' 中間層では、新しいコンテキストが使用される必要があります。
Dim db2 As Northwind = New Northwind(…)
' 変更を適用するための新しいエンティティを作成します。
Customer c2 = new Customer()
c2.CustomerID = c.CustomerID
Dim o2 As Order = New Order()
o2.OrderID = ...
c2.Orders.Add(o2)
' 更新に必要なその他の関連オブジェクトを追加します。
' オプティミスティック同時実行制御のチェックに必要なプロパティを設定します。
...
' 削除される Order o1 です。
Dim o1 As Order = New Order()
o1.OrderID = ...
' LINQ to SQL で、グラフが遷移的に追跡されるように指定します。
db2.Customers.Attach(c2)
' ここですべての変更を "再生" します。
' 更新します。
c2.ContactName = ...
o2.ShipAddress = ...
' 挿入する新しいオブジェクトです。
Dim o3 As Order = New Order()
o3.OrderID = ...
c2.Orders.Add(o3)
' Order o1 を削除します。
db2.Orders.Remove(o1)
' これで、DataContext によって更新、挿入、削除の方法が認識されます。
db2.SubmitChanges()
外部マッピング
LINQ to SQL では、属性ベースのマッピングに加え、外部マッピングもサポートされます。外部マッピングの最も一般的な形態は、XML ファイルです。マッピング ファイルを使用すると、コードからマッピングを分離することが好ましいシナリオにも対応できるようになります。
DataContext には、MappingSource を使用できるようにするための追加のコンストラクタが用意されています。MappingSource の 1 つには、XML マッピング ファイルから作成できる XmlMappingSource があります。
次の例に、マッピング ファイルの使用方法を示します。
C#
String path = @"C:\Mapping\NorthwindMapping.xml";
XmlMappingSource prodMapping =
XmlMappingSource.FromXml(File.ReadAllText(path));
Northwind db = new Northwind(
@"Server=.\SQLExpress;Database=c:\Northwind\Northwnd.mdf",
prodMapping
);
Visual Basic
Dim path As String = "C:\Mapping\NorthwindMapping.xml"
Dim prodMapping As XmlMappingSource = _
XmlMappingSource.FromXml(File.ReadAllText(path))
Dim db As Northwind = New Northwind( _
"Server=.\SQLExpress;Database=c:\Northwind\Northwnd.mdf", _
prodMapping )
Product クラスのマッピングを示しているマッピング ファイルからの抜粋を次に示します。Northwind データベースの Products テーブルにマップされた、Mapping 名前空間の Product クラスを示しています。要素と属性は、属性名およびパラメータと一致します。
<?xml version="1.0" encoding="utf-8"?>
<Database xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="Northwind"
ProviderType="System.Data.Linq.SqlClient.Sql2005Provider">
<Table Name="Products">
<Type Name="Mappings.FunctionMapping.Product">
<Column Name="ProductID" Member="ProductID" Storage="_ProductID"
DbType="Int NOT NULL IDENTITY" IsPrimaryKey="True"
IsDBGenerated="True" AutoSync="OnInsert" />
<Column Name="ProductName" Member="ProductName" Storage="_ProductName"
DbType="NVarChar(40) NOT NULL" CanBeNull="False" />
<Column Name="SupplierID" Member="SupplierID" Storage="_SupplierID"
DbType="Int" />
<Column Name="CategoryID" Member="CategoryID" Storage="_CategoryID"
DbType="Int" />
<Column Name="QuantityPerUnit" Member="QuantityPerUnit"
Storage="_QuantityPerUnit" DbType="NVarChar(20)" />
<Column Name="UnitPrice" Member="UnitPrice" Storage="_UnitPrice"
DbType="Money" />
<Column Name="UnitsInStock" Member="UnitsInStock" Storage="_UnitsInStock"
DbType="SmallInt" />
<Column Name="UnitsOnOrder" Member="UnitsOnOrder" Storage="_UnitsOnOrder"
DbType="SmallInt" />
<Column Name="ReorderLevel" Member="ReorderLevel" Storage="_ReorderLevel"
DbType="SmallInt" />
<Column Name="Discontinued" Member="Discontinued" Storage="_Discontinued"
DbType="Bit NOT NULL" />
<Association Name="FK_Order_Details_Products" Member="OrderDetails"
Storage="_OrderDetails" ThisKey="ProductID" OtherTable="Order Details"
OtherKey="ProductID" DeleteRule="NO ACTION" />
<Association Name="FK_Products_Categories" Member="Category"
Storage="_Category" ThisKey="CategoryID" OtherTable="Categories"
OtherKey="CategoryID" IsForeignKey="True" />
<Association Name="FK_Products_Suppliers" Member="Supplier"
Storage="_Supplier" ThisKey="SupplierID" OtherTable="Suppliers"
OtherKey="SupplierID" IsForeignKey="True" />
</Type>
</Table>
</Database>
.NET Framework 関数のサポートと注意事項
次の段落では、LINQ to SQL 型のサポートと .NET Framework との違いに関する基本情報を提供します。
プリミティブ型
実装済み
- 算術演算子および比較演算子
- シフト演算子 : << および >>
- 文字と数値間の変換は、UNICODE または NCHAR によって行われます。それ以外の場合、SQL の CONVERT が使用されます。
未実装
-
<Type>.Parse
- 列挙型は、テーブル内の整数と文字列に使用およびマップされます。文字列については、Parse メソッドと ToString() メソッドが使用されます。
.NET との違い
- 倍精度浮動小数点数型の ToString の出力は、SQL で CONVERT(NVARCHAR(30), @x, 2) を使用します。これは、常に、16 桁および "指数表記" を使用します。たとえば、0 の場合 "0.000000000000000e+000" となります。したがって、.NET の Convert.ToString() とは異なる文字列が返されます。
System.String
実装済み
- 非静的メソッド
- Length、Substring、Contains、StartsWith、EndsWith、IndexOf、Insert、Remove、Replace、Trim、ToLower、ToUpper、LastIndexOf、PadRight、PadLeft、Equals、CompareTo。StringComparison パラメータなどを受け取る場合を除き、すべてのシグネチャはサポートされます。詳細は以下のとおりです。
- 静的メソッド
Concat(...) all signatures
Compare(String, String)
String (indexer)
Equals(String, String)
- コンストラクタ
- 演算子
+、==、!= (Visual Basic の +、=、および <>)
未実装
- 文字の配列を受け取るまたは生成するメソッド。
- CultureInfo、StringComparison、または IFormatProvider を受け取るメソッド。
- 静的メソッド (Visual Basic では共有メソッド)
Copy(String str)
Compare(String, String, Boolean)
Compare(String, String, StringComparison)
Compare(String, String, Boolean, CultureInfo)
Compare(String, Int32, String, Int32, Int32)
Compare(String, Int32, String, Int32, Int32, Boolean)
Compare(String, Int32, String, Int32, Int32, StringComparison)
Compare(String, Int32, String, Int32, Int32, Boolean, CultureInfo)
CompareOrdinal(String, String)
CompareOrdinal(String, Int32, String, Int32, Int32)
Join(String, ArrayOf String [,...]) All Join version with first three args
- インスタンス
ToUpperInvariant()
Format(String, Object) + overloads
IndexOf(String, Int32, StringComparison)
IndexOfAny(ArrayOf Char)
Normalize()
Normalize(NormalizationForm)
IsNormalized()
Split(...)
StartsWith(String, StringComparison)
ToCharArray()
ToUpper(CultureInfo)
TrimEnd(ParamArray Char)
TrimStart(ParamArray Char)
制限および .NET との違い
SQL では、照合順序を使用して文字列の等値性と順序を特定します。これらは、SQL Server インスタンス、データベース、テーブル列、または式で指定できます。
これまでに実装した関数の変換によって、照合順序が変更されたり、変換された式で異なる照合順序が指定されたりすることはありません。そのため、既定の照合順序で大文字と小文字が区別されない場合、CompareTo や IndexOf などの関数では、(大文字と小文字が区別される) .NET 関数とは異なる結果が返されることがあります。
StartsWith(str) メソッドや EndsWith(str) メソッドでは、引数 str が定数またはクライアントで評価された式であることが想定されます。つまり、現在、str に列を使用することはできません。
System.Math
実装済みの静的メソッド
- すべてのシグネチャ
- Abs、Acos、Asin、Atan、Atan2、BigMul、Ceiling、Cos、Cosh、Exp、Floor、Log、Log10、Max、Min、Pow、Sign、Sinh、Sqrt、Tan、Tanh、または Truncate。
未実装
- IEEERemainder。
- DivRem には out パラメータが含まれるため、式で使用できません。定数 Math.PI と Math.E はクライアント側で評価されるため、変換は必要ありません。
.NET との違い
.NET 関数 Math.Round に相当する SQL 関数は、ROUND です。変換は、MidpointRounding 列挙値を示すオーバーロードが指定されたときにのみサポートされます。MidpointRounding.AwayFromZero は SQL の動作で、MidpointRounding.ToEven は CLR の動作を示します。
System.Convert
実装済み
- To<Type1>(<Type2> x) という形式で、Type1 と Type2 が次のいずれかのメソッド
- bool、byte、char、DateTime、decimal、double、float、Int16、Int32、Int64、または string。
- 動作がキャストと同一
- ToString(Double) では、完全な精度を取得するための特別なコードがあります。
- Int32 と Char の変換では、LINQ to SQL によって SQL の UNICODE 関数または NCHAR 関数が使用されます。
- それ以外の場合、変換には CONVERT 関数が使用されます。
未実装
- ToSByte、UInt16、32、64: これらの型は SQL に存在しません。
To<integer type>(String, Int32)
ToString(..., Int32) Int32 toBase で終わるオーバーロード
IsDBNull(Object)
GetTypeCode(Object)
ChangeType(...)
- IFormatProvider パラメータが含まれるバージョン。
- 配列に関連するメソッド (To/FromBase64CharArray、To/FromBase64String)。
System.TimeSpan
実装済み
- コンストラクタ
TimeSpan(Long)
TimeSpan (year, month, day)
TimeSpan (year, month, day, hour, minutes, seconds)
TimeSpan (year, month, day, hour, minutes, seconds, milliseconds)
- 演算子
比較演算子 : C# では < や == など、Visual Basic では < や = など
+、-
- 静的メソッド (Visual Basic の場合は共有メソッド)
- 非静的 (インスタンス) メソッドおよびプロパティ
Ticks, Milliseconds, Seconds, Hours, Days
TotalMilliseconds, TotalSeconds, TotalMinutes, TotalHours, TotalDays,
Equals, CompareTo(TimeSpan)
Add(TimeSpan), Subtract(TimeSpan)
Duration() [= ABS], Negate()
未実装
ToString()
TimeSpan FromDay(Double), FromHours, all From Variants
TimeSpan Parse(String)
System.DateTime
実装済み
- コンストラクタ
DateTime(year, month, day)
DateTime(year, month, day, hour, minutes, seconds)
DateTime(year, month, day, hour, minutes, seconds, milliseconds)
- 演算子
Comparisons
DateTime ? DateTime (gives TimeSpan)
DateTime + TimeSpan (gives DateTime)
DateTime ? TimeSpan (gives DateTime)
- 静的メソッド (共有メソッド)
Add(TimeSpan), AddTicks(Long),
AddDays/Hours/Milliseconds/Minutes (Double)
AddMonths/Years(Int32)
Equals
- 非静的 (インスタンス) メソッドおよびプロパティ
Day, Month, Year, Hour, Minute, Second, Millisecond, DayOfWeek
CompareTo(DateTime)
TimeOfDay()
Equals
ToString()
.NET との違い
SQL の datetime 値は、.000、.003、または .007 秒に丸められるため、.NET よりも精度が低くなります。
SQL の datetime 値の範囲は、1753 年 1 月 1 日から始まります。
SQL には、TimeSpan の組み込み型はありません。SQL では、32 ビットの整数を返す、2 つの異なる DATEDIFF メソッドを使用します。1 つは、日数を返す DATEDIFF(DAY,...) です。もう 1 つは、ミリ秒の数字を返す DATEDIFF(MILLISECOND,...) です。DateTimes が 24 日を超えるとエラーになります。対照的に、.NET では 64 ビットの整数を使用し、TimeSpans の値を測定します。
SQL で、できるだけ .NET セマンティクスに近づけるため、LINQ to SQL では TimeSpans を 64 ビットの整数に変換し、上述の 2 つの DATEDIFF メソッドを使用して 2 つの日付の差を計算します。
DateTime UtcNow は、(データベース データが含まれない式と同様に) クエリの変換時にクライアントで評価されます。
未実装
IsDaylightSavingTime()
IsLeapYear(Int32)
DaysInMonth(Int32, Int32)
ToBinary()
ToFileTime()
ToFileTimeUtc()
ToLongDateString()
ToLongTimeString()
ToOADate()
ToShortDateString()
ToShortTimeString()
ToUniversalTime()
FromBinary(Long), FileTime, FileTimeUtc, OADate
GetDateTimeFormats(...)
constructor DateTime(Long)
Parse(String)
DayOfYear
デバッグのサポート
DataContext によって、照会および変更処理のために生成される SQL を取得するメソッドとプロパティが提供されます。これらのメソッドは、LINQ to SQL 機能を理解し、特定の問題をデバッグするのに役立ちます。
生成される SQL を取得するための DataContext メソッド
| メンバ | 目的 |
| Log | SQL を、実行される前に出力します。クエリ、挿入、更新、削除の各コマンドに対応します。使用方法は次のとおりです。
C#
db.Log = Console.Out;
Visual Basic
db.Log = Console.Out
|
| GetQueryText(query) | クエリのクエリ テキストを、実行することなく返します。使用方法は次のとおりです。
C#
-
Console.WriteLine(db.GetQueryText(db.Customers));
Visual Basic
Console.WriteLine(db.GetQueryTest(db.Customers))
|
| GetChangeText() | 挿入、更新、削除のための SQL コマンドのテキストを、実行することなく返します。使用方法は次のとおりです。
C#
-
Console.WriteLine(db.GetChangeText());
Visual Basic
Console.WriteLine(db.GetChangeText())
|