我注意到当我通过不同的“成员路径”导航到同一个实体对象时,我会得到一个不同的对象。 (我正在使用更改跟踪代理,因此我获得了一个不同的更改跟踪代理对象。)
这是一个显示我的意思的例子。
var joesInfo1 = context.People.Single(p => p.Name == "Joe").Info;
var joesInfo2 = context.People.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info;
尽管joesInfo1& joesInfo2引用DB中的相同记录(同一实体),它们是不同的对象。我认为实体框架确保在这些情况下使用相同的对象。
问题#1:这真的是这样吗?或者我的观察是错误的?
通过Include急切加载时出现此问题。例如,
IQueryable<Person> allPeople = null;
using(context)
{
allPeople = context.People
//.AsNoTracking()
.Include(p => p.Info)
.Include(p => p.Children)
.Include(p => p.Parent)
.ToList();
}
var joesInfo1 = allPeople.Single(p => p.Name == "Joe").Info; // OK, Info is already there because it was eagerly loaded
var joesInfo2 = allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info;
// ERROR: "Object context disposed...", Info is not in the Person object, even though the Person object refers to the same entity (Joe) as above.
因此,看起来要急切加载工作,您必须指定您将在程序中使用的所有可能的“成员访问路径”。在某些情况下,这是不可能的。因为您的Person对象可能在您的程序中浮动,并且可以在其上调用导航属性“Parent”或“Children”(并且它是父级/子级)。
问题2:如果没有指定您将在程序中使用的所有“成员访问路径”,有没有办法让它工作?
感谢。
解答:
所以,根据布比的回答,这就是我的结论。
如果使用AsNoTracking(),则可以获得不同的“实体对象”。 (换句话说,在上面的示例中,根据您到达“Joe”Person实体的路径,您可能会得到一个不同的对象。)如果您不使用AsNoTracking,则所有Joes将是同一个对象。
这意味着:
您可以急切地加载整个层次结构或递归对象图,并在上下文之外使用它。怎么样?只是不要使用AsNoTracking()。
答案 0 :(得分:3)
关于您的代码,在第二个问题中,您正在运行第一个查询(allPeople是IQueryable)
var joesInfo1 = allPeople.Single(p => p.Name == "Joe").Info; // OK, Info is already there because it was eagerly loaded
已经处理了上下文,因此无法运行。
无论如何,我想这是你的模特
[Table("People67")]
public class Person
{
public Person()
{
Children = new List<Person>();
}
public int Id { get; set; }
[MaxLength(50)]
public string Name { get; set; }
public virtual Info Info { get; set; }
public virtual ICollection<Person> Children { get; set; }
}
public class Info
{
public int Id { get; set; }
[MaxLength(50)]
public string Description { get; set; }
}
在种子数据库之后,这段代码可以工作(查看断言)
using (var context = new Context(GetConnection()))
{
var joes1 = context.People.Single(p => p.Name == "Joe");
var joes2 = context.People.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe");
Assert.IsTrue(object.ReferenceEquals(joes1, joes2);
Assert.IsTrue(object.ReferenceEquals(joes1.Info.GetType(), joes2.Info.GetType()));
Assert.IsTrue(object.ReferenceEquals(joes1.Info, joes2.Info));
}
所以关于你的第一个问题代理是相同的类型,参考是相同的 更深入一点,如果您查看查询
ExecuteDbDataReader==========
SELECT TOP 2
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Person_Id] AS [Person_Id],
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE 'Joe' = [Extent1].[Name]
ExecuteDbDataReader==========
SELECT TOP 2
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Person_Id] AS [Person_Id],
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE 'Joe''s Dad' = [Extent1].[Name]
ExecuteDbDataReader==========
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[Person_Id] AS [Person_Id],
[Extent1].[Info_Id] AS [Info_Id]
FROM [People67] AS [Extent1]
WHERE ([Extent1].[Person_Id] IS NOT NULL) AND ([Extent1].[Person_Id] = @EntityKeyValue1)
EntityKeyValue1 = 1
ExecuteDbDataReader==========
SELECT
[Extent2].[Id] AS [Id],
[Extent2].[Description] AS [Description]
FROM ( [People67] AS [Extent1]
INNER JOIN [Infoes] AS [Extent2] ON ([Extent1].[Info_Id] = [Extent2].[Id]))
WHERE ([Extent1].[Info_Id] IS NOT NULL) AND ([Extent1].[Id] = @EntityKeyValue1)
EntityKeyValue1 = 2
你可以理解EF合并内存中的实体(查看第三个查询)。
现在,更准确地说,如果向Person添加属性Parent_Id
,则此行为不会更改。如果EF应该知道Joe已经在内存中,那么第三个查询也会运行。
<强> =================== 强>
现在是第二部分
正如我在答案开头所说,你的代码根本不起作用,因为你在第一个查询中也访问了带有已处置上下文的IQueryable。
在这种情况下,我想这是你的代码。
List<Person> allPeople;
using (var context = new Context(GetConnection()))
{
allPeople = context.People
.Include(_ => _.Info)
.Include(_ => _.Children)
.ToList();
}
// This is an in memory query because to the previous ToList
// Take care of == because is an in memory case sensitive query!
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe").Info);
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info);
Assert.IsTrue(object.ReferenceEquals(allPeople.Single(p => p.Name == "Joe").Info, allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info));
如果您激活了探查器,您会看到EF在ToList()
后没有运行查询。
<强> =================== 强>
那么,什么不起作用?
插入AsNoTracking()
时有几件事情。
在这种情况下,EF行为是不同的,实体不在上下文中(不被跟踪),EF需要访问数据库以检索它应该在内存中的实体。
例如,此代码不起作用。
List<Person> allPeople;
using (var context = new Context(GetConnection()))
{
allPeople = context.People
.Include(_ => _.Info)
.Include(_ => _.Children)
.AsNoTracking()
.ToList();
}
// This throw an exception
Assert.IsNotNull(allPeople.Single(p => p.Name == "Joe's Dad").Children.Single(p => p.Name == "Joe").Info);
修改强>
您可以通过不同方式使用AsNoTracking解决您发现的不同问题。我不知道是否有“解决方案”
我通常会实现==
(以及Equals
,!=
,GetHashCode
等等)处理字符大小写(DBMS通常不区分大小写,所以==
也必须不区分大小写)避免'=='问题(对同一数据库实体的不同引用)
然后,如果我需要,我在内存中的实体缓存,我在内存中搜索实体而不是导航属性
最后,代码不像使用导航属性那么干净,但它有效(Knuth说,“优化是所有邪恶的根源”)。
答案 1 :(得分:0)
回答问题1: 是的,这样两个调用都会导致数据库的往返,并且据我所知导致不同的对象。只使用&#39;查找&#39;你可以防止多次往返。因为它与主键一起工作,EF将首先检查具有该主键的对象是否已经加载并返回它,否则查询数据库。 https://msdn.microsoft.com/en-us/library/jj573936(v=vs.113).aspx
回答问题2: 在您的示例中,调用&#39; Single&#39;这是在上下文处理之后。如果你将.ToList()添加到你的查询中,那也会有效,但这也意味着你要加载所有记录。