LINQ to Objects, XML に続いて、 LINQ to SQL の備忘録です。個人的にはリレーショナルデータベースの永続化についてはEntity Framework が本命だと思っていますが、抑えておくべきだろうと考えて備忘録を掲載します。LINQ to SQL で使用するEntity クラスは本来ならSQLMetalやVisual Studio のデザイナ上で作成することになると思いますが、今回は手作業 Entity クラスの作成, Entity 継承の使用とEntity 間の関連の定義を行い、LINQ to SQL の動作確認をします。
動作確認環境
- 実行OS: Windows Vista Enterprise
- 開発環境 : Visual Studio 2008 Professional
- .NET 3.5
サンプルの動作確認に AdventureWorks データベースを使用しています。AdventureWorksの設定方法はこちらを参照して下さい。
1. Entityクラスを手動で作成してクエリを発行する
手始めに、手動で Entity クラスを使用してselect 文を発行してみます。まず、Console Application のプロジェクトを作成した後、Entity クラスを次のように作成します。
using System; using System.Collections.Generic; using System.Text; using System.Data.Linq.Mapping; using System.Linq; namespace LINQ2SQL.Entity { /// <summary> /// TableアトリビュートにはSystem.Data.Linqへの参照が必要 /// </summary> [Table(Name="Production.Product")] public class Product { private int _productID; [Column(Storage="_productID", IsPrimaryKey=true, IsDbGenerated=true, DbType="int NOT NULL IDENTITY")] public int ProductID { get { return _productID; } set { _productID = value; } } [Column] public string Name; [Column] public string ProductNumber; [Column(CanBeNull=true)] public decimal? Weight; [Column(Name="StandardCost")] public decimal Cost; } }
いろいろ試してみたかったので、属性にプロパティをいろいろ設定しています。System.Data.Linq.dllの参照を追加した後、クラスにマッピングの定義をしています。Table属性でマッピングするテーブル名を指定します。Column属性でテーブルのどのカラムと対応するかを設定します。フィールド名とテーブルのカラム名が一致していない場合は、ColumnAttribute.Nameプロパティでカラム名を指定します。NULLの可能性がある場合は、フィールド(またはプロパティ)もNull許容型にする必要があります。プライマリキーの場合はIsPrimaryKeyプロパティにtrueを設定します。Column.Storeageプロパティに実際にデータを格納するフィールドを指定すると、LINQ to SQL によって、値が読み書きされるときにプロパティではなくStorageで指定されたフィールドを使用して読み書きを行うようにできます。これにより、LINQ to SQL 実行時にプロパティの変更イベントを発生させないように制御できるようになります。このほか、ColumnAttributeのプロパティを見ると、何を表しているのかがわかりますので詳細はMSDNドキュメントを参照して下さい。
次に既定で作成されるProgram.cs内を次のように編集して実行してみます。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using LINQ2SQL.Entity; using System.Data.Linq; using System.Configuration; namespace LINQ2SQL1 { class Program { static string ConnectionString = ConfigurationManager.ConnectionStrings["AdventureWorksConnection"].ConnectionString; static void Main(string[] args) { FirstLinq2Sql(); Console.WriteLine("input enter"); Console.ReadLine(); } public static void FirstLinq2Sql() { DataContext dc = new DataContext(ConnectionString); Table<Product> products = dc.GetTable<Product>(); var query = from p in products where p.ProductID < 10 select new { p.ProductID, p.Name, p.Weight, p.Cost }; // 作成されたSQLを確認する場合はDataContext.GetCommand()メソッドを使用 // してDbCommandを取得し,DbCommand.CommandTextプロパティを参照します。 Console.WriteLine("Generated SQL:-----------------------"); Console.WriteLine(dc.GetCommand(query).CommandText ); Console.WriteLine("Trace:-------------------------------"); // 実行されるすべてのSQL文のトレースを取得する場合はDataContext.Logプロパティに // TextWriterを設定します。 dc.Log = Console.Out; foreach (var row in query) { Console.WriteLine(row); } } } }
実行結果は次のようになります。

サンプルではDataContext.LogにConsole.Outを指定してトレース結果を出力するようにしています。また、DataContext.GetCommand()メソッドを使用してDbCommandを取得し、DbCommand.CommandTextプロパティから生成されたSQL文を表示しています。
2. Entity クラスの継承を行う
LINQ to SQL では、InheritanceMappingAttribute を使用するとこで、単一テーブル継承のテーブル(ビュー)に対して Entity 継承機能により、異なるEntity クラスを作成させることができます。差分継承テーブルや、具象継承テーブルの場合でも、VIEWを定義することで同様にEntity継承を使用することができます。ColumnAttribute.IsDescriminatorをtrueに設定して、継承クラスを区別するプロパティかを指定できます。次のようにProductクラスを継承したクラスを作成します。
using System; using System.Collections.Generic; using System.Text; using System.Data.Linq.Mapping; using System.Linq; namespace LINQ2SQL.Entity { /// <summary> /// TableアトリビュートにはSystem.Data.Linqへの参照が必要 /// </summary> [Table(Name = "Production.Product")] [InheritanceMapping(Code = "L ", Type = typeof(LProduct))] [InheritanceMapping(Code = "M ", Type = typeof(MProduct))] [InheritanceMapping(Code = "H ", Type = typeof(HProduct))] [InheritanceMapping(Code = "K ", Type = typeof(Product), IsDefault = true)] public class Product { private int _productID; [Column(Storage = "_productID", IsPrimaryKey = true, IsDbGenerated = true, DbType = "int NOT NULL IDENTITY")] public int ProductID { get { return _productID; } set { _productID = value; } } [Column] public string Name; [Column] public string ProductNumber; [Column(CanBeNull = true)] public decimal? Weight; [Column(Name = "StandardCost")] public decimal Cost; [Column(Name = "Class", IsDiscriminator = true, CanBeNull = true)] public string Category; } public class LProduct : Product { [Column] public string Style; } public class MProduct : Product { } public class HProduct : Product { } }
Categoryプロパティ(カラム名がClass)の値がL,M,H,Kによって、クエリ時に作成するEntityクラスを変更できるようにしています。
動作確認メソッドを次のように定義します。
public static void EntityInheritanceSample() { DataContext dc = new DataContext(ConnectionString); Table<Product> products = dc.GetTable<Product>(); var query = from p in products where p.ProductID < 320 && p.ProductID > 310 select p; // 作成されたSQLを確認する場合はDataContext.GetCommand()メソッドを使用 // してDbCommandを取得し,DbCommand.CommandTextプロパティを参照します。 Console.WriteLine("Generated SQL:-----------------------"); Console.WriteLine(dc.GetCommand(query).CommandText); dc.Log = Console.Out; foreach (var row in query) { Console.WriteLine(row.GetType()); } dc.Log = null; Console.WriteLine("use is operator"); query = from p in products where p is HProduct select p; foreach (var row in query.Take(2)) { Console.WriteLine(row.GetType()); } Console.WriteLine("use OfType extention method"); var query1 = from p in products.OfType<LProduct>() select p; foreach (var row in query1.Take(2)) { Console.WriteLine(row.GetType()); } }
実行結果は次のようになります。最初のクエリでは、自動でEntityクラスが適切に生成されていることが確認できます。そのほかis演算子を使用して作成するクラスのみを選択するようにしているのが2番目のクエリです。3番目のクエリはOfType拡張メソッドを使用して、フィルタされるレコードがLProductに該当する(Categoryの値がL)レコードのみが選択されるようにしています。

3. Entity 間の関連を定義する
Entity クラス間の関連を定義することで、Entityオブジェクトから外部キーによって関連するEntityクラスのオブジェクトに遅延ローディングを利用して、透過的にアクセスできるようになります。関連は EntityRef , EntitySet のプロパティまたはフィールドに対して、AssociationAttributeを付与することで行います。例えば、Orderテーブルに外部キーCustomerIDカラムがあり、Customerテーブルの関連するレコードが一意に決まるとすると、Orderクラス内で、EntityRef<Customer>として、関連のプロパティを定義することができます。一方、顧客(Customer)は複数の注文をしている一対多の関連なので、CustomerからOrderのColumnID列に一致する1対多関連のOrderオブジェクトを参照するには、EntitySetを使用して関連プロパティをCustomerクラスに定義します。
AdventureWorksのSales.SalesOrderHeaderのCustomerIDはPerson.Contactへの外部キーとなっています。この関連を定義してみます。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data.Linq.Mapping; using System.Data.Linq; namespace LINQ2SQL.Entity { /// <summary> /// TableアトリビュートにはSystem.Data.Linqへの参照が必要 /// </summary> [Table(Name = "Sales.SalesOrderHeader")] public class SalesOrder { [Column(IsPrimaryKey=true)] public int SalesOrderID; [Column] public DateTime OrderDate; [Column] public DateTime DueDate; [Column(CanBeNull=true)] public DateTime? ShipDate; [Column] public decimal SubTotal; [Column] public decimal TotalDue; [Column] public int CustomerID; private EntityRef<Customer> _customer; [Association(Storage="_customer", ThisKey="CustomerID", IsForeignKey=true)] public Customer Customer { get { return _customer.Entity; } set { _customer.Entity = value; } } } [Table(Name = "Person.Contact")] public class Customer { public Customer() { _salesOrders = new EntitySet<SalesOrder>(); } [Column(Name="ContactID", IsPrimaryKey=true)] public int CustomerID; [Column] public string FirstName; [Column] public string LastName; private EntitySet<SalesOrder> _salesOrders; [Association(OtherKey="CustomerID", Storage="_salesOrders")] public EntitySet<SalesOrder> SalesOrders { get { return _salesOrders; } set { _salesOrders.Assign(value); } } } }
SalesOrder クラスで、Customerプロパティを定義しています。SalesOrderクラスのCustomerID が外部キーなので、AssociationAttributeの ThisKey プロパティにCustomerID を設定し、IsForeighKey に true を設定しています。一方 Customer クラスにはEntitySet<SalesOrder> 型のSalesOrdersプロパティが設定してあります。 Customer に関連する SalesOrder は複数ある可能性があり、SalesOrder プロパティの 外部キーCustomerID とCustomer クラスの キーCustomerID が一致するSalesOrderを取得するため、AssociationAttributeの OtherKey プロパティにCustomerID を設定しています。
動作確認ようのメソッドを次のように定義します。
public static void SalesOrder2() { DataContext dc = new DataContext(ConnectionString); Table<SalesOrder> orders = dc.GetTable<SalesOrder>(); var query = from o in orders select o; foreach (var item in query.Take(1)) { string order = "SalesID={0}, Name={0}"; Console.WriteLine(string.Format(order, item.SalesOrderID, item.Customer.FirstName + " " + item.Customer.LastName)); foreach (var o in item.Customer.SalesOrders) { string msg = "ID={0}, Total={1}"; Console.WriteLine(string.Format(msg, o.SalesOrderID, o.SubTotal)); } } }
プログラムでは、SalesOrderを一件取得し、求まったSalesOrderのCustomerプロパティから関連するCustomerを取得し、標準出力にセールスIDと名前を出力しています。さらに、Customerから逆にSalesOrdersプロパティを参照して、関連するSalesOrderをすべて取得し、取得したSalesOrderを出力しています。
実行結果は次のようになります。2番目に出力されているCustomer.SalesOrdersのSalesOrderIDが1番目に出力されている最初のクエリで取得したSalesOrderのSalesOrderIDと一致していることが確認できます。

DataLoadOptions を使用して即時ローディングを実現する
上記の例では、例えばSalesOrderからCustomerのデータを参照する場合は、遅延ローディング(deferred loading)により、プロパティが参照されたときに初めてSQLが作成され、クエリの結果から、Customer オブジェクトが作成されます。遅延ローディングではなく、SalesOrder構築時に同時に関連クラスを作成したい場合(immediate loading)は、DataLoadOptionsのLoadWithメソッドを使用します。LoadWithを使用した即時ロードのサンプルを次に示します。サンプルではLoadWithのラムダ式で単純にCustomerのみを選択するようにしていますが、EntitySetプロパティの場合、特定のレコードのみを選択するクエリ式(Customer.SalesOrdersを即時ロードする場合に、注文を20以上したSalesOrderを即時ロードするなど)を書くこともできます。
public static void SalesOrder3() { DataContext dc = new DataContext(ConnectionString); Table<SalesOrder> orders = dc.GetTable<SalesOrder>(); // DataLoadOptions.LoadWithを使用して即時ロードを使用するように設定する DataLoadOptions loadOptions = new DataLoadOptions(); loadOptions.LoadWith<SalesOrder>(o => o.Customer); dc.LoadOptions = loadOptions; dc.Log = Console.Out; var query = from o in orders select o; foreach (var item in query.Take(1)) { string order = "SalesID={0}, Name={0}"; Console.WriteLine(string.Format(order, item.SalesOrderID, item.Customer.FirstName + " " + item.Customer.LastName)); foreach (var o in item.Customer.SalesOrders) { string msg = "ID={0}, Total={1}"; Console.WriteLine(string.Format(msg, o.SalesOrderID, o.SubTotal)); } } }
実行結果は次のようになります。作成されたクエリを見ると、SalesOrderとCustomerを作成するためのテーブルの列を取得するSQLを1つのクエリで発行していることが確認できます。

今回は以上です。誤り、指摘点などありましたらご連絡ください。