本地数据

直接对 DbSet 运行 LINQ 查询会始终将查询发送到数据库,但你可以使用 DbSet.Local 属性访问当前内存中的数据。 还可以使用 DbContext.Entry 和 DbContext.ChangeTracker.Entries 方法访问 EF 正在跟踪的有关实体的额外信息。 本主题所介绍的方法同样适用于查询使用 Code First 和 EF 设计器创建的模型。

使用 Local 查看本地数据

DbSet 的 Local 属性提供对当前由上下文跟踪且未标记为已删除的集实体的简单访问。 访问 Local 属性永远不会将查询发送到数据库。 这意味着,它通常在执行查询后使用。 加载扩展方法可用于执行查询,以便上下文跟踪结果。 例如:

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);
    }
}

如果我们在数据库中有两个博客(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

这说明了三点:

  • 新的博客“我的新博客”包含在 Local 集合中,即使它尚未保存到数据库。 此博客的主键为零,因为数据库尚未为实体生成实际键。
  • “ADO.NET 博客”不包括在 Local 集合中,即使上下文仍在跟踪它。 这是因为我们已将其从 DbSet 中删除,因此将其标记为已删除。
  • 当使用 DbSet 执行查询时,标记为删除的博客 (ADO.NET 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

这说明了三点:

  • 添加到 Local 集合的新帖子“EF 中的新增功能”将由处于“已添加”状态的上下文跟踪。 因此,调用 SaveChanges 时,它将插入到数据库中。
  • 从 Local 集合(EF 初学者指南)中删除的帖子现在在上下文中被标记为已删除。 因此,调用 SaveChanges 时,它将从数据库中删除。
  • 随第二个查询加载到上下文中的其他帖子(ASP.NET 初学者指南)将自动添加到 Local 集合中。

关于 Local 的最后一件事是,由于它是 ObservableCollection 性能,因此对于大量实体来说并不出色。 因此,如果正在上下文中处理数千个实体,则不建议使用 Local。

对 WPF 数据绑定使用 Local

DbSet 上的 Local 属性可直接用于 WPF 应用程序中的数据绑定,因为它是 ObservableCollection 的实例。 如前面的部分所述,这意味着它将自动与上下文的内容保持同步,并且上下文的内容将自动与其保持同步。 请注意,你确实需要使用数据预填充 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 调用修改。 在大多数其他情况下,可以直接针对 Local 属性进行查询,而无需先使用 ToList。

对 Windows 窗体数据绑定使用本地和 ToBindingList

Windows 窗体不支持直接使用 ObservableCollection 进行完全保真度数据绑定。 但是,仍然可以将 DbSet Local 属性用于数据绑定,以获得前面几部分中介绍的所有好处。 这是通过 ToBindingList 扩展方法实现的,该方法创建由 Local ObservableCollection 支持的 IBindingList 实现。

对于完整的 Windows 窗体数据绑定示例,这不是一个合适的位置,但关键元素是:

  • 设置对象绑定源
  • 使用 Local.ToBindingList() 将其绑定到集的 Local 属性
  • 使用对数据库的查询填充 Local

获取有关跟踪的实体的详细信息

本系列中的许多示例都使用 Entry 方法返回实体的 DbEntityEntry 实例。 然后,此入口对象充当起点,用于收集有关实体的信息(如其当前状态),以及用于对实体执行操作(如显式加载相关实体)。

Entries 方法返回上下文跟踪的许多或所有实体的 DbEntityEntry 对象。 这样,你可以对许多实体(而不仅仅是单个条目)收集信息或执行操作。 例如:

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 且名称 =“ADO.NET Blog”的博客
BlogId = 2 且名称 =“The Visual Studio Blog”的博客
BlogId = 3 且名称 =“.NET Framework Blog”的博客
AuthorId = 1 且姓名 =“Joe Bloggs”的作者
ReaderId = 1 且姓名=“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 方法返回处于所有状态(包括“已删除”)的实体的条目。 将此与排除已删除实体的 Local 进行比较。
  • 使用非泛型 Entries 方法时,将返回所有实体类型的条目。 使用泛型 Entries 方法时,仅为作为泛型类型实例的实体返回条目。 上述方法用于获取所有博客的条目。 它还用于获取实现 IPerson 的所有实体的条目。 这演示泛型类型不一定是实际实体类型。
  • LINQ to Objects 可用于筛选返回的结果。 只要修改了任何类型的实体,就使用上述方法来查找这些实体。

请注意,DbEntityEntry 实例始终包含非 null 实体。 关系条目和存根条目不表示为 DbEntityEntry 实例,因此无需对这些实例进行筛选。