简体   繁体   English

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

[英]Entity Framework - Different proxy objects for the same entity. And Include behavior with multiple paths to same destination

I noticed that when I navigate to the same entity object via a different "member path" I get a different object. 我注意到当我通过不同的“成员路径”导航到同一个实体对象时,我得到了一个不同的对象。 (I'm using change-tracking proxies, so I get a different change-tracking proxy object.) (我正在使用更改跟踪代理,因此我获得了一个不同的更改跟踪代理对象。)

Here is an example to show what I mean. 这是一个显示我的意思的例子。

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;

Even though joesInfo1 & joesInfo2 refer to the same record in the DB (the same entity), they are different objects. 即使joesInfo1和joesInfo2引用DB(同一实体)中的相同记录,它们也是不同的对象。 I thought that Entity Framework made sure to use the same object in these cases. 我认为实体框架确保在这些情况下使用相同的对象。

Question #1: Is this really how it is? 问题1:这真的是这样吗? Or is my observation wrong? 或者我的观察错了?

This is a problem when eager loading via Include. 通过Include进行急切加载时,这是一个问题。 For example, 例如,

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.

So, it looks like to get eager loading to work, you have to specify all possible "member access paths" that you will take in your program. 因此,看起来急于加载工作,您必须指定您将在程序中使用的所有可能的“成员访问路径”。 This is not possible in some cases like this one. 在某些情况下,这是不可能的。 Because your Person object might be floating around in your program and the navigation properties "Parent" or "Children" could be called on it (and it's parents/children) any number of times. 因为您的Person对象可能在您的程序中浮动,并且可以在其上调用导航属性“Parent”或“Children”(以及它的父/子)任意次。

Question #2: Is there any way to get this to work without specifying all of the "member access paths" that you will take in your program? 问题2:如果没有指定您将在程序中使用的所有“成员访问路径”,有没有办法让它工作?

Thanks. 谢谢。


ANSWER: 回答:

So, here's what I have concluded, based on bubi's answer. 所以,根据bubi的回答,这是我得出的结论。

It is possible to get different "entity objects" if you use AsNoTracking(). 如果使用AsNoTracking(),则可以获得不同的“实体对象”。 (In other words, in the example above, depending on what path you take to get to the "Joe" Person entity, it's possible that you will get a different object.) If you don't use AsNoTracking all the Joes will be the same object. (换句话说,在上面的示例中,根据您到达“Joe”Person实体的路径,您可能会得到一个不同的对象。)如果您不使用AsNoTracking,则所有Joes将是同一个对象。

Here is what this means: 这意味着什么:

You CAN eagerly load a whole hierarchical or recursive object graph and use it outside of a context. 您可以急切地加载整个分层或递归对象图,并在上下文之外使用它。 How? 怎么样? JUST DON'T USE AsNoTracking(). 只是不要使用AsNoTracking()。

About your code, in the second question you are running the first query (allPeople is an IQueryable) 关于你的代码,在第二个问题中你正在运行第一个查询(allPeople是一个IQueryable)

var joesInfo1 = allPeople.Single(p => p.Name == "Joe").Info;  // OK, Info is already there because it was eagerly loaded

with the context already disposed so it won't run. 上下文已经处理好,所以它不会运行。

Anyway, I suppose this is your model 无论如何,我想这是你的模特

[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; }
}

After seed the database this code works (look at the assertion) 种子数据库后这个代码工作(看断言)

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));
}

so about your first question proxies are of the same type and the reference is the same. 所以关于你的第一个问题代理是相同的类型,参考是相同的。
A little bit deeper, if you have a look to the queries 如果您查看查询,请更深入一点

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

you can understand that EF merge the entities in memory (look at the third query). 你可以理解EF合并内存中的实体(看第三个查询)。

Now, to be more precise, this behaviour does not change if you add also a property Parent_Id to Person. 现在,更准确地说,如果向Person添加属性Parent_Id ,则此行为不会更改。 The third query is run also if EF should know that Joe is already in memory. 如果EF应该知道Joe已经在内存中,则运行第三个查询。

=================== ===================
Now the second part 现在是第二部分

As I said at the beginning of the answer, your code does not work at all because you are accessing to an IQueryable with a disposed context also in the first query. 正如我在答案开头所说,您的代码根本不起作用,因为您在第一个查询中也访问了带有已处置上下文的IQueryable。

In this case I suppose this is your code. 在这种情况下,我想这是你的代码。

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));

If you activate the profiler you will see that EF does not run query after the ToList() . 如果激活分析器,您将看到在ToList()之后EF不运行查询。

=================== ===================
So, what does not work? 那么,什么不起作用? Several things if you insert AsNoTracking() . 如果插入AsNoTracking()有几件事情。 In that case the EF behaviour is different, the entities are not in the context (are not tracked) and EF needs to access database to retrieve entities that it should have in memory. 在这种情况下,EF行为是不同的,实体不在上下文中(不被跟踪),EF需要访问数据库以检索它应该在内存中的实体。

For example, this code does not work. 例如,此代码不起作用。

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);

EDIT 编辑
You can solve the different issues you find using AsNoTracking in different ways. 您可以通过不同方式使用AsNoTracking解决您发现的不同问题。 I don't know if there is "the solution". 我不知道是否有“解决方案”。
I usually implement == (and Equals , != , GetHashCode and so on) taking care of character casing (DBMSs often are case insensitive so also == must be case insensitive) to avoid '==' issues (different references to same db entity). 我通常实现== (和Equals!=GetHashCode等)处理字符大小写(DBMS通常不区分大小写,所以也==必须不区分大小写)以避免'=='问题(对同一个db的不同引用)实体)。
Then, if I need, I cache in entities in memory and I search for entities in memory instead of navigating properties. 然后,如果我需要,我缓存内存中的实体,我在内存中搜索实体而不是导航属性。
At the end the code is not so clean as using navigation properties but it works(Knuth said, "optimization is the root of all evils"). 最后,代码不像使用导航属性那么干净,但它有效(Knuth说,“优化是所有邪恶的根源”)。

Answer to question 1: Yes this way both calls cause a round trip to the database and as far as i know result in different objects. 回答问题1:是的,这样两个调用都会导致数据库往返,据我所知,导致不同对象的结果。 Only using 'Find' you can prevent multiple round trips. 只使用“查找”,您可以防止多次往返。 Because it works with primary keys EF will first check if an object with that primary key is already loaded and return it, otherwise query the db. 因为它与主键一起工作,EF将首先检查具有该主键的对象是否已加载并返回它,否则查询数据库。 https://msdn.microsoft.com/en-us/library/jj573936(v=vs.113).aspx https://msdn.microsoft.com/en-us/library/jj573936(v=vs.113).aspx

Answer to question 2: In your example the call to the database happens when calling 'Single' which is after the context was disposed. 对问题2的回答:在您的示例中,在调用上下文之后调用“Single”时会调用数据库。 It would work if you add .ToList() to your query but that would also mean you're loading all records. 如果你将.ToList()添加到你的查询中它会起作用,但这也意味着你要加载所有记录。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM