DbSet实际如何在后台运行?

问题描述

我很困惑如何在实体框架中设置DbSet的值。

假设我在DataContext类(我们自己的类继承自DbContext类)中有以下代码片段?

public DbSet<Values> Values {get; set;} 

然后我实际上可以通过具有依赖项注入的控制器通过表(值表)检索值,如下所示。现在我的问题是DbSet如何设置值?在从中获取属性之前,我看不到如何设置其值。

this.context.Values.FirstOrDefaultAsync(list => list.id == id);

解决方法

谈论DbSet的工作方式时有两个主要问题:

  • 查询数据:使用界面IQueryable<...>
  • 更改数据:添加/删除/更新

IQueryable

实现IQueryable<TSource>的类的对象不代表相似项目的序列,而是代表获得可枚举相似项目序列的潜力

为此,一个IQueryable具有两个属性:ExpressionProvider。表达式以某种通用格式保存以下信息:必须获取哪些数据,提供者知道谁必须获取数据(通常是数据库管理系统DBMS)以及用于与DBMS通信的语言(通常是SQL)。

在最低级别,要获取可枚举的序列,您需要调用IQueryable.GetEnumerator()。这会将表达式发送给提供程序,该提供程序会将表达式转换为DBMS可以理解的格式。提供程序将执行查询并以Enumerable<TResult>的形式返回获取的数据。

要访问获取的项目,请重复调用MoveNext(),只要它返回true,就可以使用属性Current来获取获取的TResult。

要访问数据库,DbSet对象知道它属于哪个DbContext。 DbContext具有属性Database,可以执行转换后的SQL语句。

大多数人很少使用GetEnumerator / MoveNext / Current,而使用foreach,它在内部深处使用此方法枚举元素。

如果您仔细观察LINQ,您会发现有两组IQueryable方法。返回IQueryable<...>的人和其他人。

返回IQueryable<...>的方法将不执行查询。这些方法使用延迟执行或延迟执行:仅更改表达式。这些方法速度很快,几乎不会花费任何处理能力。

其他方法(例如ToList()/ ToDictionary()/ FirstOrDefault()/ Sum()/ Any())将实际执行查询:将Expression发送到数据库,并相应地返回获取的数据。

ToList 将如下所示:

List<TSource> ToList<TSource>(this IQueryable<TSource> source)
{
    List<TSource> result = new List<TSource>();
    using (IEnumerator<TSource> enumerator = source.GetEnumerator())
    while (enumerator.MoveNext())
    {
        // There is still an item; add it to the list
        TSource item = enumerator.Current;
        result.Add(item);
    }
    return result;
}

任何

bool Any<TSource>(this IQueryable<TSource> source)
{
    using (IEnumerator<TSource> enumerator = source.GetEnumerator())
    {
        return enumerator.MoveNext(); // I only need to know if I can get the first element
    }
}

您现在应该已经可以理解,如果您仍然有IQueryable<...>并处理上下文,为什么代码不起作用:表达式已经生成,但是尚未执行。处置dbContext之后,无法再次打开与数据库的连接:

IQueryable<Customer> newYorkCustomers;
using (var dbContext = new CustomerDbContext(...))
{
    newYorkCustomers = dbContext.Customers.Where(customer => customer.City == "New York");
}

var result = newYorkCustomers.ToList();
// Expect exception: DbContext is disposed

您现在还应该了解,如果您有一个要多次枚举的查询,将它设为列表是明智的,否则该查询将被执行两次

更改数据库中的项目

每个DbSet都知道它在哪个DbContext中。每个DbContext都有一个ChangeTracker,他会跟踪所有提取的项目以及对其所做的所有更改。

如果您使用Find查找项目,或使用查询获取完整的项目,则这些项目的原始值存储在ChangeTracker中。

changeTracker包含所有三个客户和customer1。您可以使用以下代码访问它们:

using (var dbContext = new CustomerDbContext(...))
{
    var customersWithoutOrders = dbContext.Customers
        .Where(customer => !customer.Orders.Any())
        .ToList();
    Customer customer = dbContext.Customers.Find(1);
    var changeTracker = dbContext.ChangeTracker;
    var fetchedCustomers = changeTracker.Entries<Customer>();

fetchedCustomers将为每个没有订单的客户和该客户包含DbEntityEntry

每个DbEntityEntry都为每个属性保留原始值和当前值。如果您询问DbEntityEntry的状态,它将检查原始值和当前值以确定其是否已更改。

从数据库中添加/删除项目

如果要从数据库中删除项目,则首先必须获取它。这样可以确保它也在DbChangeTracker中。

dbContext.Customers.Remove(fetchedCustomer);

这会将fetchedCustomer的DbEntityEntry的状态设置为Deleted

添加的项目也在DbChangeTracker中。它们的状态等于Added

DbContext.SaveChanges

SaveChanges将获取所有DbEntityEntries,以查看添加/删除/更改了哪些项目,并将执行所需的SQL语句。

由于DbEntityEntry知道每个属性的原始值以及当前值,因此它知道要更新的值。

,

如果克隆Entity Framework Core源代码,您将看到该过程如何工作。之所以没有“看到”设置属性的位置,是因为该代码被隐藏在Microsoft.EntityFrameworkCore.dll库中。