簡體   English   中英

實體框架 - 同一實體的不同代理對象。 並包含具有到同一目的地的多條路徑的行為

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

我注意到當我通過不同的“成員路徑”導航到同一個實體對象時,我得到了一個不同的對象。 (我正在使用更改跟蹤代理,因此我獲得了一個不同的更改跟蹤代理對象。)

這是一個顯示我的意思的例子。

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:如果沒有指定您將在程序中使用的所有“成員訪問路徑”,有沒有辦法讓它工作?

謝謝。


回答:

所以,根據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));

如果激活分析器,您將看到在ToList()之后EF不運行查詢。

===================
那么,什么不起作用? 如果插入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通常不區分大小寫,所以也==必須不區分大小寫)以避免'=='問題(對同一個db的不同引用)實體)。
然后,如果我需要,我緩存內存中的實體,我在內存中搜索實體而不是導航屬性。
最后,代碼不像使用導航屬性那么干凈,但它有效(Knuth說,“優化是所有邪惡的根源”)。

回答問題1:是的,這樣兩個調用都會導致數據庫往返,據我所知,導致不同對象的結果。 只使用“查找”,您可以防止多次往返。 因為它與主鍵一起工作,EF將首先檢查具有該主鍵的對象是否已加載並返回它,否則查詢數據庫。 https://msdn.microsoft.com/en-us/library/jj573936(v=vs.113).aspx

對問題2的回答:在您的示例中,在調用上下文之后調用“Single”時會調用數據庫。 如果你將.ToList()添加到你的查詢中它會起作用,但這也意味着你要加載所有記錄。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM