【问题标题】:How does DbSet actually work under the hood?DbSet 实际上是如何工作的?
【发布时间】:2021-01-21 05:37:51
【问题描述】:

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

假设我在 DataContext 类中有如下代码 sn-p(我们自己的类继承自 DbContext 类)?

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

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

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

【问题讨论】:

  • 如果你想知道它是如何完成的,你可以去查看Entity Framework源代码。简而言之,它使用反射来加载DbSet 属性并使用上下文对其进行初始化,以便它们知道从何处获取数据
  • 我明白了。因此,当调用子上下文类的构造时,它使用类似 obj.GetType().GetProperties();获取 DbSet 属性,然后用它初始化属性。我就是这么理解的。如果我错了,请纠正我。

标签: c# .net entity-framework asp.net-core dbset


【解决方案1】:

在谈论 DbSet 的工作原理时,有两个主要问题:

  • 查询数据:使用接口IQueryable&lt;...&gt;
  • 更改数据:添加/删除/更新

可查询

实现IQueryable&lt;TSource&gt; 的类的对象不代表类似项目的序列,它表示有可能获得可枚举的类似项目序列。

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

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

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

为了访问数据库,DbSet 对象知道它属于哪个 DbContext。 DbContext有一个属性Database,可以执行翻译后的SQL语句。

大部分人很少会使用GetEnumerator/MoveNext/Current,他们使用foreach,内部使用这种方法枚举元素。

如果您仔细查看 LINQ,您会发现有两组 IQueryable 方法。那些返回IQueryable&lt;...&gt; 和其他人。

返回IQueryable&lt;...&gt; 的方法不会执行查询。这些方法使用延迟执行或延迟执行:仅更改表达式。这些方法速度很快,几乎不会消耗任何处理能力。

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

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&lt;...&gt; 并处置上下文,为什么您的代码无法运行:表达式已生成,但尚未执行。处理完 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

现在你也应该明白了,如果你有一个查询要枚举多次,最好将其设为 List,否则查询将被执行两次

更改数据库中的项目

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

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

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

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 知道每个属性的原始值以及当前值,所以它知道要更新哪些值。

【讨论】:

    【解决方案2】:

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

    【讨论】:

      猜你喜欢
      • 2011-09-27
      • 2021-12-16
      • 2013-03-14
      • 2021-03-23
      • 2011-02-11
      • 2017-07-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多