实体框架设计器首先将导航属性作为任务获取

本文关键字:属性 导航 获取 任务 框架 实体 | 更新日期: 2023-09-27 18:21:26

Task模式规定,为了保持一致,所有东西都必须完全异步或完全不异步。

通过首先使用实体框架设计器,我可以很容易地实现

var course = await db.Courses.FindAsync(CourseID);

Courses是由实体框架生成的DbSet,因此具有所有Async方法。问题是,如果我向该类添加导航属性,则后者不是DbSet,也不包含任何异步方法定义。

例如,如果我向Students表添加导航属性,它将被创建为虚拟ICollection Students这意味着我不能使用异步方法。但我真正想要的是让实体框架自动生成任务<>以便能够等待甚至导航属性。

有可能吗?我的目标是实现这样的目标:

var course = await db.Courses.FindAsync(CourseID);
var student = await course.Students.FindAsync(StudentID);

而目前我的选择是混合异步/非异步代码:

var course = await db.Courses.FindAsync(CourseID);
var student = course.Students.First(p => p.ID = StudentID);

或者根本不使用导航属性:

var course = await db.Courses.FindAsync(CourseID);
var student = await db.Students.Where(p => p.CourseID == course.ID && p.ID == StudentsID).FirstAsync();

你能建议一个不需要代码的解决方案吗?

编辑根据https://entityframework.codeplex.com/wikipage?title=Task-基于%20Asynchronous%20Pattern%20support%20in%20EF#AsyncLazyLoading我正在搜索的内容被称为"Async Lazy Loading",它还不是一个可用的功能(也许永远不会)。看起来你可以使用Lazy Loading或异步功能,也许我应该通过等待Task.Run(course.Students.First(p=>p.ID=StudentID))将属性封装在Task中,但我不确定这是个好主意。

实体框架设计器首先将导航属性作为任务获取

在我看来,延迟加载(在我看来是枚举导航属性会触发数据库访问的情况)是一种糟糕的访问模式,因为它只是意味着数据库访问将发生在令人惊讶的地方,这可能会使应用程序性能难以预测。

以下所有解决方案都使用从System.Data.Entity导入的解决方案。

解决方案1:使用Include 进行紧急加载

var course = await db.Courses.Include(c => c.Students).FirstOrDefaultAsync(c => c.ID == CourseID);
var student = course.Students.First(p => p.ID == StudentID);

优点:

  • 加载所有对象所需的一个数据库访问—如果您想一次检索多个Course对象,则此解决方案的扩展性非常好
  • Students导航属性已加载,可以自由使用

缺点:

  • 总是至少有一个数据库访问
  • 即使您只需要一个,也会加载整个相关的Student对象集

解决方案2:使用具体集合类上存在的LoadAsync方法;

此解决方案依赖于延迟加载的集合来自EntityCollection<TEntity>类这一事实。

首先,我将定义一个扩展方法:

public static async Task LoadAsync<T>(ICollection<T> collection)
    where T : class
{
    if (collection == null) throw new ArgumentNullException("collection");
    var entityCollection = collection as System.Data.Entity.Core.Objects.DataClasses.EntityCollection<T>;
    if (entityCollection == null || entityCollection.IsLoaded) return;
    await entityCollection.LoadAsync(CancellationToken.None).ConfigureAwait(false);
}

然后你可以写这样的东西:

var course = await db.Courses.FindAsync(CourseID);
await course.Students.LoadAsync();
var student = course.Students.First(p => p.ID = StudentID);

优势:

  • 如果对象已经在上下文中加载,则可能根本没有数据库访问
  • 保证加载导航属性Students

缺点:

  • 易受"N+1查询"问题影响
  • Course和一组相关的Student对象都可能变得陈旧,这可能会在以后引发并发问题;(请注意,影响关系的并发问题比影响单个记录的并发问题更难解决)

解决方案3在具体类上使用CreateSourceQuery方法仅加载所需的Student对象

好吧,这样做是行不通的,而且实际上是一个非常糟糕的主意。

然而,可以用另一种方式编写具有相同优点/缺点的解决方案:

var course = await db.Courses.FindAsync(CourseID);
var studentsQuery = from c in db.Courses
                    where c.ID == CourseID
                    from s in c.Students
                    select s;
var student = await studentsQuery.FirstAsync(p => p.ID = StudentID);

优势:

  • 您只加载要使用的一个Student对象

缺点:

  • Students导航属性未加载,这意味着在没有潜在触发数据库访问的情况下无法使用它
  • 第二行总是会触发数据库访问(容易受到"N+1查询"问题的影响,甚至会运行方法次数)

解决方案4:加载更快,更具选择性:在初始LINQ查询中加载您感兴趣的课程和学生。

我不能100%确定该解决方案是否能按书面形式运行。

var query = from c in db.Courses
            where c.ID == CourseID
            select new { course = c, student = c.Students.First(p => p.ID == StudentID) };
var result = await query.FirstOrDefaultAsync();
var course = result.course;
var student = result.student;

优点:

  • 检索这两个对象只需要一次数据库访问
  • 您只检索要处理的对象

缺点:

  • Students导航属性未加载,这意味着在不可能触发数据库访问的情况下无法使用它

**何时使用哪种解决方案?**

  • 如果您需要填充导航属性(因为您知道将使用其大部分元素,或者因为您希望将父实体传递给另一个允许按其意愿使用该属性的组件),则使用解决方案1或2
  • 如果不需要填写导航属性,请使用解决方案4。如果明确地已经加载了Course对象,则仅使用解决方案3