Subscribed unsubscribe Subscribe Subscribe

BP Study #17での発表のために作成したサンプルコードとその解説

C# LINQ

追記: 一部意味不明な箇所を修正

公開する機会がないなあと思っていたので。
※スライド自体はここにあります。

1. パイプラインパターン

以前にもこれは言及したことがあるけど、GoF的なパターンとして確立しているというよりかは、定石だということでパターンと呼ばれているもの。あえてGoFのパターンに分類すると、Decoratorパターンと言える。より詳細な解説はこちらを参照。

一応Wikipediaの英語版には「Object pipelines」と解説があるものの、Microsoft製品を引き合いに出していることからも分かるように、これはMSDN系開発者コミュニティでは通じるけど他のコミュニティではあまり馴染みのない用語ということに留意 (まあ意味は分かるけど)。

コード:

using System;
using System.Collections.Generic;

// 掛け算を行うフィルタ
public class Multiplier: IEnumerable<int>
{
    public Multiplier(IEnumerable<int> src, int mul)
    {
        this.src = src;
        this.mul = mul;
    }

    public IEnumerator<int> GetEnumerator()
    {
        foreach (var i in this.src)
        {
            yield return i * this.mul;
        }
    }

    System.Collections.IEnumerator
    System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    IEnumerable<int> src;
    int mul;
}

// 足し算を行うフィルタ
public class Adder: IEnumerable<int>
{
    public Adder(IEnumerable<int> src, int add)
    {
        this.src = src;
        this.add = add;
    }

    public IEnumerator<int> GetEnumerator()
    {
        foreach (var i in this.src)
        {
            yield return i + this.add;
        }
    }

    System.Collections.IEnumerator
    System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    IEnumerable<int> src;
    int add;
}

// メイン
public static class PipelineTest
{
    public static void Main(string[] args)
    {
        IEnumerable<int> data = new int[] { 1, 2, 3 };
        IEnumerable<int> result = new Multiplier(
            new Adder(data, 2),
            3);
        // 1, 2, 3 という数字にそれぞれ 2 を足し 3 を掛けたものを
        // 列挙する
        foreach (var i in result)
        {
            Console.WriteLine(i);
        }
    }
}

イメージ図:
f:id:moriyoshi:20090203130600p:image

2. 拡張メソッド

与えられた単語の複数形を返す関数 (railsなどではおなじみですね!) Pluralize() を実装して、Stringオブジェクトの拡張メソッドとしてみた例。

using System;

public static class Pluralizer
{
    // この "this" がポイント
    public static string Pluralize(this string src)
    {
        switch (src)
        {
        case "tooth":
            return "teeth";

        case "child":
            return "children";
        // TODO: list more irregular inflections here
        }

        return src + "s";
    }
}

public static class ExtensionMethodTest1
{
    public static void Main(string[] args)
    {
        Console.WriteLine("tooth".Pluralize());
        Console.WriteLine("child".Pluralize());
        Console.WriteLine("dog".Pluralize());
    }
}

このようにもともとStringオブジェクトにはPluralizeなどというメソッドはないわけだけど、拡張メソッドを利用することで、別のスタティックなクラスのメソッドとして実装されたものが、あたかもStringオブジェクトのメソッドのように扱うことができるようになる。

拡張メソッドを使うには、メソッドを定義しているスタティッククラスをusingディレクティブで現在のコンパイル単位 (つまり1ファイル) で、名前空間の指定の不要な、unqualifiedな形で使える状態にする必要がある。当たり前だけど、スクリプト言語の同様の機能で一度メソッドをクラスに動的に追加したら、実行中はどこからでも*1そのメソッドを利用できるのとは違い、拡張メソッドが適用される範囲はコンパイル単位に限定される。

拡張メソッドは、実装を持てないはずのインターフェイスにも適用できるところがおもしろい。文法的にはインスタンスメソッド呼び出しだけど、実際にはスタティックメソッド呼び出しなので、heyがnullでもNullReferenceExceptionにすらならない。

using System;

public interface IHey
{
}

public static class ExtendingInterface
{
    public static string Hey(this IHey src)
    {
        return "hey";
    }
}

public static class ExtensionMethodTest2
{
    public static void Main(string[] args)
    {
        IHey hey = null;
        Console.WriteLine(hey.Hey());
    }
}

ちなみに、あるオブジェクトに対してLINQを行うには、あるオブジェクトに対して特定のメソッド呼び出し (Select(), Join()等) が行えることが要件となっている。たとえば、LINQ to Objectsなどは、拡張メソッドを実装するクラス System.Linq.Enumerable により実現されている。これを確かめるために次のようないじわるなテストを行ってみる。ちょっとサンプルにしてはソースの数が多いけど勘弁。

ExtensionMethodTest3.cs:

public static partial class ExtensionMethodTest3
{
    public static void Main(string[] args)
    {
        var data = new int[] {1, 2, 3};
        Test1(data);
        Test2(data);
    }
}

ExtensionMethodTest3.Test1.cs:

using System;
using System.Collections.Generic;
using System.Linq;

public static partial class ExtensionMethodTest3
{
    public static void Test1(IEnumerable<int> data)
    {
        foreach (var i in ((from i in data select i + 4)))
        {
            Console.WriteLine(i);
        }
    }
}

ExtensionMethodTest3.Test2.cs:

using System;
using System.Collections.Generic;
using MyExtensions;

public static partial class ExtensionMethodTest3
{
    public static void Test2(IEnumerable<int> data)
    {
        foreach (var i in ((from i in data select i + 4)))
        {
            Console.WriteLine(i);
        }
    }
}

MySelect.cs:

using System;
using System.Collections.Generic;
using System.Linq;

namespace MyExtensions
{
    public static class MySelect
    {
        class MyEnumerable<TS, TR>: IEnumerable<TR>
        {
            public MyEnumerable(IEnumerable<TS> src)
            {
                this.src = src;
            }

            public IEnumerator<TR> GetEnumerator()
            {
                foreach (var i in this.src)
                {
                    yield return (TR)(object)1;
                }
            }

            System.Collections.IEnumerator
            System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

            IEnumerable<TS> src;
        }

        public static IEnumerable<TR> Select<TS, TR>(this IEnumerable<TS> src,
                                                     Func<TS, TR> selector)
        {
            Console.WriteLine("-- {0} --", selector((TS)(object)1));
            return new MyEnumerable<TS, TR>(src);
        }
    }
}

これの実行結果は

5
6
7
-- 5 --
1
1
1

となり、Test2()ではLinq to Objectsではなく、名前空間MyExtensionsの方に実装された拡張メソッドが有効になって、全部の要素が「1」となってしまっていることが分かる。

3. ラムダ式

一概にマルチパラダイム言語の上のラムダというと「クロージャ + 無名関数」という話になってくるけども、これはC# 2.0の時代から、デリゲートを用いて、無名メソッドという文法によって記述することができた。ラムダ式が無名メソッドと違うのは、文脈に応じてデリゲートと翻訳されたり、式ツリーとして翻訳されたりするという点。ちょうどPerlスカラーコンテキストとリストコンテキストがあるのと似ている。

以下の例では、Test1()の引数としてラムダ式を与えた場合はデリゲートとしてコンパイルされ、Test2()の引数としてラムダ式を与えた場合は式ツリーとしてコンパイルされることを確認できる。

using System;
using System.Linq.Expressions;

public static class LambdaTest
{
    public static string Test1(Func<string, string> lambda)
    {
        return lambda("Mr. Laurence");
    }

    public static string Test2(Expression<Func<string, string>> lambda)
    {
        return lambda.ToString();
    }

    public static void Main(string[] args)
    {
        Console.WriteLine(Test1(who => "Hello, " + who));
        Console.WriteLine(Test2(who => "Good evening, " + who));

        // 式ツリーを手で構築するとこうなる
        var param = Expression.Parameter(typeof(string), "who");
        var expr = Expression.Lambda(
            Expression.Add(
                Expression.Constant("Good evening, "),
                param,
                typeof(string).GetMethod("Concat", 
                    new Type[] { typeof(string), typeof(string) })
            ),
            new ParameterExpression[] { param }
        ) as Expression<Func<string, string>>;
        Console.WriteLine(expr);
        Func<string, string> dg = expr.Compile();
        Console.WriteLine(dg("Mr. Laurence"));
    }
}

4. LINQ to SQL

テーブル間のアソシエーションを表すサンプル。























クラス名マップされるテーブル名プロパティ名マップされるカラム名
UseruserIdid
Namename
FirstNamefirst_name
LastNamelast_name
Orders外部キーによるアグリゲーション: user(1) - order(*)
ItemitemIdid
Namename
Priceprice
OrderorderIdid
UserIduser_id
User外部キーによるアグリゲーション: order(*) - user(1)
ItemIditem_id
Item外部キーによるアグリゲーション: order(*) - item(1)
Quantityquantity


サンプルをソースコード1つで完結させるため、サンプルデータの挿入などもコード中でやっちゃう。

using System;
using System.Linq;
using System.Data.SqlClient;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Collections.Generic;
using System.Diagnostics;

[Table(Name="user")]
public class User
{
    [Column(Name="id", IsPrimaryKey = true, IsDbGenerated = true)]
    public int Id { get; set; }

    [Column(Name="name")]
    public string Name { get; set; }

    [Column(Name="first_name")]
    public string FirstName { get; set; }

    [Column(Name="last_name")]
    public string LastName { get; set; }

    [Association(Name="orders", Storage="orders_", OtherKey="UserId")]
    public ICollection<Order> Orders
    {
        get { return orders_; }
        set { orders_.Assign( value ); }
    }

    private EntitySet<Order> orders_ = new EntitySet<Order>();
}

[Table(Name="item")]
public class Item
{
    [Column(Name="id", IsPrimaryKey = true, IsDbGenerated = true)]
    public int Id { get; set; }

    [Column(Name="name")]
    public string Name { get; set; }

    [Column(Name="price")]
    public decimal Price { get; set; }
}

[Table(Name="order")]
public class Order
{
    [Column(Name="id", IsPrimaryKey = true, IsDbGenerated = true)]
    public int Id { get; set; }

    [Column(Name="user_id")]
    public int UserId { get; set; }

    [Association(ThisKey="UserId", Name="orderToUser", Storage="userRef_",
                 IsForeignKey=true)]
    public User User
    {
        get { return userRef_.Entity; }
        set { userRef_.Entity = value; }
    }

    [Column(Name="item_id")]
    public int ItemId { get; set; }

    [Association(ThisKey="ItemId", Name="orderToItem", Storage="itemRef_",
                 IsForeignKey=true)]
    public Item Item
    {
        get { return itemRef_.Entity; }
        set { itemRef_.Entity = value; }
    }

    [Column(Name="quantity")]
    public int Quantity { get; set; }

    EntityRef<User> userRef_;
    EntityRef<Item> itemRef_;
}

public class LINQToSQLTest
{
    public static void Main(string[] args)
    {
        var conn = new SqlConnection("Data Source=.\\SQLEXPRESS;AttachDbFileName=|DataDirectory|\\test.mdf;User Instance=true;Integrated Security=True");
        using (conn)
        {
            conn.Open();
            DataContext dc = new DataContext(conn);
            SetUpFixture(dc);

            // 発行されるSQLを確認するには以下のコメントを外す
            // dc.Log = Console.Out;

            Table<Order> orderTable = dc.GetTable<Order>();
            Table<Item> itemTable = dc.GetTable<Item>();
            Table<User> userTable = dc.GetTable<User>();

            {
                IQueryable<User> query = from o in orderTable
                                         where o.Quantity > 2 select o.User;
                foreach (User u in query)
                {
                    Console.WriteLine(u.FirstName + " " + u.LastName);
                    foreach (Order o in u.Orders)
                    {
                        Console.WriteLine("Name: {0}, Quantity: {1}",
                            o.Item.Name, o.Quantity);
                    }
                }
            }

            {
                var o  = new Order {
                    User = new User() {
                        Name = "NewUser",
                        FirstName = "New",
                        LastName = "User"
                    },
                    Item = new Item() {
                        Name = "NewItem",
                        Price = 10000
                    },
                    Quantity = 8
                };

                orderTable.InsertOnSubmit(o);
                dc.SubmitChanges();
            }
        }
    }

    private static void SetUpFixture(DataContext dc)
    {
        dc.ExecuteCommand(@"
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='order') DROP TABLE [order];
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='user') DROP TABLE [user];
IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME='item') DROP TABLE [item];
");
        dc.ExecuteCommand(@"
CREATE TABLE [user] (
    id INTEGER IDENTITY(1,1) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    first_name VARCHAR(255) NOT NULL,
    last_name VARCHAR(255) NOT NULL
); 

CREATE TABLE [item] (
    id INTEGER IDENTITY(1,1) PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price DEC(28) NOT NULL DEFAULT 0
); 

CREATE TABLE [order] (
    id INTEGER IDENTITY(1,1) PRIMARY KEY,
    user_id  INTEGER NOT NULL DEFAULT 0
             FOREIGN KEY REFERENCES [user] (id) ON DELETE SET DEFAULT, 
    item_id  INTEGER NOT NULL DEFAULT 0
             FOREIGN KEY REFERENCES [item] (id) ON DELETE SET DEFAULT, 
    quantity INTEGER NOT NULL DEFAULT 0,
); 

INSERT INTO [user] (name, first_name, last_name)
VALUES ('diane2000',   'Diane',    'Hathaway');
INSERT INTO [user] (name, first_name, last_name)
VALUES ('rt',          'Robert',    'Terry');
INSERT INTO [user] (name, first_name, last_name)
VALUES ('ben',         'Benjamin', 'Kruger');
INSERT INTO [user] (name, first_name, last_name)
VALUES ('freshprince', 'Willard',  'Smith');
INSERT INTO [user] (name, first_name, last_name)
VALUES ('will',         'William',  'Morris');

INSERT INTO [item] (name) VALUES ('Item #1');
INSERT INTO [item] (name) VALUES ('Item #2');
INSERT INTO [item] (name) VALUES ('Item #3');
INSERT INTO [item] (name) VALUES ('Item #4');

INSERT INTO [order] (user_id, item_id, quantity)
VALUES (1, 1, 4);
INSERT INTO [order] (user_id, item_id, quantity)
VALUES (1, 2, 2);
INSERT INTO [order] (user_id, item_id, quantity)
VALUES (2, 1, 3);
INSERT INTO [order] (user_id, item_id, quantity)
VALUES (3, 4, 6);
INSERT INTO [order] (user_id, item_id, quantity)
VALUES (4, 3, 1);

");
    }
}

このサンプルを走らせるには、あらかじめSQL Server 2005 Express Editionを入れて、SQL Server Express Utility (sseutil.exe)を適当な場所に展開してパスを通し、

C:\Documents and Settings\Guest\Desktop\bpstudy17\LINQTest>sseutil -create test.mdf exercise

などとしてSQL Serverのユーザーインスタンスを利用するために.mdfファイルとログファイルを作成しておく。もちろんユーザーインスタンスのための設定が既に行われていることが前提。

5. LINQ to Entities

やっていることや出てくるテーブルの意味などは上のLINQ to SQLのサンプルと同一なんだけど、圧倒的に複雑になっていることが分かるんじゃないだろうか。エンタープライズ野郎の70%はXML、30%はOracleSQL Serverでできています。ともすると、CSDLファイルやSSDLファイルを手作業ベースで書いて解説している日本初の記事ではないかとすら思う…。普通は以下のようなファイルはVisualStudioでちゃっちゃと作成するものなのであまり気にすることはないはず。

exercise.csdl:

<?xml version="1.0" encoding="UTF-8" ?>
<Schema Namespace="ExerciseModel"
        Alias="Self"
        xmlns="http://schemas.microsoft.com/ado/2006/04/edm">
  <EntityContainer Name="ExerciseEntities">
    <!-- モデルに含まれるエンティティセットの宣言 -->
    <EntitySet Name="User" EntityType="ExerciseModel.User" />
    <EntitySet Name="Item" EntityType="ExerciseModel.Item" />
    <EntitySet Name="Order" EntityType="ExerciseModel.Order" />

    <!-- アソシエーションの宣言 -->
    <AssociationSet Name="FK_User_Order"
                    Association="ExerciseModel.FK_User_Order">
      <End Role="User" EntitySet="User" />
      <End Role="Order" EntitySet="Order" />
    </AssociationSet>
    <AssociationSet Name="FK_Item_Order"
                    Association="ExerciseModel.FK_Item_Order">
      <End Role="Item" EntitySet="Item" />
      <End Role="Order" EntitySet="Order" />
    </AssociationSet>
  </EntityContainer>

  <!-- エンティティの定義 -->

  <!-- User エンティティ -->
  <EntityType Name="User">
    <!-- 主 (プライマリ) キー -->
    <Key><PropertyRef Name="Id" /></Key>
    <!-- プロパティ定義 -->
    <Property Name="Id" Type="Int32" Nullable="false" />
    <Property Name="Name" Type="String" Nullable="false" MaxLength="255" 
              Unicode="true" FixedLength="false" />
    <Property Name="FirstName" Type="String" Nullable="false" MaxLength="255" 
              Unicode="true" FixedLength="false" />
    <Property Name="LastName" Type="String" Nullable="false" MaxLength="255" 
              Unicode="true" FixedLength="false" />
    <!-- ナビゲーションプロパティ定義 -->
    <NavigationProperty Name="Orders"
                        Relationship="ExerciseModel.FK_User_Order" 
                        FromRole="User" ToRole="Order" />
  </EntityType>

  <!-- Item エンティティ -->
  <EntityType Name="Item">
    <!-- 主 (プライマリ) キー -->
    <Key><PropertyRef Name="Id" /></Key>
    <!-- プロパティ定義 -->
    <Property Name="Id" Type="Int32" Nullable="false" />
    <Property Name="Name" Type="String" Nullable="false" MaxLength="255" 
              Unicode="true" FixedLength="false" />
    <Property Name="Price" Type="Decimal" Nullable="false" />
  </EntityType>

  <!-- Order エンティティ -->
  <EntityType Name="Order">
    <!-- 主 (プライマリ) キー -->
    <Key><PropertyRef Name="Id" /></Key>
    <!-- プロパティ定義 -->
    <Property Name="Id" Type="Int32" Nullable="false" />
    <Property Name="Quantity" Type="Int32" Nullable="false" />
    <!-- ナビゲーションプロパティ定義 -->
    <NavigationProperty Name="User"
                        Relationship="ExerciseModel.FK_User_Order" 
                        FromRole="Order" ToRole="User" />
    <NavigationProperty Name="Item"
                        Relationship="ExerciseModel.FK_Item_Order" 
                        FromRole="Order" ToRole="Item" />
  </EntityType>

  <!-- アソシエーション定義 -->
  <Association Name="FK_User_Order">
    <End Role="User" Type="ExerciseModel.User" Multiplicity="1" />
    <End Role="Order" Type="ExerciseModel.Order" Multiplicity="*" />
  </Association>
  <Association Name="FK_Item_Order">
    <End Role="Item" Type="ExerciseModel.Item" Multiplicity="1" />
    <End Role="Order" Type="ExerciseModel.Order" Multiplicity="*" />
  </Association>

</Schema>

exercise.msl:

<?xml version="1.0" encoding="UTF-8" ?>
<Mapping Space="C-S"
         xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS">
  <EntityContainerMapping StorageEntityContainer="dbo"
                          CdmEntityContainer="ExerciseEntities">
    <EntitySetMapping Name="User">
      <EntityTypeMapping TypeName="IsTypeOf(ExerciseModel.User)">
        <MappingFragment StoreEntitySet="user">
          <ScalarProperty Name="Id" ColumnName="id" />
          <ScalarProperty Name="Name" ColumnName="name" />
          <ScalarProperty Name="FirstName" ColumnName="first_name" />
          <ScalarProperty Name="LastName" ColumnName="last_name" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>

    <EntitySetMapping Name="Item">
      <EntityTypeMapping TypeName="IsTypeOf(ExerciseModel.Item)">
        <MappingFragment StoreEntitySet="item">
          <ScalarProperty Name="Id" ColumnName="id" />
          <ScalarProperty Name="Name" ColumnName="name" />
          <ScalarProperty Name="Price" ColumnName="price" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>

    <EntitySetMapping Name="Order">
      <EntityTypeMapping TypeName="IsTypeOf(ExerciseModel.Order)">
        <MappingFragment StoreEntitySet="order">
          <ScalarProperty Name="Id" ColumnName="id" />
          <ScalarProperty Name="Quantity" ColumnName="quantity" />
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>

    <AssociationSetMapping Name="FK_User_Order"
                           TypeName="ExerciseModel.FK_User_Order" 
                           StoreEntitySet="order">
      <EndProperty Name="User">
        <ScalarProperty Name="Id" ColumnName="user_id" />
      </EndProperty>
      <EndProperty Name="Order">
        <ScalarProperty Name="Id" ColumnName="id" />
      </EndProperty>
    </AssociationSetMapping>

    <AssociationSetMapping Name="FK_Item_Order"
                           TypeName="ExerciseModel.FK_Item_Order" 
                           StoreEntitySet="order">
      <EndProperty Name="Item">
        <ScalarProperty Name="Id" ColumnName="item_id" />
      </EndProperty>
      <EndProperty Name="Order">
        <ScalarProperty Name="Id" ColumnName="id" />
      </EndProperty>
    </AssociationSetMapping>
  </EntityContainerMapping>
</Mapping>

exercise.ssdl:

<?xml version="1.0" encoding="UTF-8" ?>
<Schema Namespace="ExerciseModel.Store"
        Alias="Self" Provider="System.Data.SqlClient" 
        ProviderManifestToken="2005" 
        xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" 
        xmlns="http://schemas.microsoft.com/ado/2006/04/edm/ssdl">
  <EntityContainer Name="dbo">
    <!-- ストアモデルに含まれるエンティティセットの宣言 -->
    <EntitySet Name="user" EntityType="ExerciseModel.Store.user" store:Type="Tables" />
    <EntitySet Name="item" EntityType="ExerciseModel.Store.item" store:Type="Tables" />
    <EntitySet Name="order" EntityType="ExerciseModel.Store.order" store:Type="Tables" />

    <!-- アソシエーションの宣言 -->
    <AssociationSet Name="FK_User_Order" Association="ExerciseModel.Store.FK_User_Order">
      <End Role="User" EntitySet="user" />
      <End Role="Order" EntitySet="order" />
    </AssociationSet>

    <AssociationSet Name="FK_Item_Order" Association="ExerciseModel.Store.FK_Item_Order">
      <End Role="Item" EntitySet="item" />
      <End Role="Order" EntitySet="order" />
    </AssociationSet>
  </EntityContainer>

  <!-- user テーブル -->
  <EntityType Name="user">
    <Key><PropertyRef Name="id" /></Key>
    <Property Name="id" Type="int" Nullable="false" />
    <Property Name="name" Type="varchar" Nullable="false"
              MaxLength="255" />
    <Property Name="first_name" Type="varchar" Nullable="false"
              MaxLength="255" />
    <Property Name="last_name" Type="varchar" Nullable="false"
              MaxLength="255" />
  </EntityType>

  <!-- item テーブル -->
  <EntityType Name="item">
    <Key><PropertyRef Name="id" /></Key>
    <Property Name="id" Type="int" Nullable="false" />
    <Property Name="name" Type="varchar" Nullable="false" MaxLength="255" />
    <Property Name="price" Type="decimal" Nullable="false" />
  </EntityType>

  <!-- order テーブル -->
  <EntityType Name="order">
    <Key><PropertyRef Name="id" /></Key>
    <Property Name="id" Type="int" Nullable="false" />
    <Property Name="user_id" Type="int" Nullable="false" />
    <Property Name="item_id" Type="int" Nullable="false" />
    <Property Name="quantity" Type="int" Nullable="false" />
  </EntityType>

  <!-- アソシエーション定義 -->
  <Association Name="FK_User_Order">
    <End Role="User" Type="ExerciseModel.Store.user" Multiplicity="1" />
    <End Role="Order" Type="ExerciseModel.Store.order" Multiplicity="*" />

    <!-- 外部キー制約 -->
    <ReferentialConstraint>
      <Principal Role="User">
        <PropertyRef Name="id" />
      </Principal>
      <Dependent Role="Order">
        <PropertyRef Name="user_id" />
      </Dependent>
    </ReferentialConstraint>
  </Association>

  <Association Name="FK_Item_Order">
    <End Role="Item" Type="ExerciseModel.Store.item" Multiplicity="1" />
    <End Role="Order" Type="ExerciseModel.Store.order" Multiplicity="*" />

    <!-- 外部キー制約 -->
    <ReferentialConstraint>
      <Principal Role="Item">
        <PropertyRef Name="id" />
      </Principal>
      <Dependent Role="Order">
        <PropertyRef Name="item_id" />
      </Dependent>
    </ReferentialConstraint>
  </Association>
</Schema>

さて、edmgenというツールを使ってCSDLファイルからエンティティデータモデル (EDM) とやらを生成することからLINQ to Entitiesへの (徒歩による) 旅は始まる。細かいことは「Entity Framework プロジェクトを手動で構成する方法を」見てもらうとして、ここでエンティティデータモデルとか言ってるのは要はドメインモデルを構成するエンティティクラス群のこと。クラスをXMLで書いたスキーマから自動生成する。って、これはApache Torqueなんかと似てる。

C:\Documents and Settings\Guest\Desktop\bpstudy17\LINQTest>edmgen.exe /mode:EntityClassGeneration /language:CSharp /p:Exercise /incsdl:exercise.csdl

できるファイルはこんなやつ。

Exercise.ObjectLayer.cs

//------------------------------------------------------------------------------
// <auto-generated>
//     このコードはツールによって生成されました。
//     ランタイム バージョン:2.0.50727.3053
//
//     このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
//     コードが再生成されるときに損失したりします。
// </auto-generated>
//------------------------------------------------------------------------------

[assembly: global::System.Data.Objects.DataClasses.EdmSchemaAttribute()]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("ExerciseModel", "FK_User_Order", "User", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(ExerciseModel.User), "Order", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(ExerciseModel.Order))]
[assembly: global::System.Data.Objects.DataClasses.EdmRelationshipAttribute("ExerciseModel", "FK_Item_Order", "Item", global::System.Data.Metadata.Edm.RelationshipMultiplicity.One, typeof(ExerciseModel.Item), "Order", global::System.Data.Metadata.Edm.RelationshipMultiplicity.Many, typeof(ExerciseModel.Order))]

// 元のファイル名: Exercise.ObjectLayer.cs
// 生成日: 2009/02/01 13:02:24
namespace ExerciseModel
{
    
    /// <summary>
    /// スキーマの ExerciseEntities にはコメントがありません。
    /// </summary>
    public partial class ExerciseEntities : global::System.Data.Objects.ObjectContext
    {
        /// <summary>
        /// アプリケーション構成ファイルの 'ExerciseEntities' セクションにある接続文字列を使用して新しい ExerciseEntities オブジェクトを初期化します。
        /// </summary>
        public ExerciseEntities() : 
                base("name=ExerciseEntities", "ExerciseEntities")
        {
            this.OnContextCreated();
        }
...

で、これを利用するソースコードは以下。

using System;
using System.Linq;
using System.Data.Linq;
using System.Data.Objects;
using System.Collections.Generic;
using System.Diagnostics;
using ExerciseModel;

public class LINQToEntityTest001
{
    public static void Main(string[] args)
    {
        var ctx = new ExerciseEntities();
        using (ctx)
        {
            ObjectQuery<User> q = (from u in ctx.User select u) as ObjectQuery<User>;
            // 生成される Entity SQL を表示するには下のコメントを外す
            // Console.WriteLine(q.ToTraceString());

            PrintUsers(q);
            PrintUsers((from o in ctx.Order where o.Quantity > 2 select o.User) as ObjectQuery<User>);
        }
    }

    static void PrintUser(User u)
    {
        Console.WriteLine("User: " + u.Name + " [" + u.Id + "]");
        Console.WriteLine("    First name: " + u.FirstName);
        Console.WriteLine("    Last name: " + u.LastName);
    }

    static void PrintUsers(IEnumerable<User> users)
    {
        foreach (User u in users) {
            PrintUser(u);
        }
        Console.WriteLine("--- End of list --");
    }

}

6. LINQ to XXXを作る

LINQを適用できるクラスを作るには、基本的には

  • LINQ用の拡張メソッドを頑張って実装する
  • IQueryable<...> と IQueryProvider を実装する

という方法があるものの、前者はいろんな意味でNGなので後者の方法でちょっとやってみようという話。

イメージとしては、IQueryableなオブジェクト (甲) と、それを生成するファクトリ (乙) であるIQueryProviderというやつがいて、甲にさらに問い合わせを行う場合は、まず甲を表すExpressionオブジェクトを取り出して、そいつをベースとした式ツリーをつくり、次に甲から甲を作ったファクトリである*2乙のインスタンスを取り出して、それのCreateQuery()を、引数として今作った式ツリーを指定して呼び出す。という感じ。

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

static internal class ExpressionDumper
{
    public static void Dump(Expression expr)
    {
        Console.WriteLine(expr);
        Dump(null, expr, 0);
    }

    private static void Indent(int count)
    {
        while (--count >= 0)
            Console.Write("  ");
    }

    private static void Dump(IConvertible name, object obj, int indentDepth)
    {
        Indent(indentDepth);
        if (name != null)
        {
            Console.Write("[" + name + "] ");
            
        }
        if (obj == null)
        {
            Console.WriteLine("(null)");
            return;
        }
        if (obj is Expression)
        {
            Expression expr = obj as Expression;
            Console.WriteLine(expr.NodeType + ": " + expr.Type);
            Indent(indentDepth); Console.WriteLine("{");
            Type exprType = expr.GetType();
            IEnumerable<PropertyInfo> props = exprType.GetProperties();
            foreach (var prop in props)
            {
                if (!prop.CanRead || prop.GetIndexParameters().Count() != 0)
                    continue;

                switch (prop.Name)
                {
                case "Type":
                    continue;

                case "Method":
                    continue;

                case "NodeType":
                    continue;
                }

                object val = prop.GetValue(expr, null);

                if (typeof(Expression).IsAssignableFrom(prop.PropertyType))
                {
                    Dump(prop.Name, val as Expression, indentDepth + 1);
                }
                else
                {
                    Dump(prop.Name, val, indentDepth + 1);
                }
            }
            Indent(indentDepth); Console.WriteLine("}");
        }
        else if (obj is System.Collections.IEnumerable &&
                 !(obj is IQueryable) && !(obj is string))
        {
            Console.WriteLine(obj.GetType().Name);
            Indent(indentDepth); Console.WriteLine("{");
            try
            {
                int idx = 0;
                foreach (var i in obj as System.Collections.IEnumerable)
                {
                    Dump(idx, i, indentDepth + 1);
                    ++idx;
                }
            }
            catch (Exception)
            {
                Console.WriteLine("(null Enumerator)");
            }
            Indent(indentDepth); Console.WriteLine("}");
        }
        else
        {
            Console.WriteLine(obj);
        }
    }
}

internal class Provider: IQueryProvider
{
    public IQueryable CreateQuery(Expression expr)
    {
        ExpressionDumper.Dump(expr);
        return new QueryableThing(this, expr);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expr)
    {
        ExpressionDumper.Dump(expr);
        return (IQueryable<TElement>)new QueryableThing(this, expr);
    }

    public object Execute(Expression expr)
    {
        return null;
    }

    public TResult Execute<TResult>(Expression expr)
    {
        return default(TResult);
    }
}

public class QueryableThing: IQueryable<string>
{
    static Provider defaultProvider;

    public Type ElementType
    {
        get { return typeof(string); }
    }

    public Expression Expression
    {
        get { return expr; } 
    }

    public IQueryProvider Provider
    {
        get { return provider; }
    }

    System.Collections.IEnumerator
    System.Collections.IEnumerable.GetEnumerator()
    {
        return Provider.Execute<System.Collections.IEnumerable>(expr).GetEnumerator();
    }

    public IEnumerator<string> GetEnumerator()
    {
        return Provider.Execute<IEnumerable<string>>(expr).GetEnumerator();
    }

    public QueryableThing()
    {
        this.expr = Expression.Constant(this);
        this.provider = defaultProvider; 
    }

    public QueryableThing(IQueryProvider provider, Expression expr)
    {
        this.provider = provider;
        this.expr = expr;
    }

    static QueryableThing()
    {
        defaultProvider = new Provider();
    }

    private IQueryProvider provider;
    private Expression expr;
}

public static class LINQTest011
{
    public static void Main(string[] args)
    {
        var q = new QueryableThing();

        { var t = from s in q select s; }
        { var t = from s in q where s == "abc" select s; }
        { var t = from s in q where s.StartsWith("ab") select s; }
    }
}

*1:そのクラスが適用される文脈では

*2:必ずしもそうでなくてもいいけど