如何在LINQ一对多关系中高效获取关联集合的计数
问题描述
我有一个包含UsersMarked集合的Book实体:
public class Book { [Key] public int Id { get; set; } [Required] public string Title { get; set; } = string.Empty; public int Date { get; set; } public int AuthorId { get; set; } public Author? Author { get; set; } public string Cover { get; set; } = string.Empty; public string? Description { get; set; } public List<User> UsersMarked { get; set; } = new(); public BookGenre Genre { get; set; } }
需要将其转换为包含ReadersCount(标记书籍的用户数量)的BookPageDto:
public record BookPageDto( string Title, int Date, int ReadersCount, string Cover, BookGenre Genre, string Description);
目前我通过Include加载整个UsersMarked集合来获取计数:
public async Task<Book?> GetBookByTitleAsync(string title) { return await _context.Books .Include(b=>b.UsersMarked) .FirstOrDefaultAsync(b => b.Title.ToLower() == title); }
这种方式会把整个集合加载到内存中,内存效率很低。而且无法直接用_context.Books.Include(b=>b.UsersMarked.Count())(会报错),想知道除了给Book实体添加Count字段外,还有哪些方法可以获取UsersMarked的计数。
解决方案
1. 投影查询直接映射到DTO(推荐)
直接在查询时通过Count()统计关联用户数,同时投影到目标DTO,EF Core会生成高效的SQL,在数据库层面完成计数,不会加载整个UsersMarked集合:
public async Task<BookPageDto?> GetBookPageDtoByTitleAsync(string title) { return await _context.Books .Where(b => b.Title.ToLower() == title) .Select(b => new BookPageDto( b.Title, b.Date, b.UsersMarked.Count(), b.Cover, b.Genre, b.Description ?? string.Empty)) .FirstOrDefaultAsync(); }
这种方式一步完成查询和DTO映射,内存占用最少,性能最优。
2. 利用EF Core的计数导航属性(EF Core 5+)
可以通过Fluent API配置一个数据库计算列,让数据库自动维护计数:
首先在Book实体中添加属性:
public int ReadersCount { get; set; }
然后在DbContext的OnModelCreating方法中配置计算列:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>() .Property(b => b.ReadersCount) .HasComputedColumnSql("(SELECT COUNT(*) FROM [UsersMarked] WHERE [UsersMarked].[BookId] = [Book].[Id])"); }
此后查询Book实体时,ReadersCount会直接从数据库获取统计值,不需要手动计数,也不会加载关联集合。不过这种方式依赖数据库支持计算列,适合频繁需要统计的场景。
3. 单独查询计数
如果已经获取了Book实体但未加载UsersMarked,可以单独发起计数查询:
public async Task<int> GetUsersMarkedCountAsync(int bookId) { // 假设是多对多关系,直接查询中间表效率更高 return await _context.Set<BookUser>() // 替换为你的多对多中间表实体 .CountAsync(bu => bu.BookId == bookId); // 如果没有显式中间表,也可以通过User侧查询 // return await _context.Users.CountAsync(u => u.MarkedBooks.Any(b => b.Id == bookId)); }
这种方式会多一次数据库请求,适合零散需要计数的场景,不如投影方式高效。
内容的提问来源于stack exchange,提问作者JackFord
相关产品推荐
相关产品推荐

