載入相關資料Loading Related Data

Entity Framework Core 可讓您在模型中使用導覽屬性來載入相關實體。Entity Framework Core allows you to use the navigation properties in your model to load related entities. 有三種常見的 O/RM 模式可用來載入相關資料。There are three common O/RM patterns used to load related data.

  • 積極式載入表示會從資料庫以初始查詢一部分的方式載入相關資料。Eager loading means that the related data is loaded from the database as part of the initial query.
  • 明確式載入表示會從資料庫於稍後以明確方式載入相關資料。Explicit loading means that the related data is explicitly loaded from the database at a later time.
  • 消極式載入表示會於存取導覽屬性時從資料庫以透明的方式載入相關資料。Lazy loading means that the related data is transparently loaded from the database when the navigation property is accessed.

提示

您可以在 GitHub 上檢視此文章的範例 (英文)。You can view this article's sample on GitHub.

積極式載入Eager loading

您可以使用 Include 方法來指定要包含於查詢結果中的相關資料。You can use the Include method to specify related data to be included in query results. 在下列範例中,於結果中所傳回部落格的 Posts 屬性將會填入相關文章。In the following example, the blogs that are returned in the results will have their Posts property populated with the related posts.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .ToList();
}

提示

Entity Framework Core 會將導覽屬性自動修正為先前已載入至內容執行個體的任何其他實體。Entity Framework Core will automatically fix-up navigation properties to any other entities that were previously loaded into the context instance. 因此,即使未明確包含導覽屬性的資料,如果先前已載入部分或所有相關的實體,則仍然可能會填入該屬性。So even if you don't explicitly include the data for a navigation property, the property may still be populated if some or all of the related entities were previously loaded.

您可以將來自多個關聯性的相關資料包含至單一查詢。You can include related data from multiple relationships in a single query.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .Include(blog => blog.Owner)
        .ToList();
}

包括多個層級Including multiple levels

您可以使用 ThenInclude 方法,透過關聯性向下切入以包含多個層級的相關資料。You can drill down through relationships to include multiple levels of related data using the ThenInclude method. 下列範例會載入所有部落格、其相關文章,以及每篇文章的作者。The following example loads all blogs, their related posts, and the author of each post.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
        .ToList();
}

注意

Visual Studio 的目前版本會提供不正確的程式碼自動完成選項,並可能會在於集合導覽屬性之後使用 ThenInclude 方法時,將正確的運算式標示為具有語法錯誤。Current versions of Visual Studio offer incorrect code completion options and can cause correct expressions to be flagged with syntax errors when using the ThenInclude method after a collection navigation property. 這是於 https://github.com/dotnet/roslyn/issues/8237 (英文) 所追蹤 IntelliSense 錯誤 (bug) 的症狀。This is a symptom of an IntelliSense bug tracked at https://github.com/dotnet/roslyn/issues/8237. 只要程式碼正確且可成功編譯,就能放心略過這些假性的語法錯誤。It is safe to ignore these spurious syntax errors as long as the code is correct and can be compiled successfully.

您可以將多個呼叫鏈結到 ThenInclude,以繼續包含更深層級的相關資料。You can chain multiple calls to ThenInclude to continue including further levels of related data.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
                .ThenInclude(author => author.Photo)
        .ToList();
}

您可以結合上述全部,以便在相同查詢中包含來自多個層級和多個根的相關資料。You can combine all of this to include related data from multiple levels and multiple roots in the same query.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
            .ThenInclude(author => author.Photo)
        .Include(blog => blog.Owner)
            .ThenInclude(owner => owner.Photo)
        .ToList();
}

您可能會想要針對所包括的其中一個實體包含多個相關實體。You may want to include multiple related entities for one of the entities that is being included. 例如,查詢 Blog 時,您會包括 Posts,接著想要同時包含 PostsAuthorTagsFor example, when querying Blogs, you include Posts and then want to include both the Author and Tags of the Posts. 若要執行此動作,您必須指定每個從根開始的包含路徑。To do this, you need to specify each include path starting at the root. 例如,Blog -> Posts -> AuthorBlog -> Posts -> TagsFor example, Blog -> Posts -> Author and Blog -> Posts -> Tags. 這不表示您將會收到多餘的聯結,在大部分情況下,EF 將會在產生 SQL 時合併聯結。This does not mean you will get redundant joins, in most cases EF will consolidate the joins when generating SQL.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Tags)
        .ToList();
}

衍生類型中的 IncludeInclude on derived types

您可以使用 IncludeThenInclude來包含只定義於衍生類型上導覽的相關資料。You can include related data from navigations defined only on a derived type using Include and ThenInclude.

假設有下列模型:Given the following model:

public class SchoolContext : DbContext
{
    public DbSet<Person> People { get; set; }
    public DbSet<School> Schools { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<School>().HasMany(s => s.Students).WithOne(s => s.School);
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Student : Person
{
    public School School { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    public List<Student> Students { get; set; }
}

身分為學生之所有人員的 School 導覽內容可以使用多個模式進行積極式載入:Contents of School navigation of all People who are Students can be eagerly loaded using a number of patterns:

  • 使用 castusing cast

    context.People.Include(person => ((Student)person).School).ToList()
    
  • 使用 as 運算子using as operator

    context.People.Include(person => (person as Student).School).ToList()
    
  • 使用 Include 的多載,其會接受類型 string 的參數using overload of Include that takes parameter of type string

    context.People.Include("Student").ToList()
    

略過的 IncludeIgnored includes

如果您變更查詢,使其不再傳回作為查詢開始的實體類型執行個體,則會略過 Include 運算子。If you change the query so that it no longer returns instances of the entity type that the query began with, then the include operators are ignored.

在下列範例中,Include 運算子會以 Blog 為基礎,但接著會使用 Select 運算子來變更查詢以傳回匿名類型。In the following example, the include operators are based on the Blog, but then the Select operator is used to change the query to return an anonymous type. 在此情況下,Include 運算子不會有任何作用。In this case, the include operators have no effect.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .Select(blog => new
        {
            Id = blog.BlogId,
            Url = blog.Url
        })
        .ToList();
}

根據預設,EF Core 將在略過 Include 運算子時記錄警告。By default, EF Core will log a warning when include operators are ignored. 如需檢視記錄輸出的詳細資訊,請參閱記錄See Logging for more information on viewing logging output. 您可以將略過 Include 運算子時的行為變更為擲回或不執行任何動作。You can change the behavior when an include operator is ignored to either throw or do nothing. 這通常會在 DbContext.OnConfiguring 中為您的內容設定選項時完成,或者,如果您使用 ASP.NET Core,則是在 Startup.cs 中完成。This is done when setting up the options for your context - typically in DbContext.OnConfiguring, or in Startup.cs if you are using ASP.NET Core.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFQuerying;Trusted_Connection=True;ConnectRetryCount=0")
        .ConfigureWarnings(warnings => warnings.Throw(CoreEventId.IncludeIgnoredWarning));
}

明確式載入Explicit loading

注意

此功能是在 EF Core 1.1 中引入。This feature was introduced in EF Core 1.1.

您可以透過 DbContext.Entry(...) API 來明確地載入導覽屬性。You can explicitly load a navigation property via the DbContext.Entry(...) API.

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    context.Entry(blog)
        .Collection(b => b.Posts)
        .Load();

    context.Entry(blog)
        .Reference(b => b.Owner)
        .Load();
}

您也可以透過執行會傳回相關實體的個別查詢,來明確地載入導覽屬性。You can also explicitly load a navigation property by executing a separate query that returns the related entities. 如果啟用變更追蹤,則 EF Core 在載入實體時將會自動設定新載入實體的導覽屬性來參考已經載入的任何實體,並將已載入實體的導覽屬性設定為參考新載入的實體。If change tracking is enabled, then when loading an entity, EF Core will automatically set the navigation properties of the newly-loaded entitiy to refer to any entities already loaded, and set the navigation properties of the already-loaded entities to refer to the newly-loaded entity.

您也可以取得表示導覽屬性內容的 LINQ 查詢。You can also get a LINQ query that represents the contents of a navigation property.

這可讓您執行像是透過相關實體來執行彙總運算子,而不需將其載入至記憶體之類的動作。This allows you to do things such as running an aggregate operator over the related entities without loading them into memory.

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    var postCount = context.Entry(blog)
        .Collection(b => b.Posts)
        .Query()
        .Count();
}

您也可以篩選要將哪些相關實體載入至記憶體。You can also filter which related entities are loaded into memory.

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    var goodPosts = context.Entry(blog)
        .Collection(b => b.Posts)
        .Query()
        .Where(p => p.Rating > 3)
        .ToList();
}

消極式載入Lazy loading

注意

此功能是在 EF Core 2.1 中引入。This feature was introduced in EF Core 2.1.

使用消極式載入的最簡單方式是安裝 Microsoft.EntityFrameworkCore.Proxies (英文) 套件,並呼叫 UseLazyLoadingProxies 來啟用它。The simplest way to use lazy-loading is by installing the Microsoft.EntityFrameworkCore.Proxies package and enabling it with a call to UseLazyLoadingProxies. 例如: For example:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseLazyLoadingProxies()
        .UseSqlServer(myConnectionString);

或在使用 AddDbContext 時:Or when using AddDbContext:

.AddDbContext<BloggingContext>(
    b => b.UseLazyLoadingProxies()
          .UseSqlServer(myConnectionString));

EF Core 接著將針對可覆寫的所有導覽屬性 (也就是說,它必須是 virtual 並位於可繼承的類別上) 啟用消極式載入。EF Core will then enable lazy loading for any navigation property that can be overridden--that is, it must be virtual and on a class that can be inherited from. 例如,在下列實體中,系統將會對 Post.BlogBlog.Posts 導覽屬性進行消極式載入。For example, in the following entities, the Post.Blog and Blog.Posts navigation properties will be lazy-loaded.

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public virtual Blog Blog { get; set; }
}

沒有 Proxy 的消極式載入Lazy loading without proxies

消極式載入 Proxy 的運作方式是將 ILazyLoader 服務插入至實體,如實體類型建構函式中所述。Lazy-loading proxies work by injecting the ILazyLoader service into an entity, as described in Entity Type Constructors. 例如: For example:

public class Blog
{
    private ICollection<Post> _posts;

    public Blog()
    {
    }

    private Blog(ILazyLoader lazyLoader)
    {
        LazyLoader = lazyLoader;
    }

    private ILazyLoader LazyLoader { get; set; }

    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts
    {
        get => LazyLoader.Load(this, ref _posts);
        set => _posts = value;
    }
}

public class Post
{
    private Blog _blog;

    public Post()
    {
    }

    private Post(ILazyLoader lazyLoader)
    {
        LazyLoader = lazyLoader;
    }

    private ILazyLoader LazyLoader { get; set; }

    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog
    {
        get => LazyLoader.Load(this, ref _blog);
        set => _blog = value;
    }
}

這並不需要實體類型為可繼承的,或是導覽屬性為虛擬的,並且可讓使用 new 建立的實體執行個體可以在附加到內容之後進行消極式載入。This doesn't require entity types to be inherited from or navigation properties to be virtual, and allows entity instances created with new to lazy-load once attached to a context. 不過,它需要對 ILazyLoader 服務的參考,這在 Microsoft.EntityFrameworkCore.Abstractions 套件中定義。However, it requires a reference to the ILazyLoader service, which is defined in the Microsoft.EntityFrameworkCore.Abstractions package. 此套件包含最基本的型別集,因此相依於它的影響很小。This package contains a minimal set of types so that there is very little impact in depending on it. 不過,若要完全避免對實體型別中任何 EF Core 套件的相依性,您可以將 ILazyLoader.Load 方法插入為委派。However, to completely avoid depending on any EF Core packages in the entity types, it is possible to inject the ILazyLoader.Load method as a delegate. 例如: For example:

public class Blog
{
    private ICollection<Post> _posts;

    public Blog()
    {
    }

    private Blog(Action<object, string> lazyLoader)
    {
        LazyLoader = lazyLoader;
    }

    private Action<object, string> LazyLoader { get; set; }

    public int Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts
    {
        get => LazyLoader.Load(this, ref _posts);
        set => _posts = value;
    }
}

public class Post
{
    private Blog _blog;

    public Post()
    {
    }

    private Post(Action<object, string> lazyLoader)
    {
        LazyLoader = lazyLoader;
    }

    private Action<object, string> LazyLoader { get; set; }

    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public Blog Blog
    {
        get => LazyLoader.Load(this, ref _blog);
        set => _blog = value;
    }
}

上述程式碼會使用 Load 擴充方法,來更簡潔地使用委派:The code above uses a Load extension method to make using the delegate a bit cleaner:

public static class PocoLoadingExtensions
{
    public static TRelated Load<TRelated>(
        this Action<object, string> loader,
        object entity,
        ref TRelated navigationField,
        [CallerMemberName] string navigationName = null)
        where TRelated : class
    {
        loader?.Invoke(entity, navigationName);

        return navigationField;
    }
}

注意

消極式載入委派的建構函式參數必須稱為 "lazyLoader"。The constructor parameter for the lazy-loading delegate must be called "lazyLoader". 預計將於未來版本中推出使用與此不同之名稱的設定。Configuration to use a different name than this is planned for a future release.

由於 EF Core 將會自動修正導覽屬性,您的物件圖形最後可能會出現循環。Because EF Core will automatically fix-up navigation properties, you can end up with cycles in your object graph. 例如,載入部落格與其相關文章將會產生參考文章集合的部落格物件。For example, loading a blog and its related posts will result in a blog object that references a collection of posts. 那些文章都會具有針對該部落格的參考。Each of those posts will have a reference back to the blog.

某些序列化架構並不允許這類循環。Some serialization frameworks do not allow such cycles. 例如,Json.NET 將會在遇到循環時擲回下列例外狀況。For example, Json.NET will throw the following exception if a cycle is encountered.

Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Blog' with type 'MyApplication.Models.Blog' (針對具有類型 'MyApplication.Models.Blog' 的屬性 'Blog' 偵測到自我參考迴圈)。Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Blog' with type 'MyApplication.Models.Blog'.

如果您使用 ASP.NET Core,則可將 Json.NET 設定為略過它在物件圖形中所找到的循環。If you are using ASP.NET Core, you can configure Json.NET to ignore cycles that it finds in the object graph. 這可在 Startup.csConfigureServices(...) 方法中完成。This is done in the ConfigureServices(...) method in Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddMvc()
        .AddJsonOptions(
            options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
        );

    ...
}