1. 程式人生 > 其它 >EF Core 的關聯查詢

EF Core 的關聯查詢

0 前言

本文會列舉出 EF Core 關聯查詢的方法:

在第一、二、三節中,介紹的是 EF Core 的基本能力,在實體中配置好關係,即可使用,且其使用方式,與程式設計思維吻合,是本文推薦的方式。

第四節中,將介紹 Linq 語句的兩種關聯查詢方式:分別是 lambda 方式和 query 方式。


1 概述

資料庫中,表與表之間可能是有一定關聯關係的,在查詢資料過程中,我們經常會用到關聯查詢(常見的關聯查詢有如:inner join、left join 等)。

而在程式中,使用 EF Core 寫關聯查詢語句是比較難寫的(或者說大部分 ORM 工具都是如此)。

其實 EF Core 提供了關係的配置,通過簡單一些設定,可以讓我們以程式碼的思維,去處理這些有關聯關係的資料。

下面舉個栗子:

官方例子的 Blog 和 Post 是一對多的關係,如果要查出某個 Blog 下的 Post,正常在資料庫中,我們會寫連線語句,大概如下:

SELECT b.BlogId, b.Url, p.PostId, p.Title, p.Context
FROM Blog b
LEFT JOIN Post p ON b.BlogId = p.BlogId
WHERE b.BlogId = 1

通過左連線(left join)進行關聯查詢。

而在 EF Core 中,如果我們建立起關係,以及配置好相應的導航屬性,可以直接將實體關聯起來,通過實體操作與實體關聯的其他實體(如官方的例子,通過 Blog 操作與 Blog 關聯的 Post):

var blog = db.Blogs.Include(b => b.Posts) // 關聯 Post
    .Where(t => t.BlogId == 1)            // 查出 BlogId = 1 的記錄
    .FirstOrDefault();                    // 查出第一條記錄

var url = blog.Url;                       // 獲取 blog 的 url 屬性
var postCount = blog.Posts.Count;         // 獲取與 Blog 關聯的 Post 的數量
var title = blog.Posts[0].Title;

這種關聯關係,在程式中操作,其實是很方便的。


2 基本實現

下面將會以一個簡單的例子實現,具體可以檢視官方關係的內容。

2.1 配置導航屬性

如下面,將會建立 Blog 和 Post 之間一對多的關係:

public class Blog // 主體實體
{
    public int BlogId { get; set; } // 主體鍵
    public string Url { get; set; }

    public List<Post> Posts { get; set; } // 集合導航屬性
}

public class Post // 依賴實體
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; } // 外來鍵
    public Blog Blog { get; set; } // 引用導航屬性
}

Post.BlogBlog.Posts 的反向導航屬性(反之亦然)

2.2 新增 DbSet

在自定義的 DbContext 中新增相應對應 DbSet:

public class EDbContext : DbContext
{
    public virtual DbSet<Blog> Blogs { get; set; }
    public virtual DbSet<Post> Posts { get; set; }

    public EDbContext() { }

    public EDbContext(DbContextOptions<EDbContext> options) : base(options) { }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseSqlServer("server=localhost;database=EfCoreRelationship;uid=sa;pwd=Qwe123456;");
        base.OnConfiguring(options);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

2.3 查詢

如此即可進行查詢:

var blog = db.Blogs.Include(b => b.Posts) // 關聯 Post
    .Where(t => t.BlogId == 2)            // 查出 BlogId = 2 的記錄
    .FirstOrDefault();                    // 查出第一條記錄

var url = blog.Url;                       // 獲取 blog 的 url 屬性
var postCount = blog.Posts.Count;         // 獲取與 Blog 關聯的 Post 的數量
var title = blog.Posts[0].Title;

3 補充

這一節,將是對基本實現的一些內容的補充。

3.1 手動配置關係

基本實現中,沒有在自定義的 DbContext 中明確配置關係,可以如下配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts);
}

3.2 陰影屬性(shadow property)

如果依賴實體(如例子中 Post 實體)上,沒有明確定義外來鍵(如例子中 BlogId),具體可以檢視陰影和索引器屬性

public class Blog // 主體實體
{
    public int BlogId { get; set; } // 主體鍵
    public string Url { get; set; }

    public List<Post> Posts { get; set; } // 集合導航屬性
}

public class Post // 依賴實體
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    // 自動引入 BlogId 陰影屬性
    public Blog Blog { get; set; } // 引用導航屬性
}

3.3 級聯刪除

參考級聯刪除

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .OnDelete(DeleteBehavior.Cascade);
}

DeleteBehavior 列舉值及其定義,可以參考DeleteBehavior 列舉

3.4 補充:載入相關資料

參考載入相關資料

3.4.1 預先載入

查詢的時候,使用 Include 方法。

var blogs = db.Blogs.Include(b => b.Posts) // 關聯 Post
    .ToList(); 

包含多個層級,使用 ThenInclude

var blogs = db.Blogs.Include(blog => blog.Posts)
    .ThenInclude(post => post.Author)
    .ToList(); 

3.4.2 顯式載入

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

3.4.3 延遲載入

相關資料的延遲載入


4 Linq 語句實現關聯查詢

4.1 Lambda 方式

lambda 方式實現 EF Core 左連線查詢(left join),使用 SelectMany 方法:

版本1:

var blogs = _db.Set<Blog>()
            .SelectMany(b => _db.Set<Post>().Where(p => b.BlogId == p.BlogId).DefaultIfEmpty(), 
                        (b, ps) => new { b.Url, ps.Title })
            .ToList();

版本2:

var blogs = _db.Set<Blog>()
            .GroupJoin(_db.Set<Post>(),
                        b => b.BlogId,
                        p => p.BlogId, (b, p) => new { b, p })
            .SelectMany(n => n.p.DefaultIfEmpty(), 
                        (n, p) => new { n.b.Url, p!.Title })
            .ToList();

兩個版本的 sql 語句(都是一樣的):

SELECT [b].[Url], [p].[Title]
FROM [Blog] AS [b]
LEFT JOIN [Post] AS [p] ON [b].[BlogId] = [p].[BlogId]

4.2 Query 方式

兩表關聯 query 方式基本寫法:

var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
    			on b.BlogId equals p.BlogId
            select new { b, p };

其他寫法(實際上是基於 SelectMany 方法):

var query = from b in context.Set<Blog>()
            from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId)
            select new { b, p };

var query2 = from b in context.Set<Blog>()
             from p in context.Set<Post>().Where(p => b.BlogId == p.BlogId).DefaultIfEmpty()
             select new { b, p };

對應的 sql 語句:

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
INNER JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url], [p].[PostId], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Rating], [p].[Title]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[BlogId] = [p].[BlogId]

參考來源

EF Core 官方文件:關係

EF Core 官方文件:複雜查詢運算子