仓储模式下EF Core外键导航属性返回空的问题及方案探讨
EF Core导航属性加载问题:Select投影与Include的区别及最优方案
问题描述
我基于EF Core实现了泛型仓储基类RepositoryBase,实体Localitate包含外键JudetId和导航属性judet,但查询时导航属性始终返回空。于是我添加了GetLocalitatiBasicAsync方法,通过Select显式指定获取导航属性judet,想请教:
- 这种方案是否为最优解?
- 该方式与使用
Include加载导航属性的区别是什么?
现有代码实现
仓储基类RepositoryBase<T>
using mediere_API.DataLayer.Entities; using mediere_API.DataLayer.Repository.Interfaces; using Microsoft.EntityFrameworkCore; namespace mediere_API.DataLayer.Repository.Implementations { public class RepositoryBase<T> : IDisposable, IRepositoryBase<T> where T : BaseEntity, new() { private readonly DbContext _db; private readonly DbSet<T> _dbSet; protected RepositoryBase(EfDbContext context) { _db = context; _dbSet = context.Set<T>(); } public async Task<IList<T>> GetAll(bool asNoTracking = false, bool includeDeleted = false) { var query = includeDeleted ? _dbSet : _dbSet.Where(entity => entity.DeletedAt == null); return asNoTracking ? await query.AsNoTracking().ToListAsync() : await query.ToListAsync(); } public async Task<T> GetById(int id, bool asNoTracking = false, bool includeDeleted = false) { return includeDeleted ? await GetRecords(asNoTracking).FirstOrDefaultAsync(e => e.Id == id) : await GetRecords(asNoTracking).FirstOrDefaultAsync(e => e.Id == id && e.DeletedAt == null); } public virtual void Insert(T record) { if (_db.Entry(record).State == EntityState.Detached) { _db.Attach(record); _db.Entry(record).State = EntityState.Added; } } public virtual void Update(T record) { if (_db.Entry(record).State == EntityState.Detached) _db.Attach(record); _db.Entry(record).State = EntityState.Modified; } public void Delete(T record) { if (record != null) { record.DeletedAt = DateTime.UtcNow; Update(record); } } public void Dispose() { _db?.Dispose(); } protected IQueryable<T> GetRecords(bool asNoTracking = false, bool includeDeleted = false) { var result = includeDeleted ? _dbSet : _dbSet.Where(e => e.DeletedAt == null); return asNoTracking ? result.AsNoTracking() : result; } } }
实体Localitate
using mediere_API.Dtos; using System.ComponentModel.DataAnnotations; namespace mediere_API.DataLayer.Entities { public class Localitate : BaseEntity { [Required] public string Nume { get; set; } [Required] public int JudetId { get; set; } public virtual Judet judet { get; set; } public Localitate() { } } }
自定义查询方法GetLocalitatiBasicAsync
public async Task<List<Localitate>> GetLocalitatiBasicAsync() { return await base.GetRecords(true).Select(l => new Localitate() { Id = l.Id, Nume = l.Nume, judet = l.judet }).ToListAsync(); }
问题解答
1. 当前方案是否为最优解?
不是。你当前的Select投影方式虽然能拿到导航属性数据,但存在以下问题:
- 写法繁琐:每次需要加载导航属性时,都要手动指定所有需要的实体字段,扩展性差;
- 实体跟踪失效:手动
new出来的Localitate对象不会被EF Core上下文跟踪,后续如果要修改实体并保存,需要额外处理; - 维护成本高:如果
Localitate或Judet实体添加新字段,需要同步修改Select中的赋值逻辑。
更优的方案是结合泛型仓储特性,使用Include进行贪婪加载,或者在泛型仓储中添加支持多Include的重载方法,让代码更简洁、可维护。
2. Select显式导航与Include的区别
(1)加载本质不同
- Include:属于贪婪加载(Eager Loading),会生成SQL JOIN语句,一次性查询主实体和关联实体的所有数据,返回完整的实体对象,导航属性直接被填充。
- Select投影:属于按需加载,只查询你指定的字段,生成的SQL仅包含
Select中列出的列,返回的是你构造的对象(即使是实体类型,也只是部分字段被赋值)。
(2)SQL生成差异
使用
Include的SQL示例:SELECT [l].[Id], [l].[Nume], [l].[JudetId], [l].[DeletedAt], [j].[Id], [j].[Nume], ... FROM [Localitates] AS [l] LEFT JOIN [Judets] AS [j] ON [l].[JudetId] = [j].[Id] WHERE [l].[DeletedAt] IS NULL会查询
Localitate和Judet的所有字段。使用
Select投影的SQL示例:SELECT [l].[Id], [l].[Nume], [j].[Id], [j].[Nume], ... FROM [Localitates] AS [l] LEFT JOIN [Judets] AS [j] ON [l].[JudetId] = [j].[Id] WHERE [l].[DeletedAt] IS NULL仅查询你在
Select中指定的字段,减少数据传输量。
(3)实体状态与跟踪
- Include:返回的实体默认被上下文跟踪(除非添加
AsNoTracking),可以直接修改并调用SaveChanges保存。 - Select投影:手动构造的实体对象不会被上下文跟踪,因为EF Core没有参与实体的创建过程,仅将查询结果映射到对象上。
(4)使用场景不同
- Include:适合需要完整实体对象、后续要进行修改操作,或者关联实体需要被完整使用的场景。
- Select投影:适合只需要部分数据的场景(比如给前端返回DTO),能提升查询性能,减少不必要的数据传输。
优化建议
如果需要获取完整的Localitate实体并包含judet导航属性,推荐直接在自定义方法中使用Include:
public async Task<List<Localitate>> GetLocalitatiBasicAsync() { return await base.GetRecords(true) .Include(l => l.judet) .ToListAsync(); }
如果需要在泛型仓储中支持多导航属性加载,可以添加如下重载方法:
// 在RepositoryBase<T>中添加 public async Task<IList<T>> GetAllWithIncludes(bool asNoTracking = false, bool includeDeleted = false, params Expression<Func<T, object>>[] includes) { var query = includeDeleted ? _dbSet : _dbSet.Where(entity => entity.DeletedAt == null); foreach (var include in includes) { query = query.Include(include); } return asNoTracking ? await query.AsNoTracking().ToListAsync() : await query.ToListAsync(); }
然后在自定义仓储中调用:
public async Task<List<Localitate>> GetLocalitatiBasicAsync() { return await base.GetAllWithIncludes(true, false, l => l.judet) as List<Localitate>; }
内容的提问来源于stack exchange,提问作者Octavian Niculescu
相关产品推荐
相关产品推荐

