読者です 読者をやめる 読者になる 読者になる

しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

Entity Framework の俺的まとめ

個人的に超ブームな Entity Framework について、日本語情報が少なすぎるので私が知りうる限りの情報をまとめてみました。勘違いしている部分などあると思うので、コメントで突っ込んでもらえるとありがたいです。

Entity Framework とは

.NET Framework 3.5 SP1 で追加された新しいデータアクセステクノロジです。正式には ADO.NET Entity Framework と言います。

早い話が O/R マッパーなのですが、.NET 3.5 で追加された LINQ to SQL よりも洗練されたフレームワークとなっています。特徴としては LINQ to SQL が式木(Expression Trees)から Transact-SQL を生成していたのに対して、Entity Framework では Entity SQL と呼ばれる SQL ライクなクエリ言語に変換されてから実行されます。

Entity SQL はデータベースエンジンに依存しない形で定義されているので、SQL Server しか使えなかった LINQ to SQL とは異なり、データプロバイダを変更するだけで様々なデータベースエンジンに対応できます。*1

機能も LINQ to SQL より強化されていて、EDM (Entity Data Model) ファイルを作成して複雑な概念モデルを作成・マッピングして利用することが出来ます。ちなみに EDM ファイルにはエンティティ・アソシエーションなどの情報が含まれているので、最後に説明するコード・ファースト開発以外では必要になります。

今回は Entity Framework を使用していて気になった以下の内容についてまとめます。

  • 遅延読み込み
  • POCO エンティティ
  • (モデル・ファースト)
  • コード・ファースト

遅延読み込み

LINQ to SQL では普通に使えていたのですが、Entity Framework 4 以前では関連付けられたオブジェクトの自動的な読み込みがサポートされていませんでした。

4 以前ではデフォルトの動作が明示的な読み込みですので、必要なタイミングで EntityReference.Load メソッド*2を呼び出して関連オブジェクトを読み込む必要がありました。

// MyEntities オブジェクトの生成
using (var entities = new MyEntities)
{
    // LINQ を使用して値段が 1000 以上の項目を取得する
    var result = from p in entities.Products
                 where p.Price > 1000
                 select p;

    foreach (var item in result)
    {
        // Customer プロパティを明示的に読み込む。**Reference プロパティは自動的に生成される
        item.CustomerReference.Load();

        Console.WriteLine(item.Customer.Name);
    }
}

一目瞭然だと思いますが、この方法では foreach でループが実行される回数だけ Customer プロパティを読み込むためにクエリが実行されてしまいます。明らかに毎回実行するのは無駄なので、パフォーマンスが悪化する原因になります。

このようにループ内で予め使用することがわかっている場合は、最初のクエリ実行時に関連するオブジェクトを Include メソッドで指定することで読み込んでおくことができます。

// MyEntities オブジェクトの生成
using (var entities = new MyEntities)
{
    // LINQ を使用して値段が 1000 以上の項目を取得する
    // 同時に Include メソッドで読み込むプロパティを指定
    var result = from p in entities.Products.Include("Customer")
                 where p.Price > 1000
                 select p;

    foreach (var item in result)
    {
        // この時点で Customer プロパティは既に読み込まれている
        Console.WriteLine(item.Customer.Name);
    }
}

この 2 つのサンプルコードでは明らかに後者の方が効率が良いことが理解してもらえると思います。

ここまで Entity Framework 4 以前について長々と書いてきましたが、初期リリースというのもあってか、いろいろ間に合わなかった部分が多かったようです。しかし、Entity Framework 4 では遅延読み込みがサポートされているので、複雑に考えることなく関連オブジェクトの利用が出来るようになっています。

POCO エンティティ

今までのように自動生成されたエンティティには、変更追跡や遅延実行のために様々なインタフェースやメソッドが実装されていて、依存関係も多くて非常に複雑なものでした。それを解決するのが今回説明する POCO エンティティです。

POCO エンティティはその名の通り Plain Old CLR Object で作成されたエンティティのことで、今までのように自動生成されたエンティティと比べると、依存性の少ない非常にクリーンなコードになりテストも容易になります。しかもちゃんと変更追跡と遅延読み込みも決まり事を守るだけで利用することが出来るので安心してください。

変更追跡と遅延読み込みといった機能は、POCO エンティティに対して Entity Framework が自動的に変更追跡プロキシと遅延読み込みプロキシを作成することで今まで通りの使い勝手を実現しているのですが、何でも利用できるというわけではなくて利用できるクラスに対しての要件が存在します。

基本的には下記の項目を守っておけば問題ありません。動的にプロキシクラスを作成していると考えれば理由がわかると思います。

  • sealed でも abstract でも無い public なクラス
  • 引数を取らない public、protected なコンストラクタを持つクラス
  • (遅延読み込みプロキシを利用する場合)ナビゲーションプロパティは sealed ではない public virtual なプロパティ
  • (変更追跡プロキシを利用する場合)マップされるプロパティは sealed ではない、get と set を持つ public virtual なプロパティ

では、この要件に合う Product クラスを作成してみたいと思います。

// 自動プロパティを持つ public なクラス
public class Product
{
    public virtual int ProductId { get; set; }
    public virtual string Name { get; set; }
    public virtual int Price { get; set; }
    public virtual Customer Customer { get; set; }
}

一般的には C# 3.0 で追加された自動プロパティを利用して作成することが多いです。データの検証は DataAnnotations を使えば大丈夫です。

こうして作成した POCO エンティティは ObjectContext を経由して利用します。ObjectContext は LINQ to SQL の DataContext のようなもので、このクラスを経由してデータベースへのマッピングを行います。

// ObjectContext を継承する必要がある
public ProductContext : ObjectContext
{
    public ProductContext()
        : base("name=ProductEntities", "ProductEntities")
    {
        // 型毎に ObjectSet を作成する
        _products = CreateObjectSet<Product>();
        _customers = CreateObjectSet<Customer>();
    }

    private ObjectSet<Product> _products;
    private ObjectSet<Customer> _customers;

    public ObjectSet<Product> Products
    {
        get { return _products; }
    }

    public ObjectSet<Customer> Customers
    {
        get { return _customers; }
    }
}

コンストラクタでテーブルに相当する ObjectSet をエンティティ毎に作成します。そして ObjectSet に対して LINQ を使用してクエリを実行することが出来ます。

POCO でコードは非常にクリーンになりますが、自分で作成する必要がある部分が多いです。必要であれば Visual Studio ギャラリーにその手間を省くためのアドオンとして EF 4.x POCO Entity Generator for C# extension が公開されているので、これを利用するのもいいと思います。

モデル・ファースト

基本的には空の EDM ファイルを作成して、Visual Studio のデザイナを利用してエンティティ・アソシエーションの定義を行います。データベースは不要で、作成した概念モデルからテーブルなどを丸ごと作成することが出来ます。

Visual Studio のデザイナの操作に慣れている方でしたら簡単に概念モデルを作成することが出来ると思います。デザイナと EDM の話になってしまうので今回は省略します。

コード・ファースト

Entity Framework CTP4 ではコードのみでエンティティ・アソシエーションを定義する方法が提供されています。EDM が不要でインタフェースも用意されているので、モックを作成してのテストも非常に簡単です。

追加された中でも主要なクラスは以下になります。Database クラスは System.Data.Entity.Infrastructure に、それ以外は System.Data.Entity 名前空間に存在しています。

  • Database クラス
  • DbContext クラス
  • DbSet クラス
  • IDbSet インタフェース

基本的には命名規約を守るだけで自動的にリレーションシップの解決まで行ってくれます。とりあえずエンティティを作成してみましょう。

// 自動プロパティを持つ public なクラス
public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
    public int CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
}

見た目は POCO エンティティと殆ど変化がありません。しかし ProductId プロパティは自動的にプライマリキーとして認識されますし、CustomerId プロパティは外部キーとして認識されます。もちろん遅延読み込みプロキシを作成してくれますので、Customer プロパティも意図した通りに動作します。

これでエンティティは完成しましたので、次はデータベースとの処理を行うためのコンテキストを作成します。使用するクラスは DbContext と DbSet クラスです。

// 実際にデータベースと処理を行うコンテキストクラス
// DbContext を継承する必要がある
public class ProductContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Customer> Customers { get; set; }
}

はい、これで完成です。ObjectContext を使ったときに比べて非常にシンプルですが、実際にちゃんと動作します。接続文字列など何も指定していませんが、DbContext はデフォルトでコンテキストクラス名と同じ名前の接続文字列を Web.config から探してくれます。

この後は今まで通り LINQ で DbSet に対して処理を行うだけです。コードが素晴らしく綺麗ですね。命名規約を使うところが ASP.NET MVC と同じですし、テストも行いやすいので一緒に使いたくなりますね。

あと、DbSet クラスですが IDbSet インタフェースを実装しているので、簡単にコンテキストクラスのモックを作成することも可能です。

// インタフェースを定義
public interface IProductContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Customer> Customers { get; set; }
}

// 実際のコンテキスト
public class ProductContext : DbContext, IProductContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Customer> Customers { get; set; }
}

// テスト用のモックコンテキスト
public class MockProductContext : IProductContext
{
    public MockProductContext()
    {
        Products = new MockProductDbSet();
        Customers = new MockCustomerDbSet();
    }

    public IDbSet<Product> Products { get; set; }
    public IDbSet<Customer> Customers { get; set; }
}

public class MockProductDbSet : IDbSet<Product>
{
    // 実装は省略...
}

public class MockCustomerDbSet : IDbSet<Customer>
{
    // 実装は省略...
}

モックの作成にはいろいろな方法があると思いますので、一例として捉えてくださいね。

以上で、Entity Framework の俺的まとめを終わります。コード・ファーストに関してはまた別エントリで作成する予定です。

参考

ADO.NET team blog
http://blogs.msdn.com/b/adonet/
Entity Framework Design
http://blogs.msdn.com/b/efdesign/

*1:実質には SQL Server 用のデータプロバイダぐらいしかないようですが…。

*2:Entity Framework 4 での POCO エンティティでは LoadProperty なども使えるようになりました。