ローカル データ

DbSet に対して LINQ クエリを直接実行すると、常にデータベースにクエリが送信されますが、DbSet.Local プロパティを使用すると、現在メモリ内にあるデータにアクセスできます。 また、DbContext.Entry メソッドと DbContext.ChangeTracker.Entries メソッドを使用すると、EF がエンティティに関して追跡している追加情報にアクセスできます。 このトピックで紹介するテクニックは、Code First および EF Designer で作成されたモデルに等しく使用できます。

Local を使用してローカル データを確認する

DbSet の Local プロパティを使用すると、コンテキストによって現在追跡され、Deleted としてマークされていないセットのエンティティに簡単にアクセスできます。 Local プロパティにアクセスしても、クエリがデータベースに送信されることはありません。 つまり、この方法は通常、クエリが既に実行された後で使用されます。 Load 拡張メソッドを使用すると、クエリを実行し、コンテキストによって結果が追跡されるようにすることができます。 次に例を示します。

using (var context = new BloggingContext())
{
    // Load all blogs from the database into the context
    context.Blogs.Load();

    // Add a new blog to the context
    context.Blogs.Add(new Blog { Name = "My New Blog" });

    // Mark one of the existing blogs as Deleted
    context.Blogs.Remove(context.Blogs.Find(1));

    // Loop over the blogs in the context.
    Console.WriteLine("In Local: ");
    foreach (var blog in context.Blogs.Local)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            blog.BlogId,  
            blog.Name,
            context.Entry(blog).State);
    }

    // Perform a query against the database.
    Console.WriteLine("\nIn DbSet query: ");
    foreach (var blog in context.Blogs)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            blog.BlogId,  
            blog.Name,
            context.Entry(blog).State);
    }
}

データベースに 2 つのブログ (BlogId が 1 の "ADO.NET Blog" と、BlogId が 2 の "The Visual Studio Blog") がある場合は、次の出力が予想されます。

In Local:
Found 0: My New Blog with state Added
Found 2: The Visual Studio Blog with state Unchanged

In DbSet query:
Found 1: ADO.NET Blog with state Deleted
Found 2: The Visual Studio Blog with state Unchanged

これは、次の 3 つの点を示しています。

  • 新しいブログ "My New Blog" は、まだデータベースには保存されていませんが、Local コレクションに含まれています。 データベースでエンティティの実際のキーがまだ生成されていないため、このブログの主キーは 0 です。
  • "ADO.NET Blog" は、まだコンテキストによって追跡されていても、Local コレクションに含まれていません。 これは、DbSet から削除したので、削除済みとしてマークされたためです。
  • DbSet を使用してクエリを実行すると、削除のマークが付いたブログ (ADO.NET Blog) は結果に含まれ、データベースにまだ保存されていない新しいブログ (My New Blog) は結果に含まれません。 これは、DbSet がデータベースに対してクエリを実行しており、返される結果には常にデータベース内にあるものが反映されるためです。

Local を使用してコンテキストのエンティティを追加および削除する

DbSet の Local プロパティは、ObservableCollection を返します。これには、コンテキストのコンテンツとの同期が保たれるように、イベントがフックされています。 つまり、エンティティは Local コレクションでも DbSet でも追加または削除できます。 また、新しいエンティティをコンテキストに取り込むクエリでは、Local コレクションがそれらのエンティティで更新されることにもなります。 次に例を示します。

using (var context = new BloggingContext())
{
    // Load some posts from the database into the context
    context.Posts.Where(p => p.Tags.Contains("entity-framework")).Load();  

    // Get the local collection and make some changes to it
    var localPosts = context.Posts.Local;
    localPosts.Add(new Post { Name = "What's New in EF" });
    localPosts.Remove(context.Posts.Find(1));  

    // Loop over the posts in the context.
    Console.WriteLine("In Local after entity-framework query: ");
    foreach (var post in context.Posts.Local)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            post.Id,  
            post.Title,
            context.Entry(post).State);
    }

    var post1 = context.Posts.Find(1);
    Console.WriteLine(
        "State of post 1: {0} is {1}",
        post1.Name,  
        context.Entry(post1).State);  

    // Query some more posts from the database
    context.Posts.Where(p => p.Tags.Contains("asp.net")).Load();  

    // Loop over the posts in the context again.
    Console.WriteLine("\nIn Local after asp.net query: ");
    foreach (var post in context.Posts.Local)
    {
        Console.WriteLine(
            "Found {0}: {1} with state {2}",
            post.Id,  
            post.Title,
            context.Entry(post).State);
    }
}

"entity-framework" と "asp.net" というタグが付けられたいくつかの投稿があったとすると、出力は次のようになるでしょう。

In Local after entity-framework query:
Found 3: EF Designer Basics with state Unchanged
Found 5: EF Code First Basics with state Unchanged
Found 0: What's New in EF with state Added
State of post 1: EF Beginners Guide is Deleted

In Local after asp.net query:
Found 3: EF Designer Basics with state Unchanged
Found 5: EF Code First Basics with state Unchanged
Found 0: What's New in EF with state Added
Found 4: ASP.NET Beginners Guide with state Unchanged

これは、次の 3 つの点を示しています。

  • Local コレクションに追加された新しい投稿 "What's New in EF" は、コンテキストによって Added 状態で追跡されます。 そのため、SaveChanges が呼び出された場合、データベースに挿入されます。
  • ローカル コレクションから削除された投稿 (EF Beginners Guide) が、コンテキストで削除済みとしてマークされました。 そのため、SaveChanges が呼び出された場合、データベースから削除されます。
  • 2 番目のクエリでコンテキストに読み込まれた追加の投稿 (ASP.NET Beginners Guide) は、自動的に Local コレクションに追加されます。

Local に関する最後の注意点は、ObservableCollection であるため、エンティティが多数になるとパフォーマンスが良くない点です。 そのため、コンテキストで何千ものエンティティを処理している場合は、Local の使用はお勧めできません。

WPF データ バインディングに Local を使用する

DbSet の Local プロパティは ObservableCollection のインスタンスなので、WPF アプリケーションでのデータ バインディングに直接使用できます。 前のセクションで説明したように、これは、このプロパティがコンテキストのコンテンツと自動的に同期され、コンテキストのコンテンツが自動的にこのプロパティと同期されることを意味します。 Local ではデータベース クエリが一度も発生しないため、バインド対象となる何かがあるように、Local コレクションにデータを事前設定する必要があることに注意してください。

ここは完全な WPF データ バインディング サンプルに適した場所ではありませんが、主な要素は次のとおりです。

  • バインディング ソースを設定する
  • セットの Local プロパティにバインドする
  • データベースへのクエリを使用して Local にデータを設定する。

ナビゲーション プロパティへの WPF バインディング

マスターと詳細のデータ バインディングを実行している場合は、詳細ビューをいずれかのエンティティのナビゲーション プロパティにバインドしたいことがあります。 このように動作させるための簡単な方法は、ナビゲーション プロパティの ObservableCollection を使用することです。 次に例を示します。

public class Blog
{
    private readonly ObservableCollection<Post> _posts =
        new ObservableCollection<Post>();

    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual ObservableCollection<Post> Posts
    {
        get { return _posts; }
    }
}

Local を使用して SaveChanges のエンティティをクリーンアップする

ほとんどの場合、ナビゲーション プロパティから削除されたエンティティは、コンテキストで削除済みとして自動的にマークされることはありません。 たとえば、Blog.Posts コレクションから Post オブジェクトを削除した場合、SaveChanges が呼び出されたときに、その投稿は自動的には削除されません。 削除されるようにする必要がある場合は、SaveChanges を呼び出す前に、またはオーバーライドされた SaveChanges の一部として、これらの残っているエンティティを見つけ、削除済みとしてマークしなければならないことがあります。 次に例を示します。

public override int SaveChanges()
{
    foreach (var post in this.Posts.Local.ToList())
    {
        if (post.Blog == null)
        {
            this.Posts.Remove(post);
        }
    }

    return base.SaveChanges();
}

上のコードでは、Local コレクションを使用してすべての投稿を検索し、ブログ参照が削除済みになっていないすべての投稿をマークします。 ToList 呼び出しが必要です。呼び出さない場合、コレクションは列挙されている間に Remove 呼び出しによって変更されてしまうためです。 他のほとんどの状況では、最初に ToList を使用せずに、Local プロパティに対して直接クエリを実行できます。

Windows フォームのデータ バインディングに Local および ToBindingList を使用する

Windows フォームでは、ObservableCollection を直接使用した完全に忠実なデータ バインディングがサポートされていません。 ただし、データ バインディングに DbSet Local プロパティを使用して、前のセクションで説明したすべての利点を利用できます。 これは、Local ObservableCollection によってサポートされる IBindingList 実装を作成する ToBindingList 拡張メソッドを通じて実現されます。

ここは完全な Windows フォームのデータ バインディング サンプルに適した場所ではありませんが、主な要素は次のとおりです。

  • オブジェクト バインディング ソースを設定する
  • Local.ToBindingList() を使用してセットの Local プロパティにバインドする
  • データベースへのクエリを使用して Local にデータを設定する

追跡されるエンティティに関する詳細情報を取得する

このシリーズの例の多くは、Entry メソッドを使用して、エンティティの DbEntityEntry インスタンスを返します。 このエントリ オブジェクトは、現在の状態などのエンティティに関する情報を収集したり、関連エンティティの明示的な読み込みなどの操作をエンティティに対して実行したりするための開始点として機能します。

Entries メソッドは、コンテキストによって追跡されている多くのエンティティまたはすべてのエンティティの DbEntityEntry オブジェクトを返します。 これにより、ただ 1 つのエントリではなく、多くのエンティティを対象にして、情報を収集したり操作を実行したりできます。 次に例を示します。

using (var context = new BloggingContext())
{
    // Load some entities into the context
    context.Blogs.Load();
    context.Authors.Load();
    context.Readers.Load();

    // Make some changes
    context.Blogs.Find(1).Title = "The New ADO.NET Blog";
    context.Blogs.Remove(context.Blogs.Find(2));
    context.Authors.Add(new Author { Name = "Jane Doe" });
    context.Readers.Find(1).Username = "johndoe1987";

    // Look at the state of all entities in the context
    Console.WriteLine("All tracked entities: ");
    foreach (var entry in context.ChangeTracker.Entries())
    {
        Console.WriteLine(
            "Found entity of type {0} with state {1}",
            ObjectContext.GetObjectType(entry.Entity.GetType()).Name,
            entry.State);
    }

    // Find modified entities of any type
    Console.WriteLine("\nAll modified entities: ");
    foreach (var entry in context.ChangeTracker.Entries()
                              .Where(e => e.State == EntityState.Modified))
    {
        Console.WriteLine(
            "Found entity of type {0} with state {1}",
            ObjectContext.GetObjectType(entry.Entity.GetType()).Name,
            entry.State);
    }

    // Get some information about just the tracked blogs
    Console.WriteLine("\nTracked blogs: ");
    foreach (var entry in context.ChangeTracker.Entries<Blog>())
    {
        Console.WriteLine(
            "Found Blog {0}: {1} with original Name {2}",
            entry.Entity.BlogId,  
            entry.Entity.Name,
            entry.Property(p => p.Name).OriginalValue);
    }

    // Find all people (author or reader)
    Console.WriteLine("\nPeople: ");
    foreach (var entry in context.ChangeTracker.Entries<IPerson>())
    {
        Console.WriteLine("Found Person {0}", entry.Entity.Name);
    }
}

この例には、Author クラスと Reader クラスが導入されています。どちらのクラスも IPerson インターフェイスを実装しています。

public class Author : IPerson
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    public string Biography { get; set; }
}

public class Reader : IPerson
{
    public int ReaderId { get; set; }
    public string Name { get; set; }
    public string Username { get; set; }
}

public interface IPerson
{
    string Name { get; }
}

データベースに次のデータがあるとします。

BlogId = 1 で Name = "ADO.NET Blog" のブログ
BlogId = 2 で Name = "The Visual Studio Blog" のブログ
BlogId = 3 で Name = ".NET Framework Blog" のブログ
AuthorId = 1 で Name = "Joe Bloggs" の作成者
ReaderId = 1 で Name = "John Doe" の閲覧者

コードを実行した場合の出力は、次のようになります。

All tracked entities:
Found entity of type Blog with state Modified
Found entity of type Blog with state Deleted
Found entity of type Blog with state Unchanged
Found entity of type Author with state Unchanged
Found entity of type Author with state Added
Found entity of type Reader with state Modified

All modified entities:
Found entity of type Blog with state Modified
Found entity of type Reader with state Modified

Tracked blogs:
Found Blog 1: The New ADO.NET Blog with original Name ADO.NET Blog
Found Blog 2: The Visual Studio Blog with original Name The Visual Studio Blog
Found Blog 3: .NET Framework Blog with original Name .NET Framework Blog

People:
Found Person John Doe
Found Person Joe Bloggs
Found Person Jane Doe

これらの例は、いくつかの点を示しています。

  • Entries メソッドは、Deleted を含むすべての状態のエンティティのエントリを返します。 Deleted のエンティティが除外される Local と比較してみてください。
  • 非ジェネリック Entries メソッドを使用すると、すべてのエンティティ型のエントリが返されます。 ジェネリック Entries メソッドを使用すると、ジェネリック型のインスタンスであるエンティティの場合にだけエントリが返されます。 これは、前に、すべてのブログのエントリを取得するために使用しました。 また、IPerson を実装しているすべてのエンティティのエントリを取得するためにも使用しました。 これは、ジェネリック型が実際のエンティティ型である必要はないことを示しています。
  • LINQ to Objects を使用すると、返された結果をフィルター処理できます。 これは、前に、変更された任意の型のエンティティを検索するために使用しました。

DbEntityEntry インスタンスには、常に null 以外のエンティティが含まれていることに注意してください。 リレーションシップ エントリとスタブ エントリは、DbEntityEntry インスタンスとして表されることがないため、これらをフィルター処理する必要はありません。