实体框架 - 同一实体的不同代理对象。并包含具有到同一目的地的多条路径的行为

时间:2017-07-28 23:07:35

标签: c# .net entity-framework entity-framework-6

我注意到当我通过不同的“成员路径”导航到同一个实体对象时,我会得到一个不同的对象。 (我正在使用更改跟踪代理,因此我获得了一个不同的更改跟踪代理对象。)

这是一个显示我的意思的例子。

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()。

2 个答案:

答案 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()添加到你的查询中,那也会有效,但这也意味着你要加载所有记录。