プログラミング私的ナレッジベース

EntityFramework

最終更新:

knowledgebase

- view
管理者のみ編集可

プログラミング

EntityFrameworkについて

http://msdn.microsoft.com/ja-jp/library/bb386871%28v=vs.90%29.aspx

EntityFramework 5 VisualStudio2010とVisualStudio2012に対応。
EntityFramework 5 Beta 2012年3月29日 VisualStudio11に対応。また、VisualStudio2010ではエラーが発生するため、まだ動作しない。
EntityFramework 4.3.1 2012年3月29日

インストール

VisualStudioから使用する場合、VisualStudioのExtensionであるNuGetManagerを使います。

VisualStudio2010では拡張機能であるNuGetPackageManagerを導入すれば、VisualStudio内からパッケージの取得・管理を行うことができる。

EF5

  • NotMapped属性の仕様が変更。

バックエンドデータベース

SQL Server Compact 4.0

Microsoft SQL Server Compact 4.0 は、ソフトウェア開発者が ASP.NET Web サイトの構築と Windows デスクトップ アプリケーションの作成に使用できる、無償の埋め込み型データベースです。SQL Server Compact 4.0 はコンパクトで、アプリケーション フォルダー内にバイナリ ファイルをプライベート配置することができ、Visual Studio と WebMatrix を使用してアプリケーションを簡単に開発でき、スキーマとデータを SQL Server にシームレスに移行できます。

Code Firstプログラミング

Code Firstプログラミングとは、データベースを使うにあたってのデータ構造をSQLや別のツールを使ってあらかじめ構築することなく、C#のオブジェクト指向プログラミングで定義されたクラスをそのままデータ構造として使うプログラミング方法です。

EntityFrameworksでは使用するクラスを元にデータ構造を構築し、データベースの作成します。

  • PlainOldなクラスをデータモデルクラスに使用できる(POCOと同じ)
  • CoC(convention over configuration) (Wikipedia:設定より規約)
  • IDEが持つデザイナやXMLによる、マッピング記述が不必要

バックエンドのデータソースとしてSQLServerCEバージョン4.0を使用しています。

サンプルソース

namespace Magic.Unicorn
{
    public class Princess : IPerson
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection Unicorns { get; set; }
        public virtual ICollection LadiesInWaiting { get; set; }
    }

    public class Unicorn
    {
        public int Id { get; set; }
        public string Name { get; set; }

        [Timestamp]
        public byte[] Version { get; set; }

        public int PrincessId { get; set; } // FK for Princess reference
        public virtual Princess Princess { get; set; }
    }

    public class Castle
    {
        [Key]
        public string Name { get; set; }

        public Location Location { get; set; }

        public virtual ICollection LadiesInWaiting { get; set; }
    }

    [ComplexType]
    public class Location
    {
        public string City { get; set; }
        public string Kingdom { get; set; }

        public ImaginaryWorld ImaginaryWorld { get; set; }
    }

    [ComplexType]
    public class ImaginaryWorld
    {
        public string Name { get; set; }
        public string Creator { get; set; }
    }

    public class LadyInWaiting : IPerson
    {
        [Key, Column(Order = 0)]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int PrincessId { get; set; } // FK for Princess reference

        [Key, Column(Order = 1)]
        public string CastleName { get; set; } // FK for Castle reference

        public string FirstName { get; set; }
        public string Title { get; set; }

        [NotMapped]
        public string Name
        {
            get
            {
                return String.Format("{0} {1}", Title, FirstName);
            }
        }

        public virtual Castle Castle { get; set; }
        public virtual Princess Princess { get; set; }
    }

    public interface IPerson
    {
        string Name { get; }
    }

    public class UnicornsContext : DbContext
    {
        public DbSet Unicorns { get; set; }
        public DbSet Princesses { get; set; }
        public DbSet LadiesInWaiting { get; set; }
        public DbSet Castles { get; set; }
    }

    public class UnicornsContextInitializer : DropCreateDatabaseAlways
    {
        protected override void Seed(UnicornsContext context)
        {
            var cinderella = new Princess { Name = "Cinderella" };
            var sleepingBeauty = new Princess { Name = "Sleeping Beauty" };
            var snowWhite = new Princess { Name = "Snow White" };

            new List
            {
                new Unicorn { Name = "Binky" , Princess = cinderella },
                new Unicorn { Name = "Silly" , Princess = cinderella },
                new Unicorn { Name = "Beepy" , Princess = sleepingBeauty },
                new Unicorn { Name = "Creepy" , Princess = snowWhite },
                                //new Unicorn { Name = "No Princess"  } // 外部関連であるPrincessを未設定にはできない。
            }.ForEach(u => context.Unicorns.Add(u));

            var efCastle = new Castle
            {
                Name = "The EF Castle",
                Location = new Location
                {
                    City = "Redmond",
                    Kingdom = "Rainier",
                    ImaginaryWorld = new ImaginaryWorld
                    {
                        Name = "Magic Unicorn World",
                        Creator = "ADO.NET"
                    }
                },
            };

            new List
            {
                new LadyInWaiting { Princess = cinderella,
                                    Castle = efCastle,
                                    FirstName = "Lettice",
                                    Title = "Countess" },
                new LadyInWaiting { Princess = sleepingBeauty,
                                    Castle = efCastle,
                                    FirstName = "Ulrika",
                                    Title = "Lady" },
                new LadyInWaiting { Princess = snowWhite,
                                    Castle = efCastle,
                                    FirstName = "Yolande",
                                    Title = "Duchess" }
            }.ForEach(l => context.LadiesInWaiting.Add(l));
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            Database.SetInitializer(new UnicornsContextInitializer());
                        var context = new UnicornsContext();
                        
            // Many of the code fragments can be run by inserting them here
        }
    }
}

DbContext

DbContextはEntityFrameworkをつかってバックエンドデータベースと関連付けされたDbSetを管理するクラスです。<br> DbSetのインスタンスは自動的にDbContext内でオブジェクトが設定されます。このオブジェクトは再設定することはないので、後述のようなGetterプロパティのみを公開したIDbSet型のプロパティを使います。

public class UnicornsContext : DbContext
{
    public DbSet Unicorns { get; set; }
    public DbSet Princesses { get; set; }
    public DbSet LadiesInWaiting { get; set; }
    public DbSet Castles { get; set; }
}

// または、下記のようにIDbSetを使ったプロパティ

public class UnicornsContext : DbContext
{
    public IDbSet Unicorns { get; set; }
    public IDbSet Princesses { get; set; }
    public IDbSet LadiesInWaiting { get; set; }
    public IDbSet Castles { get; set; }
}

ContextのDbSetオブジェクトは再設定する必要がないため、Getterプロパティのみ公開するプロパティがもっとも理想です。

Setメソッドは、DbContext.Setメソッドとして実装しており、この関数はDbSet<T>を返すメソッドです。

public class UnicornsContext : DbContext
{
        public IDbSet Unicorns
        {
                get { return Set(); }
        }

        public IDbSet Princesses
        {
                get { return Set(); }
        }

        public IDbSet LadiesInWaiting
        {
                get { return Set(); }
        }

        public IDbSet Castles
        {
                get { return Set(); }
        }
}

エンティティクラス

日付(DateTime)

DateTime型のフィールドです。

Nullを指定してもよい場合は、明示的にNull許容型のDataTime型として定義しなければなりません。

public class TestEntity
{
        // Nullは指定できない。
        // また、値は必ず設定する必要がある。
        public DateTime NotNullDate{get; set;}
        
        // Nullは指定可能。
        // 値は設定しない場合、自動的にNullになる。
        public DateTime? NullDate{get;set;}
}

NotNullateはNullにすることはできないため、TestEntityのオブジェクトを作成したら必ず有効な値を設定しなければなりません。

複合型フィールド

シリアライズ可能なクラスを値型としてEntityFrameworksに認識させることで、1つのテーブルのフィールドとして構築することができます。

次のような場合、CastleクラスのテーブルにはLocationクラスのフィールドが含まれます。

public class Castle
{
        [Key]
        public string Name { get; set; }

        public Location Location { get; set; }

        public virtual ICollection LadiesInWaiting { get; set; }
}
        
[ComplexType]
public class Location
{
        public string City { get; set; }
        public string Kingdom { get; set; }

        public ImaginaryWorld ImaginaryWorld { get; set; }
}

[ComplexType]
public class ImaginaryWorld
{
        public string Name { get; set; }
        public string Creator { get; set; }
}

外部関連

Princessプロパティは自動的にPrincessIdフィールドを外部キーとして外部関連エンティティを構築します。<br> Prinsessプロパティで取得できるオブジェクトは、DbContextにより外部キーによって遅延読み込みが発生し、Princessプロパティアクセス時にデータベースから値が取得されエンティティを返します。

public class Unicorn
{
        public int Id { get; set; }
        public string Name { get; set; }

        [Timestamp]
        public byte[] Version { get; set; }

        public int PrincessId { get; set; } // FK for Princess reference
        public virtual Princess Princess { get; set; }
}

実際のUnicornテーブルのカラムは次のようになります。

int Id
byte[] Version
int PrincessId

モデルの階層化(TPT)

属性

KeyAttribute
[Key]
モデルの主キーとなるプロパティを指定する。
StringLengthAttribute
[StringLength(XXX)]
MaxLengthAttribute
[MaxLength(XXX)]
string型の場合、フィールドの最大文字数を設定します。MinLengthは未実装?
ConcurrencyCheckAttribute
[ConcurrencyCheck]
データ検証を有効にするプロパティにマークする。
RequiredAttribute
[Required]
NULLを設定できないプロパティであることをマークする。
TimestampAttribute
[Timestamp]
タイムスタンプ情報を格納するフィールドであることを指定する。このフィールドはモデルクラスではbyte[]でマッピングします。
ComplexTypeAttribute
[ComplexType]
モデルに含むことができる複合データクラスである場合に指定する。クラス定義にのみ指定可能。この属性を設定したクラスは、データベースにシリアライズ化されて保存可能となります。ただし、複合データクラスは公開プロパティにプリミティブ型か他の複合データクラスしかメンバ変数に含むことはできません。
ColumnAttribute
[Column]
TableAttribute
[Table]
テーブル作成に関する情報を設定する。Nameプロパティは、作成するテーブル名を任意のものに設定します。
InversePropertyAttribute
[InverseProperty]
ForeignKeyAttribute
[ForeignKey]
DatabaseGeneratedAttribute
[DatabaseGenerated]
NotMappedAttribute
[NotMapped]

データベースの設定

App.configやWeb.configに使用するデータベースの種類などを設定します。<br> 設定項目は使用するデータベース毎に異なるため、使用するデータベースのマニュアル等を参考にします。


        
                
        

記述した設定情報は、DbContextのコンストラクタで指定することで使用できます。

public class UnicornsContext : DbContext
{
        public UnicornsContext()
                : base("UnicornsCEDatabase")
                // または、次のように項目名を指定した記述も可能
                // base("name=UnicornsCEDatabase")
        {
        }
}

エンティティの使い方

エンティティの追加

エンティティのDbSetコレクションに新しいエンティティを追加して、DbContext.SaveChanges()を呼び出すことでデータが保存(アタッチ)されます。

context.Unicorns.Add(new Unicorn { Id = -1 ,Name="bbb"});
context.Unicorns.Add(new Unicorn { Name = "ccc" });

context.SaveChanges();

注意点として、DbContext.SaveChanges()を呼び出していない状態でも内部的にデータが保存される段階となることを忘れてはいけません。

context.Unicorns.Add(new Unicorn { Id = -1 ,Name="bbb"});
context.Unicorns.Add(new Unicorn { Name = "ccc" });

DumpUnicornTest(context); // 何も出力されない

context.SaveChanges();

DumpUnicornTest(context); // 上で追加した2つのUnicornレコードをダンプする

また、UnicornはIdがint型となっており、自動的にAutoIncrementが設定されているため、Unicornsに要素を追加した順にIdが割り振られます。<br> 上記のようにIdに「-1」という設定できない値や、未設定の場合でも正常な値を割り振ります。この動作はDbContext.SaveChanges()を実行した段階で行われます。

次のコードはDbContext.SaveChanges()を呼び出す前はIDが「-1」のレコードを見つけることができるが、DbContext.SaveChanges()を呼び出した後は見つけることができません。

context.Unicorns.Add(new Unicorn { Id = -1 ,Name="bbb"});

var newUnicorn = context.Unicorns.Find(-1);
if(newUnicorn != null)
{
        Console.WriteLine("Idが-1のUnicornが存在する");
}

context.SaveChanges();

var newUnicorn2 = context.Unicorns.Find(-1);
if(newUnicorn2 == null)
{
        Console.WriteLine("UnicornはNULLです");
}

Console.WriteLine("newUnicornのID=" + newUnicorn.Id); // IDは「-1」ではなく、DbContext.SaveChanges()で割り振られた新しいIDになっている。

エンティティの削除

DbSet.Removeにエンティティを削除するオブジェクトを与えます。

context.Unicorns.Remove(context.Unicorns.Find(1));

LINQを使ったエンティティの検索方法

DbSetはそのままLINQで使用可能なコレクションとして使用できるので、次のようなLINQを記述できます。

var unicorns = from u in context.Unicorns
               where u.Name.StartsWith("B")
               select u;

エンティティのキーから検索

エンティティの主キーで検索を行う場合は、取得したいエンティティのDbSet.Findメソッドで行います。<br> 複合キーを持つエンティティの場合、モデル定義時に指定したカラムの順番に引数を指定します。

var unicorn = context.Unicorns.Find(3);
// ID=3のエンティティを取得する。

var castle = context.Castles.Find("The EF Castle");
// IDが文字列型の場合でも、取得できます。

var lady = context.LadiesInWaiting.Find(3, "The EF Castle");
// エンティティが複合キーを定義している場合でも、可変引数を使用することで取得可能。

ジェネリックな方法でプロパティにアクセス

var unicorn = context.Unicorns.Find(3);
// Read the current value of the Name property

string currentName1 = context.Entry(unicorn).Property(u => u.Name).CurrentValue;
// 普通に「unicorn.Name」へアクセスする意味と同じ。

Console.WriteLine("Name={0}", currentName1);

context.Entry(unicorn).Property(u => u.Name).CurrentValue = "Franky"; // Setterも同じ。

// ◆文字列によるプロパティへのアクセスも可能。
object currentName2 = context.Entry(unicorn).Property("Name").CurrentValue; // Getter呼び出し
context.Entry(unicorn).Property("Name").CurrentValue = "Squeaky";           // Setter呼び出し

エンティティのプロパティ名を指定しなくても、プロパティの名前や値を巡回できます。

var unicorn = context.Unicorns.Find(3);

DbPropertyValues pvals = context.Entry(unicorn).CurrentValues;

foreach(var propertyName in pvals.PropertyNames)
{ // すべてのプロパティをダンプ
        Console.WriteLine("Property {0} has value {1}",
                           propertyName, pvals[propertyName]);
        
        // pvals[propertyName]への代入も可能。
}

エンティティにComplexTypeに設定されたクラスを持つフィールドがある場合、次のような方法でアクセスが可能です。

var castle = context.Castles.Find("The EF Castle");
// CastleクラスのLocationプロパティはComplexType属性が設定されたLocationクラス

var location = context.Entry(castle)
                       .Property(c => c.Location)
                       .CurrentValue;
// locationはLocationオブジェクト

var world1 = context.Entry(castle)
                     .Property(c => c.Location.ImaginaryWorld)
                     .CurrentValue;
// この呼び出し方法ができるのは、当たり前といえば当たり前。

var world2 = context.Entry(castle)
                     .ComplexProperty(c => c.Location) // ComplexTypeであるLocationにアクセス
                     .Property(l => l.ImaginaryWorld)  // Location.ImaginaryWordにアクセス
                     .CurrentValue;
// ComplexTypeのプロパティへアクセスする場合は、ComplexPropertyを使う。
// 前項の方法でもImaginaryWorldにアクセスできるので、わざわざComplexPropertyを使う必要はない気もする・・・。


var world3 = context.Entry(castle)
                     .Property("Location.ImaginaryWorld")
                     .CurrentValue;
// プロパティの指定に文字列を使う。もっとも実用的かも。
// ただし、文字列を使ってプロパティのリフレクションを取得しているハズなので、パフォーマンスはよくない。


// ComplexTypeの階層構造を遡って行く事もできる。
var creator1 = context.Entry(castle)
                       .ComplexProperty(c => c.Location)
                       .ComplexProperty(l => l.ImaginaryWorld)
                       .Property(w => w.Creator)
                       .CurrentValue;
                                           
var creator2 = context.Entry(castle)
                       .Property(c => c.Location.ImaginaryWorld.Creator)
                       .CurrentValue;
                                           
var creator3 = context.Entry(castle)
                       .Property("Location.ImaginaryWorld.Creator")
                       .CurrentValue;

ComplexTypeを巡回する場合、ComplexTypeはDbPropertyValuesオブジェクトなので、次のような再帰呼び出しでダンプ可能。

public static void WritePropertyValues(string parentPropertyName, DbPropertyValues propertyValues)
{
    foreach (var propertyName in propertyValues.PropertyNames)
    {
        var nestedValues = propertyValues[propertyName] as DbPropertyValues; // propertyNameが"Location"のプロパティはComplexTypeなので、DbPropertyValuesオブジェクトとなる。
        if (nestedValues != null)
        {
            WritePropertyValues(parentPropertyName + propertyName + ".", nestedValues);
        }
        else
        {
            Console.WriteLine("Property {0}{1} has value {2}",
                              parentPropertyName, propertyName,
                              propertyValues[propertyName]);
        }
    }
}

using (var context = new UnicornsContext())
{
    var castle = context.Castles.Find("The EF Castle");

    WritePropertyValues("", context.Entry(castle).CurrentValues);
}

関連

関連エンティティの積極的読み込み

Includeを使って外部関連しているプロパティを同時に読み込むことができます。

var princesses1 = context.Princesses
                                  .Include(p => p.Unicorns)
                                  .ToList();
// Include()を使うと、Pricessの読み込み時に同時に関連するUnicornのを読み込みます。

var princesses2 = context.Princesses
                                  .Include("Unicorns")
                                  .ToList();
// Includeにはプロパティ名を文字列で指定することもできる。

もちろん、積極的に外部関連エンティティを読みこまなくても遅延読み込みによってデータが読み込まれます。

関連エンティティの遅延読み込み

外部関連エンティティへのアクセス時にエンティティのデータをデータベースから読み込むことを遅延読み込みといいます。

EntityFrameworksで関連エンティティを遅延読み込み可能にするには、プロパティをvirtualで定義します。

public class Princess 
{ 
    public int Id { get; set; } 
    public string Name { get; set; } 
    public virtual ICollection Unicorns { get; set; } // 遅延読み込み可能
}

遅延読み込み無効化

すべての遅延読み込みが行われなくなります。

public class UnicornsContext : DbContext
{
    public UnicornsContext()
    {
        this.Configuration.LazyLoadingEnabled = false;
    }
}

ライフサイクル

EntityState Added IDをまだ持っていないエンティティ。SaveChanges()ではInsert処理が行われ、キーカラムがAutoIncrement属性を持ったテーブルの場合、自動的にIDが作成され割り振られる。
Deleted
Detached DbContextで管理されていないエンティティ。
Modified
Unchanged DbContextにアタッチ済みのエンティティ。フィールド値の変更がない状態。

Detachedなオブジェクトも、DbSet.Attach()を使ってDbContextにアタッチしたオブジェクトとして使用することが可能です。<br> もしくは、直接エンティティのStateをUnchangedに設定する方法が使えます。

var u = new Unicorn { Id = 1, Name = "アタッチしました" };
Console.WriteLine("Status=" + context.Entry(u).State); // Detached
context.Unicorns.Attach(u); // 推奨方法
// context.Entry(u).State = EntityState.Unchanged
// ↑Stateに直接代入しても同じ効果を得られる。
Console.WriteLine("Status=" + context.Entry(u).State); // Unchanged

context.SaveChanges();

このとき、SaveChanges()の呼び出しによりId=1のレコードは更新されるため、上記のコードのUnicornはNameが「アタッチしました」に更新されてしまいます。

EntityState.Add

このステータスを設定したエンティティは、SaveChanges()で新規レコードとしてテーブルに追加されます。

using (var context = new UnicornsContext())
{
    var unicorn = new Unicorn { Name = "Franky", PrincessId = 1};
    context.Entry(unicorn).State = EntityState.Added; // ライフサイクルステータスを「Added」に設定。
        //context.Unicors.Add(unicorn); // 内部でStateをEntityState.Addedに設定している。
        
    context.SaveChanges(); // Addedのエンティティを新規データとして処理する
}

新規追加しようとするエンティティのキーフィールドに値が入っててもよい。<br> キーのフィールド属性がAutoIncrementを設定してある場合は、自動的にIdを設定します。

Idがすでにデータベースに存在する場合、SaveChanges()でエラーが発生します。

using (var context = new UnicornsContext())
{
    var unicorn = new Unicorn { Id=1, Name = "Franky", PrincessId = 1}; // すでに「Id=1」がデータベースに存在するとする。
    context.Entry(unicorn).State = EntityState.Added;
        
    context.SaveChanges(); // 例外発生
        // Id=1がすでにデータベースに存在するので、
        // EntityState.Addedを設定したエンティティをテーブルに追加しようとして失敗する。
}

EntityState.Detached

DbContextで管理していないエンティティはすべてこのステータスとなります。

データベースに存在するキーを設定しても、エンティティのオブジェクトがDbContextで管理されていない(アタッチしていない)ため、EntityState.Detachedとなります。

var u1 = new Unicorn { Name = "Toshiva" };
Console.WriteLine("Status=" + context.Entry(u1).State); // EntityState.Detached

var u2 = new Unicorn { Id = 1, Name = "LG" }; // Id=1はデータベースに存在する
Console.WriteLine("Status=" + context.Entry(u2).State); // EntityState.Detached

DbContextから切り離されたエンティティオブジェクトは外部参照のトラッキングは行いません。

var u1 = context.Unicorns.Find(1);
Console.WriteLine(u1.Princess); // Princessを自動的に取得する

var u2 = context.Unicorns.Find(2);
context.Entity(u2).State = System.Data.EntityState.Detached;

Console.WriteLine(u1.Princess); // Princessを自動的に取得できないため、Nullを返す。

ローカルデータ(未コミットデータ)

DbContextが管理するエンティティはSaveChangedが呼び出されるまでデータベースに保存されません(永続化されない)。

このような永続化前のDbContextが持つデータをローカルデータと呼びます。

    context.Unicorns.Load(); // Unicornすべてを読み込み

    context.Unicorns.Add(new Unicorn { Name = "Linqy" }); // 新しいUnicornを追加
        // ただし、永続化はまだ行われていない。

    context.Unicorns.Remove(context.Unicorns.Find(1)); // Unicornを削除
        // ただし、永続化はまだ行われていない。

        
    // ◆ローカルデータのダンプ
    Console.WriteLine("In Local: ");
    foreach (var unicorn in context.Unicorns.Local)
    {
        Console.WriteLine("Found {0}: {1} with state {2}",
                          unicorn.Id, unicorn.Name, 
                          context.Entry(unicorn).State);
    }

    // ◆ 永続化済みデータのダンプ
    Console.WriteLine("
In DbSet query: ");
    foreach (var unicorn in context.Unicorns)
    {
        Console.WriteLine("Found {0}: {1} with state {2}",
                          unicorn.Id, unicorn.Name,
                          context.Entry(unicorn).State);
    }
In Local:
Found 0: Linqy with state Added
Found 2: Silly with state Unchanged
Found 3: Beepy with state Unchanged
Found 4: Creepy with state Unchanged

In DbSet query:
Found 1: Binky with state Deleted
Found 2: Silly with state Unchanged
Found 3: Beepy with state Unchanged
Found 4: Creepy with state Unchanged

低レベルSQLの実行

SQLを直接実行することができます。

var unicorns = context.Unicorns.SqlQuery("select * from Unicorns").ToList();
// Unicornクラスのオブジェクトを取得(エンティティ)

var unicornNames = context.Database.SqlQuery("select Name from Unicorns").ToList();
// エンティティ以外も取得可能。
// この場合、戻り値はstring型を要素に持つコレクションとなる。

context.Database.ExecuteSqlCommand("update Unicorns set Name = 'Franky' where Name = 'Beepy'"); 
// SELECT以外のSQLはExecuteSqlCommand()を使う。

参照

  • Using DbContext in EF4.1
    • EF4.1の使い方。EF4.3でも基本は同じ。
  • Walkthrough
    • 既存のDBからのマイグレーション方法を解説したブログポスト。
目安箱バナー