使用LoadAsync()的Query()虽然应该返回实体但不返回实体

时间:2015-06-25 10:05:43

标签: c# entity-framework entity-framework-6.1

context.Pupils.Attach(pupil);

pupil.SchoolclassCodes集合是空的,但必须有一个schoolclassCode,因为在底部的LoadAsync方法是带有Id I查询的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();

为什么底部LoadAsync 有效但顶部LoadAsync

更新

很抱歉有关pupil.SchoolclassCodeId的混淆,这是[NotMappedAttribute]

这是关系。

Pupil N有M SchoolclassCodes。

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

最后一个LoadAsync正如我所说,关系设置没有问题。

问题是第一个LoadAsync不能如上所述工作!

LAZY装载完全禁用!我在任何地方都没有使用虚拟道具!

更新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; }
}

1 个答案:

答案 0 :(得分:2)

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 = falseforeach完全没有任何内容!为什么呢?

好吧,首先AsyncLoad不适用于Collection(p => p.SchoolclassCodes),而是IQueryableIsLoaded派生,因此false应为SchoolclassCode,这是可以理解的

但是一个foreach (var code in context.SchoolclassCodes.Local) Console.WriteLine(" " + code.Id); 确实被加载到上下文中:

foreach

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

答案是:PupilSchoolclassCode之间的关系是多对多的。在这种情况下,实体框架不会进行关系修正,即自动将Pupil.SchoolclassCodes添加到Query,因此您不会在那里看到它。如果你真的想要修复关系,你必须手动完成。

更新1

引自MSDN

  

ToList方法提供对实体框架在加载相关实体时将使用的基础查询的访问。然后,您可以在执行查询之前使用LINQ将过滤器应用于查询,方法是调用LINQ扩展方法,例如LoadQuery等。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; } } 方法可以同时使用引用和集合导航属性,但对于可用于仅加载集合的一部分的集合最有用。

有点令人困惑。这似乎与我的论点相矛盾,但事实并非如此。实际上,在上面引用了“#34; load&#34;意味着加载到上下文中,而不是加载到导航属性中,所以MSDN和我的回答都是正确的。为了证明我的主张,让我们从几个实验开始,然后我们将深入研究源代码。

模型

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

Pupil

SchoolclassCodePupil之间的关系与以前一样是多对多关系,Book与新添加的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)

数据

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

Load

实验1:直接加载

我们将相关数据直接加载到导航属性中。为简单起见,我们使用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,另一部分用于Load。使用两个上下文来确保这两个部分不会相互干扰。我们使用集合的IsLoaded方法将相关数据直接加载到导航属性中。结果表明:

  1. 该集合的true属性设置为pupil.Books;
  2. 可以在导航属性中找到加载的数据(例如pupil.SchoolclassCodescontext.Books.Local);
  3. 也可以在上下文中找到已加载的数据(例如context.SchoolclassCodes.LocalQuery)。
  4. 实验2:带查询的部分加载

    我们使用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)...

    大部分代码与实验1相同;请注意以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 开头的行。输出:

    IsLoaded

    看到区别?

      在这两种情况下,
    1. false现在都是SchoolclassCodes;
    2. 加载的数据仍会进入上下文;
    3. 但是,加载的数据不会进入Books案例中的导航属性,而会进入Books案例。
    4. 差异是由关系类型引起的:SchoolclassCodes是一对多,而Query是多对多。实体框架以不同的方式处理这两种类型。

      实验3:带有查询的完全加载

      那么如果我们在没有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()Query不同。

      查询方法的源代码

      那么引擎盖下发生了什么? EF6的源代码可以在CodePlex找到。以下context.Entry(pupil).Collection(p => p.Books).Query() 电话:

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

      可以追溯到下面的代码片段,为了清楚起见,我编辑了这段代码:

      TEntity

      此处Book_contextObjectContextDbContext后面的sourceQuerySELECT 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])) 为以下实体SQL语句:< / p>

      AddQueryParameters

      @EntityKeyValue1后,参数1绑定到值Id,即pupil的{​​{1}}。所以上面的查询基本上与:

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

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

      的情况

      集合的加载方法

      的源代码

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

      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

      查询后加载方法的源代码

      如果我们在Load之后Query做了什么?

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

      Load被定义为QueryableExtensions课程中的扩展方法,而且非常简单:

      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,循环遍历所有加载的项目,并且对它们完全没有任何作用。除了已经完成的事情:这些项目被添加到上下文中,如果关系是一对多,则关系修复启动并修复关联。这是调查员工作的一部分。

      再做一次实验:侧载

      在上面我们看到集合的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
      

      与实验3完全相同。

      更新2

      要删除多对多关系中的部分关联,官方建议的方法是首先Load所有相关对象,然后删除关联。例如:

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

      当然,这可能会从数据库中加载不需要的相关对象。如果这是不合需要的,我们可以下拉到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();
      

      这样只加载相关的相关对象。事实上,如果我们已经知道相关对象的主键,那么即使可以消除它:

      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,则必须修改上述代码。