次の方法で共有


Entity Framework

ADO.NET Entity Framework 4.1 における Code First

Rowan Miller

4 月にリリースされた ADO.NET Entity Framework 4.1 には、Microsoft .NET Framework 4 および Visual Studio 2010 でリリースされた既存の Entity Framework 4 の機能を基盤として構築された一連の新機能が付属しています。

Entity Framework 4.1 は、スタンドアロン インストーラー (msdn.microsoft.com/data/ee712906、英語) としても、"EntityFramework" NuGet パッケージとしても入手可能です。また、ASP.NET MVC 3.01 のインストールにも含まれています。

Entity Framework 4.1 には、DbContext API と Code First という 2 つの主要新機能があります。今回の記事では、この 2 つの機能を使用してアプリケーションを開発する方法について解説します。Code First の概要について簡単に説明してから、高度な機能をいくつか掘り下げます。

DbContext API は、既存の ObjectContext 型と、Entity Framework の以前のリリースに含まれていた数多くのその他の型を簡素化して抽象化したものです。DbContext API のサーフェイスは、共通のタスクと共通のコーディング パターン向けに最適化されています。共通機能はルート レベルで公開され、より高度な機能は API をドリル ダウンすると使用可能になります。

Code First は、既存の Database First パターンや Model First パターンの代わりとなる、Entity Framework の新しい開発パターンです。Code First により、CLR クラスを使用してモデルを定義できるようになります。その後、これらの CLR クラスを既存のデータベースにマップしたり、CLR クラスを使用してデータベース スキーマを生成したりすることができます。追加の構成は、データ注釈を使用して指定することも、Fluent API を通じて指定することも可能です。

概要

Code First が登場してからしばらく経っているので、ここでは詳しく説明しません。基本事項については、bit.ly/evXlOc で、「EF 4.1 Code First Walkthrough」(Entity Framework 4.1 の Code First に関するチュートリアル、英語) を参照してください。Code First アプリケーションを動かすことができる完全なコード リストを図 1 に示します。

図 1 Code First への着手

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System;

namespace Blogging
{
  class Program
  {
    static void Main(string[] args)
    {
      Database.SetInitializer<BlogContext>(new BlogInitializer());

      // TODO: Make this program do something!
    }
  }

  public class BlogContext : DbContext
  {
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
      // TODO: Perform any fluent API configuration here!
    }
  }

  public class Blog
  {
    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Abstract { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
  }

  public class RssEnabledBlog : Blog
  {
    public string RssFeed { get; set; }
  }

  public class Post
  {
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public byte[] Photo { get; set; }

    public virtual Blog Blog { get; set; }
  }

  public class BlogInitializer : DropCreateDatabaseIfModelChanges<BlogContext>
  {
    protected override void Seed(BlogContext context)
    {
      context.Blogs.Add(new RssEnabledBlog
      {
        Name = "blogs.msdn.com/data",
        RssFeed = "https://blogs.msdn.com/b/data/rss.aspx",
        Posts = new List<Post>
        {
          new Post { Title = "Introducing EF4.1" },
          new Post { Title = "Code First with EF4.1" },
        }
      });

      context.Blogs.Add(new Blog { Name = "romiller.com" });
      context.SaveChanges();
    }
  }
}

説明をわかりやすくするため、ここでは、Code First を使ってデータベースを生成することにしました。データベースは、データを保持および照会するために、最初に BlogContext を使用するときに作成しています。この記事の後の部分では、Code First を既存のデータベース スキーマにマップするケースについて説明します。今回の記事では、モデルを変更するたびにデータベースの削除と再作成を行うために、データベースの初期化子を使用しています。

Fluent API によるマッピング

Code First では、モデルの形状を推測するために CLR クラスを調べることから始めます。一連の規則は、主キーなどを検出するために使用します。データ注釈や Fluent API を使用して、規則によって検出されたものよりも優先させたり、追加したりすることができます。Fluent API を使用した共通タスクの実現について執筆された記事はたくさんあるため、ここでは、実行できるさらに高度な構成をいくつか見ていくことにします。特に、API の "マッピング" の分野に注目します。マッピング構成を使用すると、既存のデータベース スキーマにマップしたり、生成されたスキーマの形状に影響を及ぼしたりすることが可能になります。Fluent API は、DbModelBuilder 型によって公開され、DbContext の OnModelCreating メソッドをオーバーライドすることで最も簡単にアクセスできます。

エンティティ分割: エンティティ分割によって、エンティティ型のプロパティを複数のテーブルに分散させることができます。たとえば、投稿する写真データを別のテーブルに分割して、異なるファイル グループに格納できるようにするとします。エンティティ分割は、複数の Map 呼び出しを使用して、プロパティのサブセットを特定のテーブルにマップします。図 2 では、Photo プロパティを "PostPhotos" テーブルに、その他のプロパティを "Posts" テーブルにマップしています。プロパティのリストには主キーを含めませんでした。主キーは、常に各テーブルに必要です。自分で含めることもできましたが、実は Code First が自動的に追加します。

図 2 エンティティ分割

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Post>()
    .Map(m =>
      {
        m.Properties(p => new { p.Title, p.Content });
        m.ToTable("Posts");
      })
    .Map(m =>
      {
        m.Properties(p => new { p.Photo });
        m.ToTable("PostPhotos");
      });
}

Table-per-Hierarchy (TPH) 継承: TPH は、1 つのテーブルに継承階層を格納して、各行の型を特定するために識別子の列を使用します。Code First では、構成をまったく指定しなければ、既定で TPH を使用します。識別子の列は適切に "Discriminator" と名付けられ、それぞれの型の CLR 型名が識別子の値に使用されます。

しかし、TPH マッピングの実行方法をカスタマイズすることを考える場合もあるでしょう。これには、Map メソッドを使用して基本型の識別子の列の値を構成し、Map<TEntityType> を使用して各派生型を構成します。ここでは、次のように "HasRssFeed" 列を使用して、"Blog" と "RssEnabledBlog" インスタンスを区別するために true/false 値を格納しています。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m => m.Requires("HasRssFeed").HasValue(false))
    .Map<RssEnabledBlog>(m => m.Requires("HasRssFeed").HasValue(true));
}

この例でも、型を区別するのにスタンドアロンの列を使用していますが、実際には RSS フィードがあることから RssEnabledBlogs を特定できます。このため、Entity Framework が、型を区別するのに "Blog.RssFeed" を格納している列を使用する必要があることを認識できるよう、マッピングを記述し直すことができます。次のように、列が null 以外の値を含む場合は、RssEnabledBlog する必要があります。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map<RssEnabledBlog>(m => m.Requires(b => b.RssFeed).HasValue());
}

Table-per-Type (TPT) 継承: TPT は、基本型のすべてのプロパティを 1 つのテーブルに格納します。派生型の追加プロパティは別のテーブルに格納し、外部キーを指定して基本テーブルと関係付けます。TPT のマッピングでは、Map 呼び出しを使用して基本テーブル名を指定してから、Map<TEntityType> を使用して各派生型のテーブルを構成します。次の例では、すべてのブログに共通のデータを "Blogs" テーブルに格納し、RSS 対応のブログに固有のデータを "RssBlogs" テーブルに格納しています。

modelBuilder.Entity<Blog>()
  .Map(m => m.ToTable("Blogs"))
  .Map<RssEnabledBlog>(m => m.ToTable("RssBlogs"));

Table-per-Concrete Type (TPC) 継承: TPC は、型ごとにデータをまったく別のテーブルに格納します。各テーブル間に外部キー制約は指定しません。構成は、各派生型を構成するときに "MapInheritedProperties" 呼び出しを含めることを除けば、TPT マッピングに似ています。MapInheritedProperties は、Code First が、基本クラスから継承されたすべてのプロパティを派生クラスのテーブル内の新しい列にマップし直すことを認識できるようにします。

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m => m.ToTable("Blogs"))
    .Map<RssEnabledBlog>(m =>
      {
        m.MapInheritedProperties();
        m.ToTable("RssBlogs");
      });
}

原則として、Code First では、整数型の主キーに ID 列を使用します。ただし、TPC では、主キーの生成に使用できる、すべてのブログを含む単一のテーブルがありません。このため、Code First は、TPC マッピングを使用するときは ID を無効にします。複数のテーブル間で一意になる値を生成するよう設定された既存のデータベースにマッピングを行うと、Fluent API のプロパティの構成セクションを通じて ID を再度有効にすることができます。

ハイブリッド マッピング: 当然、スキーマの形状が、ここまで説明したパターンのいずれかに常に一致するとは限りません。特に、既存のデータベースにマッピングを行う場合は顕著です。さいわい、マッピング API は自身で構成でき、複数のマッピングを組み合わせることができます。図 3 に、エンティティ分割と TPT 継承マッピングを組み合わせている例を示します。ブログのデータは "Blogs" および "BlogAbstracts" に分割され、RSS 対応のブログに固有のデータは、別の "RssBlogs" テーブルに格納されます。

図 3 エンティティ分割と TPT 継承マッピングの組み合わせ

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m =>
      {
        m.Properties(b => new { b.Name });
        m.ToTable("Blogs");
      })
    .Map(m =>
      {
        m.Properties(b => new { b.Abstract });
        m.ToTable("BlogAbstracts");
      })
    .Map<RssEnabledBlog>(m =>
      {
         m.ToTable("RssBlogs");
      });
}

変更追跡 API

ここまでは、データベース マッピングの構成について見てきました。ここからは、データの操作について説明します。より高度なシナリオに進むため、基本的なデータ アクセスについて詳しくない方は、冒頭で紹介した「EF 4.1 Code First Walkthrough」(Entity Framework 4.1 の Code First に関するチュートリアル) をお読みください。

1 つのエンティティの状態情報: ログを記録する際など、多くの場合、エンティティの状態情報にアクセスできると便利です。たとえば、エンティティの状態や、変更されたプロパティについての情報などです。DbContext は、"Entry" メソッドを使用して、各エンティティに関するこのような情報にアクセスできるようにします。図 4 のコード スニペットは、データベースから 1 つの "Blog" を読み込み、プロパティを変更して、各プロパティの現在値と元の値をコンソールに出力します。

図 4 エンティティの状態情報の取得

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Change the name of one blog
    var blog = db.Blogs.First();
    blog.Name = "ADO.NET Team Blog";

    // Print out original and current value for each property
    var propertyNames = db.Entry(blog).CurrentValues.PropertyNames;
    foreach (var property in propertyNames)
    {
      System.Console.WriteLine(
        "{0}\n Original Value: {1}\n Current Value: {2}", 
        property, 
        db.Entry(blog).OriginalValues[property],
        db.Entry(blog).CurrentValues[property]);
    }
  }

  Console.ReadKey();
}

図 4 のコードを実行すると、コンソール出力は次のようになります。

BlogId
 Original Value: 1
 Current Value: 1
 
Name
 Original Value: blogs.msdn.com/data
 Current Value: ADO.NET Team Blog
 
Abstract
 Original Value:
 Current Value:
 
RssFeed
 Original Value: https://blogs.msdn.com/b/data/rss.aspx
 Current Value: https://blogs.msdn.com/b/data/rss.aspx

複数のエンティティの状態情報: DbContext は、"ChangeTracker.Entries" メソッドを使用してて、複数のエンティティについての情報にアクセスできるようにします。DbContext には、特定の型のエンティティを提供するジェネリック オーバーロードと、すべてのエンティティを提供する非ジェネリック オーバーロードの両方があります。ジェネリック パラメーターはエンティティ型である必要はありません。たとえば、特定のインターフェイスを実装するすべての読み込み済みオブジェクトのエントリを取得できます。図 5 のコードでは、すべてのブログをメモリに読み込み、いずれかのブログのプロパティを変更して、追跡している各ブログの状態を出力します。

図 5 DbContext による複数のエンティティに関する情報へのアクセス

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load all blogs into memory
    db.Blogs.Load();

    // Change the name of one blog
    var blog = db.Blogs.First();
    blog.Name = "ADO.NET Team Blog";

    // Print out state for each blog that is in memory
    foreach (var entry in db.ChangeTracker.Entries<Blog>())
    {
      Console.WriteLine("BlogId: {0}\n State: {1}\n",
        entry.Entity.BlogId,
        entry.State);
    }
  }

図 5 のコードを実行すると、コンソール出力は次のようになります。

BlogId: 1
  State: Modified
 
BlogId: 2
  State: Unchanged

ローカル インスタンスのクエリ: DbSet に対して LINQ クエリを実行すると、常に、処理対象のデータベースにクエリが送信されます。これにより、常に完全で最新の結果を得られることが保証されますが、必要なすべてのデータが既にメモリにあることがわかっていれば、ローカル データをクエリすることでデータベースへのラウンドトリップを避けることができます。図 6 のコードでは、すべてのブログをメモリに読み込んだら、データベースにアクセスしないブログに LINQ クエリを 2 つ実行します。

図 6 メモリ内のデータに対する LINQ クエリの実行

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load all blogs into memory
    db.Blogs.Load();

    // Query for blogs ordered by name
    var orderedBlogs = from b in db.Blogs.Local 
                       orderby b.Name
                       select b;

    Console.WriteLine("All Blogs:");
    foreach (var blog in orderedBlogs)
    {
      Console.WriteLine(" - {0}", blog.Name);
    }

    // Query for all RSS enabled blogs
    var rssBlogs = from b in db.Blogs.Local
                   where b is RssEnabledBlog
                   select b;

    Console.WriteLine("\n Rss Blog Count: {0}", rssBlogs.Count());
  }

  Console.ReadKey();
}

図 6 のコードを実行すると、コンソール出力は次のようになります。

All Blogs:
 - blogs.msdn.com/data
 - romiller.com
 
Rss Blog Count: 1

クエリとしてのナビゲーション プロパティ: DbContext は、特定のエンティティ インスタンスのナビゲーション プロパティの内容を表すクエリを取得できるようにします。これにより、メモリに格納する項目を形状化したりフィルター処理したりできるため、不必要なデータを返すことを回避できます。

たとえば、ブログのインスタンスが 1 つあり、そこにいくつの投稿が寄せられているかを知りたいとします。図 7 のコードを記述することもできますが、ここでは遅延読み込みを使用して、数を見つけるためだけにすべての関連投稿をメモリに戻しています。

図 7 遅延読み込みによるデータベースの項目数の取得

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load a single blog
    var blog = db.Blogs.First();

    // Print out the number of posts
    Console.WriteLine("Blog {0} has {1} posts.",
      blog.BlogId,
      blog.Posts.Count());
  }

  Console.ReadKey();
}

本当に必要な 1 つの整数結果だけを取得する場合と比べると、やたらと多くのデータがデータベースから転送され、メモリも多く消費されています。

さいわい、DbContext の Entry メソッドを使用してコードを最適化して、ブログに関連する投稿のコレクションを表すクエリを取得することができます。LINQ は構成可能なので、"Count" 演算子で連鎖させることができ、1 つの整数結果だけが返されるようにクエリ全体がデータベースにプッシュされます (図 8 参照)。

図 8 DbContext によるクエリ コードの最適化とリソースの節約

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load a single blog
    var blog = db.Blogs.First();

    // Query for count
    var postCount = db.Entry(blog)
      .Collection(b => b.Posts)
      .Query()
      .Count();

    // Print out the number of posts
    Console.WriteLine("Blog {0} has {1} posts.",
      blog.BlogId,
      postCount);
  }

  Console.ReadKey();
}

配置に関する考慮事項

ここまでは、データのアクセスについて見てきました。ここからは、アプリケーションの完成後、製品リリースを準備する際に考慮すべきことについて説明します。

接続文字列: これまでは、Code First によってデータベースを localhost\SQLEXPRESS に生成するようにしてきました。アプリケーションを配置するときになったら、おそらく Code First によって指定されているデータベースを変更することになります。App.config ファイル (Web アプリケーションでは Web.config) に接続文字列を追加するのが、推奨アプローチです。これは、Code First を使用して既存のデータベースにマップする際の推奨アプローチでもあります。接続文字列名がコンテキストの完全修飾名と一致すると、Code First は実行時にその名称を自動的に選択します。ただし、name=<接続文字列名> 構文を使用して接続名を受け取る DbContext コンストラクターを使用するのが推奨アプローチです。これにより、Code First は常に構成ファイルを使用するようになります。接続文字列が見つからなければ、例外がスローされます。次の例に、サンプル アプリケーションがターゲットとするデータベースに影響を与えるのに使用できる接続文字列のセクションを示します。

<connectionStrings>
  <add 
    name="Blogging" 
    providerName="System.Data.SqlClient"
    connectionString="Server=MyServer;Database=Blogging;
    Integrated Security=True;MultipleActiveResultSets=True;" />
</connectionStrings>

次に、更新されたコンテキストのコードを示します。

public class BlogContext : DbContext
{
  public BlogContext() 
    : base("name=Blogging")
  {}

  public DbSet<Blog> Blogs { get; set; }
  public DbSet<Post> Posts { get; set; }
}

"MultipleActiveResultSets" を有効にすることをお勧めします。これにより、2 つのクエリを同時にアクティブにすることできます。たとえば、すべてのブログを列挙しながら、ブログに関連付けられている投稿をクエリする際にこれが必要になります。

データベース初期化子: 既定では、ターゲットのデータベースが存在しなければ、Code First によって自動的にデータベースが作成されます。配置を行う際にもこの機能が必要だと考える人もいるため、アプリケーションが最初に起動されるときに、運用データベースのみが作成されます。しかし、運用環境を管理するデータベース管理者が存在する場合、データベース管理者が運用環境を作成する可能性がはるかに高く、アプリケーションが配置されるときに、ターゲットのデータベースが存在しなければ失敗すべきです。ここでは、初期化子の既定のロジックをオーバーライドして、スキーマが変更されるたびにデータベースが削除され再度作成されるよう構成しました。これは、運用環境に配置後は、決してそのままにはしておきたくないロジックです。

配置の際に初期化子の動作を変更または無効にするのに推奨されるのは、App.config ファイル (Web アプリケーションには Web.config) を使用するアプローチです。appSettings セクションでは、キーが DatabaseInitializerForType であるエントリを追加します。その後に、コンテキストの型名前と、それが定義されているアセンブリを追加します。値は "Disabled" か、初期化子の型名 (この後には、初期化子の型が定義されているアセンブリが続く) になります。

次の例は、今回使用してきたコンテキストの初期化子のロジックをすべて無効にします。

<appSettings>
  <add 
    key="DatabaseInitializerForType Blogging.BlogContext, Blogging" 
    value="Disabled" />
</appSettings>

次の例では、初期化子を既定の機能に戻し、存在しない場合にのみデータベースが作成されるようになります。

<appSettings>
  <add 
    key="DatabaseInitializerForType Blogging.BlogContext, Blogging" 
    value="System.Data.Entity.CreateDatabaseIfNotExists EntityFramework" />
</appSettings>

ユーザー アカウント: 運用アプリケーションにデータベースを作成させる場合、まず最初に、データベースの作成権限とスキーマの変更権限のあるアカウントを使用してアプリケーションを実行する必要があります。実行中もこれらの権限をそのままにしておくと、アプリケーションのセキュリティ侵害による潜在的な影響が劇的に増加します。アプリケーションが、データをクエリして保持するのに必要な最小限の権限で実行することを強くお勧めします。

詳細情報

今回の記事では、ADO.NET Entity Framework 4.1 に付属する Code First 開発と新しい DbContext API の概要を示しました。また、Fluent API を使用して、既存のデータベースにマップを行ったり、Code First によって生成されるデータベース スキーマの形状に影響を与えたりする方法を確認しました。次に変更追跡 API について解説し、それを使用してローカルのエンティティ インスタンスとそれらのインスタンスについての追加情報をクエリする方法を示しました。最後に、データ アクセスのために Code First を使用するアプリケーションを配置する際の考慮事項について扱いました。

Entity Framework 4.1 に付属する機能の詳細については、msdn.com/data/ef を参照してください。Entity Framework 4.1 を使用するうえでわからないことがありましたら、開発者向け技術情報サイトのフォーラム (social.msdn.microsoft.com/Forums/ja-jp/adodotnetentityframework/threads、英語) もご活用ください。

Rowan Miller はマイクロソフトの Entity Framework チームのプログラム マネージャーです。彼のブログ (romiller.com、英語) では、Entity Framework についての詳細が解説されています。

この記事のレビューに協力してくれた技術スタッフの Arthur Vickers に心より感謝いたします。