c# - Entity Framework - 同一实体的不同代理对象。并包括具有多条路径到同一目的地的行为

标签 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 指的是数据库中的同一条记录(同一个实体),但它们是不同的对象。我认为 Entity Framework 确保在这些情况下使用相同的对象。

问题 #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 对象可能在您的程序中四处 float ,并且导航属性“Parent”或“Children”可以在它(以及它的 parent / child )上被调用任意多次。

问题 #2:有没有什么方法可以在不指定您将在程序中采用的所有“成员访问路径”的情况下让它工作?

谢谢。


回答:

所以,这是我根据 bubi 的回答得出的结论。

如果您使用 AsNoTracking(),则可以获得不同的“实体对象”。 (换句话说,在上面的示例中,根据您到达“Joe”Person 实体所采用的路径,您可能会获得不同的对象。)如果您不使用 AsNoTracking,则所有 Joes 都将是同一个对象。

这是什么意思:

您可以急切地加载整个分层或递归对象图并在上下文之外使用它。如何?只是不要使用 AsNoTracking()。

最佳答案

关于您的代码,在第二个问题中您正在运行第一个查询(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 说,“优化是万恶之源”)。

关于c# - Entity Framework - 同一实体的不同代理对象。并包括具有多条路径到同一目的地的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45383568/

相关文章:

c# - 如何解决 Microsoft SQL Server。错误 233。提供商 : SSL Provider

c# - 在 C# 中使用按钮/事件显示新表单并隐藏现有表单

c# - 哪个 .NET 类代表 WebForms 的主要 CONTROLLER 类?

c# - MySql 数据库中的 EF 重命名列

c# - DataReader + MySql Connector + Dispose

javascript - 如何通过POST请求在[WebMethod]上传递参数

c# - 加载数据内文件 : can't ignore 1st row while skipping columns?

c# - 将 cookie 从 CookieContainer 写入 IE cookie 存储

c# - Entity Framework 中存储函数的使用

Linq To Entities 获取列表中的倒数第二个条目