带有 LoadAsync() 的 Query() 不是 return 一个实体,尽管它应该

Query() with LoadAsync() does not return an entity although it should

context.Pupils.Attach(pupil);

pupil.SchoolclassCodes集合是空的,但是必须有一个schoolclassCode,因为在底部的LoadAsync方法中是带有我在这里查询的Id的SchoolclassCode

await context.Entry(pupil).Collection(p => p.SchoolclassCodes).Query().Where(s => s.Id == schoolclassCodeId).LoadAsync();

用 pupil.SchoolclassCodes 集合中的一个 schoolclassCode 填充学生

await context.Entry(pupil).Collection(p => p.SchoolclassCodes).LoadAsync();

为什么 bottom LoadAsync 工作但 top LoadAsync 不工作?

更新

对于 pupil.SchoolclassCodeId 的混淆,这是一个 [NotMappedAttribute]

,我们深表歉意

就是这个关系

学生 N 有 M 个 SchoolclassCodes。

每个实体都有另一个实体的集合。

最后的LoadAsync如我所说,关系设置没有问题

问题是第一个 LoadAsync 如上所述不起作用!

延迟加载已完全禁用!我在任何地方都不使用虚拟道具!

更新 2

public class SchoolclassCode
{
    public SchoolclassCode()
    {
        Pupils = new HashSet<Pupil>();
    }

    public int Id { get; set; }
    public ISet<Pupil> Pupils { get; set; }

}

public class Pupil
{
    public Pupil()
    {
        SchoolclassCodes = new HashSet<SchoolclassCode>();
    }

    public int Id { get; set; }
    public ISet<SchoolclassCode> SchoolclassCodes { get; set; }

    [NotMapped]
    public int SchoolclassCodeId { get; set; }
}

字段 Pupil.SchoolclassCodeId 显然未用于此问题的目的,所以让我们忘记它。

您的第二个查询:

await context.Entry(pupil).Collection(p => p.SchoolclassCodes).LoadAsync();

按预期工作。我们可以用下面的代码来验证一下:

await context.Entry(pupil).Collection(p => p.SchoolclassCodes).LoadAsync();
Console.WriteLine("IsLoaded = " + context.Entry(pupil).Collection(p => p.SchoolclassCodes).IsLoaded);
foreach (var code in pupil.SchoolclassCodes)
  Console.WriteLine("  " + code.Id);

假设 pupil 在其 SchoolclassCodes 中有三个元素,那么 IsLoaded 将是 true,并且 foreach 循环将显示三个 id。

然后是您的第一个查询:

await context.Entry(pupil).Collection(p => p.SchoolclassCodes).Query().Where(s => s.Id == schoolclassCodeId).LoadAsync();

让我们测试一下:

var pupil = context.Pupils.First();
var schoolclassCodeId = 1;
await context.Entry(pupil).Collection(p => p.SchoolclassCodes).Query().Where(s => s.Id == schoolclassCodeId).LoadAsync();
Console.WriteLine("IsLoaded = " + context.Entry(pupil).Collection(p => p.SchoolclassCodes).IsLoaded);
foreach (var code in pupil.SchoolclassCodes)
  Console.WriteLine("  " + code.Id);

假设确实有一个 SchoolclassCode,其中 Id1AsyncLoad 应该恰好将一个 SchoolclassCode 加载到内存中。然而在输出中你可以看到 IsLoaded = false,而 foreach 什么也没有给出!为什么?

嗯,首先AsyncLoad不是应用于Collection(p => p.SchoolclassCodes),而是从它派生的IQueryable,所以IsLoaded应该是false,这个可以理解。

但确实有一个 SchoolclassCode 加载到上下文中:

foreach (var code in context.SchoolclassCodes.Local)
  Console.WriteLine("  " + code.Id);

这个foreach输出一个1。那么为什么我们在 pupil.SchoolclassCodes 中找不到 SchoolclassCode

答案是:SchoolclassCodePupil的关系是many-to-many。在这种情况下 Entity Framework 不会进行关系修复,即自动将 SchoolclassCode 添加到 Pupil.SchoolclassCodes,因此您不会在那里看到它。如果你真的想修复关系,你将不得不手动完成。

更新 1

引自MSDN

The Query method provides access to the underlying query that the Entity Framework will use when loading related entities. You can then use LINQ to apply filters to the query before executing it with a call to a LINQ extension method such as ToList, Load, etc. The Query method can be used with both reference and collection navigation properties but is most useful for collections where it can be used to load only part of the collection.

有点混乱。这似乎与我的论点矛盾,但事实并非如此。实际上,在上面的引用中,单词 "load" 的意思是 "load into the context",而不是 "load into the navigation property",因此 MSDN 和我的回答都是正确的。为了证明我的说法,让我们从一些实验开始,然后我们将深入研究源代码。

模型

出于演示目的,我们在模型中添加另一个 class:

public class Pupil
{
  public Pupil()
  {
    Book = new HashSet<Book>();
    SchoolclassCodes = new HashSet<SchoolclassCode>();
  }

  public int Id { get; set; }
  public ISet<Book> Books { get; set; }
  public ISet<SchoolclassCode> SchoolclassCodes { get; set; }
}

public class Book
{
  public int Id { get; set; }
  public Pupil Pupil { get; set; }
}

public class SchoolclassCode
{
  public SchoolclassCode()
  {
    Pupils = new HashSet<Pupil>();
  }

  public int Id { get; set; }
  public ISet<Pupil> Pupils { get; set; }
}

PupilSchoolclassCode的关系是many-to-many,和之前一样,Pupil和新增的Book的关系是one-to-many。上下文 class 是:

public class SchoolEntities: DbContext
{
  public SchoolEntities()
    : base("name=SchoolEntities")
  {
  }

  public DbSet<Pupil> Pupils { get; set; }
  public DbSet<Book> Books { get; set; }
  public DbSet<SchoolclassCode> SchoolclassCodes { get; set; }
}

数据

我们在数据库中有以下条目:

Pupil  (Id = 1)
  the Books property contains:
    Book  (Id = 1)
    Book  (Id = 2)
  the SchoolclassCodes property contains:
    SchoolclassCode  (Id = 1)
    SchoolclassCode  (Id = 2)
    SchoolclassCode  (Id = 3)

实验 1:直接加载

我们直接在导航中加载相关数据属性。为简单起见,我们使用 Load 方法而不是 LoadAsync。它们做的完全一样,只是前者是同步的而后者是异步的。验证码:

using (var context = new SchoolEntities()) {
  Console.WriteLine("Books direct load");
  var pupil = context.Pupils.First();
  context.Entry(pupil).Collection(p => p.Books).Load();
  Console.WriteLine("  IsLoaded = " + context.Entry(pupil).Collection(p => p.Books).IsLoaded);
  Console.WriteLine("  Items in the pupil:");
  foreach (var item in pupil.Books)
    Console.WriteLine("    " + item.Id);
  Console.WriteLine("  Items in the context:");
  foreach (var item in context.Books.Local)
    Console.WriteLine("    " + item.Id);
}
using (var context = new SchoolEntities()) {
  Console.WriteLine("SchoolclassCodes direct load");
  var pupil = context.Pupils.First();
  context.Entry(pupil).Collection(p => p.SchoolclassCodes).Load();
  Console.WriteLine("  IsLoaded = " + context.Entry(pupil).Collection(p => p.SchoolclassCodes).IsLoaded);
  Console.WriteLine("  Items in the pupil:");
  foreach (var item in pupil.SchoolclassCodes)
    Console.WriteLine("    " + item.Id);
  Console.WriteLine("  Items in the context:");
  foreach (var item in context.SchoolclassCodes.Local)
    Console.WriteLine("    " + item.Id);
}

和输出:

Books direct load
  IsLoaded = True
  Items in the pupil:
    1
    2
  Items in the context:
    1
    2
SchoolclassCodes direct load
  IsLoaded = True
  Items in the pupil:
    1
    2
    3
  Items in the context:
    1
    2
    3

实验分为两部分,一部分用于Books,另一部分用于SchoolclassCodes。使用两个上下文来确保两个部分不会相互干扰。我们使用collection的Load方法将相关数据直接加载到导航属性中。结果显示:

  1. collection的IsLoaded属性设置为true
  2. 加载的数据可以在导航属性中找到(即pupil.Bookspupil.SchoolclassCodes);
  3. 加载的数据也可以在上下文中找到(即context.Books.Localcontext.SchoolclassCodes.Local)。

实验 2:部分加载查询

我们使用Query方法加载部分相关数据,然后是Where:

using (var context = new SchoolEntities()) {
  Console.WriteLine("Books partial query load");
  var pupil = context.Pupils.First();
  context.Entry(pupil).Collection(p => p.Books).Query().Where(s => s.Id == 1).Load();
  Console.WriteLine("  IsLoaded = " + context.Entry(pupil).Collection(p => p.Books).IsLoaded);
  Console.WriteLine("  Items in the pupil:");
  foreach (var item in pupil.Books)
    Console.WriteLine("    " + item.Id);
  Console.WriteLine("  Items in the context:");
  foreach (var item in context.Books.Local)
    Console.WriteLine("    " + item.Id);
}
using (var context = new SchoolEntities()) {
  Console.WriteLine("SchoolclassCodes partial query load");
  var pupil = context.Pupils.First();
  context.Entry(pupil).Collection(p => p.SchoolclassCodes).Query().Where(s => s.Id == 1).Load();
  Console.WriteLine("  IsLoaded = " + context.Entry(pupil).Collection(p => p.SchoolclassCodes).IsLoaded);
  Console.WriteLine("  Items in the pupil:");
  foreach (var item in pupil.SchoolclassCodes)
    Console.WriteLine("    " + item.Id);
  Console.WriteLine("  Items in the context:");
  foreach (var item in context.SchoolclassCodes.Local)
    Console.WriteLine("    " + item.Id);
}

大部分代码与实验一相同;请注意以 context.Entry(pupil)... 开头的行。输出:

Books partial query load
  IsLoaded = False
  Items in the pupil:
    1
  Items in the context:
    1
SchoolclassCodes partial query load
  IsLoaded = False
  Items in the pupil:
  Items in the context:
    1

看出区别了吗?

    在这两种情况下,
  1. IsLoaded 现在都是 false
  2. 加载的数据仍然进入上下文;
  3. 但是,加载的数据在 SchoolclassCodes 情况下不会进入导航 属性,而在 Books 情况下会。

差异是由关系类型造成的:Books 是 one-to-many,而 SchoolclassCodes 是 many-to-many。 Entity Framework 对这两种类型的处理方式不同。

实验 3:满载查询

那么如果我们使用 Query 而没有 Where 呢?让我们看看:

using (var context = new SchoolEntities()) {
  Console.WriteLine("Books full query load");
  var pupil = context.Pupils.First();
  context.Entry(pupil).Collection(p => p.Books).Query().Load();
  // output statements omitted...
}
using (var context = new SchoolEntities()) {
  Console.WriteLine("SchoolclassCodes full query load");
  var pupil = context.Pupils.First();
  context.Entry(pupil).Collection(p => p.SchoolclassCodes).Query().Load();
  // output statements omitted...
}

输出:

Books full query load
  IsLoaded = False
  Items in the pupil:
    1
    2
  Items in the context:
    1
    2
SchoolclassCodes full query load
  IsLoaded = False
  Items in the pupil:
  Items in the context:
    1
    2
    3

即使我们加载了所有相关数据,IsLoaded仍然是错误的,加载的数据仍然没有进入SchoolclassCodes。显然 Load()Query().Load() 不同。

查询方法的源代码

那么幕后发生了什么?可以在 CodePlex 上找到 EF6 的源代码。以下Query调用:

context.Entry(pupil).Collection(p => p.Books).Query()

可以追溯到以下代码片段,为清楚起见,我对其进行了编辑:

string sourceQuery = GenerateQueryText();
var query = new ObjectQuery<TEntity>(sourceQuery, _context, mergeOption);
AddQueryParameters(query);
return query;

这里TEntityBook_context是我们DbContext后面的ObjectContextsourceQuery是下面的EntitySQL 语句:

SELECT VALUE [TargetEntity]
FROM (SELECT VALUE x
      FROM [SchoolEntities].[Pupil_Books] AS x
      WHERE Key(x.[Pupil_Books_Source]) = ROW(@EntityKeyValue1 AS EntityKeyValue1)) AS [AssociationEntry]
INNER JOIN [SchoolEntities].[Books] AS [TargetEntity]
  ON Key([AssociationEntry].[Pupil_Books_Target]) = Key(Ref([TargetEntity]))

AddQueryParameters 之后,参数 @EntityKeyValue1 绑定到值 1,即 pupilId。所以上面的查询基本上是一样的:

context.Books.Where(s => s.Pupil.Id == pupil.Id)

也就是说,Query 方法只是构建一个查询,检索 BooksPupil.Id 匹配给定学生的 Id。它与将数据加载到 pupil.Books 无关。这也适用于 pupil.SchoolclassCodes.

的情况

Collection 加载方法的源代码

接下来我们检查以下方法调用:

context.Entry(pupil).Collection(p => p.Book).Load()

这个 Load 调用导致他关注(为清楚起见再次编辑):

var sourceQuery = CreateSourceQuery<TEntity>(mergeOption, out hasResults);
IEnumerable<TEntity> refreshedValues;
refreshedValues = sourceQuery.Execute(sourceQuery.MergeOption);
Merge(refreshedValues, mergeOption, true /*setIsLoaded*/);

如你所见,它构造了一个查询,和我们上面看到的查询一模一样,然后它执行查询并接收refreshedValues中的数据,最后将数据合并到导航属性,即pupil.Books

查询后加载方法的源代码

如果我们在 Query 之后执行 Load 会怎样?

context.Entry(pupil).Collection(p => p.Book).Query().Load()

这个LoadQueryableExtensionsclass中定义为一个扩展方法,很简单:

public static void Load(this IQueryable source)
{
    Check.NotNull(source, "source");

    var enumerator = source.GetEnumerator();
    try
    {
        while (enumerator.MoveNext())
        {
        }
    }
    finally
    {
        var asDisposable = enumerator as IDisposable;
        if (asDisposable != null)
        {
            asDisposable.Dispose();
        }
    }
}

是的,这次展示了完整的源代码;我没有编辑任何东西。没错,它实际上是一个空的 foreach,遍历所有已加载的项目并且绝对不对它们进行任何操作。除了已经完成的事情:这些项目被添加到上下文中,如果关系是 one-to-many,关系 fix-up 就会启动并修复关联。这是普查员工作的一部分。

再做一个实验:侧载

在上面我们看到 collection 的 Query 方法只是构造一个普通查询(一个 IQueryable)。当然,构造此类查询的方法不止一种。我们不必以 context.Entry(...).Collection(...) 开头。我们可以从头开始:

using (var context = new SchoolEntities()) {
  Console.WriteLine("Books side load");
  var pupil = context.Pupils.First();
  context.Books.Where(s => s.Pupil.Id == pupil.Id).Load();
  // output statements omitted...
}
using (var context = new SchoolEntities()) {
  Console.WriteLine("SchoolclassCodes side load");
  var pupil = context.Pupils.First();
  context.SchoolclassCodes.Where(s => s.Pupils.Select(t => t.Id).Contains(pupil.Id)).Load();
  // output statements omitted...
}

输出:

Books side load
  IsLoaded = False
  Items in the pupil:
    1
    2
  Items in the context:
    1
    2
SchoolclassCodes side load
  IsLoaded = False
  Items in the pupil:
  Items in the context:
    1
    2
    3

和实验三完全一样

更新 2

要删除many-to-many关系中的部分关联,官方推荐的方式是先Load所有相关的object,再删除关联。例如:

context.Entry(pupil).Collection(p => p.SchoolclassCodes).Load();
var code = pupil.SchoolclassCodes.Where(...).First();
pupil.SchoolclassCodes.Remove(code);
context.SaveChanges();

当然,这可能会从数据库中加载不需要的相关 object。如果这是不可取的,我们可以下拉到 ObjectContext 并使用 ObjectStateManager:

var code = context.Entry(pupil).Collection(p => p.SchoolclassCodes).Query().Where(...).First();
var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager;
objectStateManager.ChangeRelationshipState(pupil, code, p => p.SchoolclassCodes, EntityState.Deleted);
context.SaveChanges();

这样只会加载相关的 object。其实如果我们已经知道相关object的主键,连那个也可以去掉:

var code = new SchoolclassCode { Id = 1 };
context.SchoolclassCodes.Attach(code);
var objectStateManager = ((IObjectContextAdapter)context).ObjectContext.ObjectStateManager;
objectStateManager.ChangeRelationshipState(pupil, code, p => p.SchoolclassCodes, EntityState.Deleted);
context.SaveChanges();

不过要注意的是EF7 will remove ObjectContext,以后如果要迁移到EF7,就得修改上面的代码了