在谈论 DbSet 的工作原理时,有两个主要问题:
- 查询数据:使用接口
IQueryable<...>
- 更改数据:添加/删除/更新
可查询
实现IQueryable<TSource> 的类的对象不代表类似项目的序列,它表示有可能获得可枚举的类似项目序列。
为此,IQueryable 具有两个属性:Expression 和 Provider。表达式以一些通用格式保存有关必须获取哪些数据的信息,提供者知道谁必须获取数据(通常是数据库管理系统 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() 将实际执行查询:将表达式发送到数据库并相应地返回获取的数据。
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
现在你也应该明白了,如果你有一个查询要枚举多次,最好将其设为 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 知道每个属性的原始值以及当前值,所以它知道要更新哪些值。