EF数据库优先模式下,能否自动填充非实体属性?
好问题!EF的Include方法本质是用来加载EF所管理的导航属性(也就是基于数据库外键关联的实体),而你新增的NewData是通过存储过程动态获取的,不属于EF默认跟踪的关联关系,所以直接用Include("NewData")是行不通的。不过我们可以通过几种方式模拟出类似Include的自动填充效果,下面给你详细说几种方案:
方案1:自定义扩展方法模拟Include(简单直观)
我们可以写一个扩展方法,让它像Include一样被链式调用,内部自动帮你调用存储过程赋值。这种方式最接近你想要的用法,而且实现起来很简单:
首先,给Data类的NewData加上[NotMapped]特性,告诉EF这个属性不需要映射到数据库(避免更新时出错):
public class Data { // 原有属性... [NotMapped] public NewData NewData { get; set; } }
然后写扩展方法:
public static class DataQueryExtensions { public static IQueryable<Data> IncludeNewData(this IQueryable<Data> query, YourDbContext context) { // 先把查询结果取出来 var dataList = query.ToList(); // 遍历每个Data,调用存储过程赋值 foreach (var data in dataList) { data.NewData = context.GetDataFromStoredProcedure(data.Id); } return dataList.AsQueryable(); } }
使用的时候就和你期望的几乎一样了:
var dataObject = context.Datas.Where(...).IncludeNewData(context).FirstOrDefault();
⚠️ 注意:这个方案如果查询结果很多,会触发N+1次存储过程调用(每个Data调用一次),性能可能受影响。如果是大数据量场景,看下面的优化版:
优化版(批量调用存储过程)
如果你的存储过程支持接收多个DataId作为参数(比如用表值参数或者逗号分隔的Id串),可以先批量获取所有需要的Id,再一次性调用存储过程,然后关联赋值:
public static IQueryable<Data> IncludeNewData(this IQueryable<Data> query, YourDbContext context) { var dataList = query.ToList(); if (!dataList.Any()) return dataList.AsQueryable(); // 提取所有Data的Id var dataIds = dataList.Select(d => d.Id).ToList(); // 批量调用存储过程获取所有NewData var newDataList = context.GetBatchDataFromStoredProcedure(dataIds).ToList(); // 关联赋值 foreach (var data in dataList) { data.NewData = newDataList.FirstOrDefault(nd => nd.DataId == data.Id); } return dataList.AsQueryable(); }
这样就只需要调用一次存储过程,性能会好很多。
方案2:使用投影查询(适合EF Core,避免N+1)
如果你用的是EF Core,可以直接在查询时通过Select投影来一次性获取原实体和存储过程结果,这种方式不会有N+1问题:
var dataObject = context.Datas .Where(d => /* 你的条件 */) .Select(d => new Data { // 复制原Data的所有属性 Id = d.Id, Name = d.Name, // ...其他原有属性 // 直接调用存储过程赋值NewData NewData = context.GetDataFromStoredProcedure(d.Id) }) .FirstOrDefault();
不过这个方案的缺点是,如果Data类的属性很多,手动复制会很麻烦。可以用AutoMapper这类工具来简化属性映射,或者在EF Core中使用context.Entry(d).CurrentValues来复制属性,但稍微复杂一点。
方案3:EF Core无键实体+关联查询(更贴合EF风格)
如果你用的是EF Core 3.0及以上版本,可以把存储过程的结果映射成无键实体,然后通过Join来关联原Data实体:
首先定义NewData类,并配置为无键实体:
public class NewData { public int DataId { get; set; } // 和Data的Id关联 // 其他存储过程返回的属性... } // 在DbContext的OnModelCreating中配置 protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<NewData>().HasNoKey(); }
然后查询时通过Join关联:
var dataWithNewData = context.Datas .Where(d => /* 你的条件 */) .Join( // 调用存储过程获取NewData集合 context.Set<NewData>().FromSqlRaw("EXEC GetDataFromStoredProcedure @DataId = {0}", d.Id), d => d.Id, nd => nd.DataId, (d, nd) => new { Data = d, NewData = nd } ) .Select(x => { x.Data.NewData = x.NewData; return x.Data; }) .FirstOrDefault();
这个方案更符合EF的查询范式,但需要额外配置无键实体,适合需要多次复用这个查询的场景。
关键注意事项
不管用哪种方案,一定要给NewData属性加上[NotMapped]特性(或者在Fluent API中用modelBuilder.Entity<Data>().Ignore(d => d.NewData);),否则EF会尝试把这个属性映射到数据库列,导致更新或者查询时出错。
内容的提问来源于stack exchange,提问作者Joel

