带有 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
,其中 Id
是 1
,AsyncLoad
应该恰好将一个 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
?
答案是:SchoolclassCode
和Pupil
的关系是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; }
}
Pupil
和SchoolclassCode
的关系是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
方法将相关数据直接加载到导航属性中。结果显示:
- collection的
IsLoaded
属性设置为true
;
- 加载的数据可以在导航属性中找到(即
pupil.Books
和pupil.SchoolclassCodes
);
- 加载的数据也可以在上下文中找到(即
context.Books.Local
和context.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
看出区别了吗?
在这两种情况下,IsLoaded
现在都是 false
;
- 加载的数据仍然进入上下文;
- 但是,加载的数据在
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;
这里TEntity
是Book
,_context
是我们DbContext
后面的ObjectContext
,sourceQuery
是下面的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
,即 pupil
的 Id
。所以上面的查询基本上是一样的:
context.Books.Where(s => s.Pupil.Id == pupil.Id)
也就是说,Query
方法只是构建一个查询,检索 Books
与 Pupil.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()
这个Load
在QueryableExtensions
class中定义为一个扩展方法,很简单:
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,就得修改上面的代码了
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
,其中 Id
是 1
,AsyncLoad
应该恰好将一个 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
?
答案是:SchoolclassCode
和Pupil
的关系是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 asToList
,Load
, etc. TheQuery
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; }
}
Pupil
和SchoolclassCode
的关系是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
方法将相关数据直接加载到导航属性中。结果显示:
- collection的
IsLoaded
属性设置为true
; - 加载的数据可以在导航属性中找到(即
pupil.Books
和pupil.SchoolclassCodes
); - 加载的数据也可以在上下文中找到(即
context.Books.Local
和context.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
看出区别了吗?
-
在这两种情况下,
IsLoaded
现在都是false
;- 加载的数据仍然进入上下文;
- 但是,加载的数据在
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;
这里TEntity
是Book
,_context
是我们DbContext
后面的ObjectContext
,sourceQuery
是下面的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
,即 pupil
的 Id
。所以上面的查询基本上是一样的:
context.Books.Where(s => s.Pupil.Id == pupil.Id)
也就是说,Query
方法只是构建一个查询,检索 Books
与 Pupil.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()
这个Load
在QueryableExtensions
class中定义为一个扩展方法,很简单:
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,就得修改上面的代码了