如何在Entity Framework数据库优先模式下用FromSqlRaw引用Lookup表字段?
我查了大量类似文章,都没讲清楚如何用DbSet的FromSqlRaw方法从多表(如Lookup表)返回关联数据。我的场景中SlotData与SlotDenom通过外键关联,已用NuGet生成数据模型。控制器的Details()方法能正常传递包含关联表的数据,但Index()方法用FromSqlRaw调用存储过程时,仅能获取SlotData的数据,SlotDenom的信息无法加载——尽管存储过程已包含联查逻辑。
相关代码片段
DbContext配置
modelBuilder.Entity<SlotDatum>(entity => { entity.HasKey(e => e.Id).HasName("PK_Slot_Data_1"); entity.ToTable("Slot_Data"); entity.HasIndex(e => e.Id, "IX_Slot_Data"); entity.Property(e => e.CalDate) .HasColumnType("date") .HasColumnName("Cal_Date"); entity.Property(e => e.Denom_Id).HasColumnName("Denom_ID"); entity.Property(e => e.Drop).HasColumnType("money"); entity.Property(e => e.Handle).HasColumnType("money"); entity.Property(e => e.Win).HasColumnType("money"); entity.Property(d => d.Denom_Id).HasColumnType("int"); entity.HasOne(d => d.Denom).WithMany(p => p.SlotData) .HasForeignKey(d => d.Denom_Id) .HasConstraintName("FK_Slot_Data_SlotDenom"); });
SlotDatum模型
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace FlashWeb.Models.DB; public partial class SlotDatum { [Key] public int Id { get; set; } public DateOnly CalDate { get; set; } public int? Denom_Id { get; set; } public decimal? Handle { get; set; } public decimal? Drop { get; set; } public decimal? Win { get; set; } public int? SlotCount { get; set; } public virtual SlotDenom? Denom { get; set; } }
SlotDenom模型
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace FlashWeb.Models.DB; public partial class SlotDenom { [Key] public int Denom_Id { get; set; } public decimal? Value { get; set; } public string? Description { get; set; } public string? Status { get; set; } public int? ReportOrder { get; set; } public virtual ICollection<SlotDatum> SlotData { get; } = new List<SlotDatum>(); }
存储过程SQL逻辑
SELECT Id, Cal_Date, SUM(Handle) AS 'Handle', SUM([Drop]) AS 'Drop', SUM(Win) AS 'Win', SlotCount, denom.Denom_id, denom.[Description] AS 'Description', ReportOrder, [Status] FROM Slot_Data sd RIGHT OUTER JOIN SlotDenom denom ON denom.Denom_id = sd.Denom_id WHERE denom.ReportOrder IS NOT NULL AND Cal_Date = @sBusDate GROUP BY SlotCount, denom.Denom_id, [Description], ReportOrder, [Status], Id, Cal_Date ORDER BY ReportOrder
原因分析
EF Core的FromSqlRaw默认仅加载主实体(此处为SlotDatum),不会自动加载关联的导航属性(Denom)——哪怕SQL查询包含关联表字段。而Details方法通过Include(s => s.Denom)显式告知EF要加载关联数据,因此能正常工作。
解决方案
方案1:结合Include与FromSqlRaw(EF Core 3.0+支持)
在FromSqlRaw后链式调用Include,让EF Core执行存储过程后,自动通过外键加载关联的SlotDenom数据。此方式要求存储过程返回结果包含外键字段Denom_Id。
修改Index方法代码:
try { IQueryable<SlotDatum> query; if (slotDatum.CalDate != DateOnly.MinValue) query = _context.SlotData.FromSqlRaw("EXEC sp_GetSlotData {0}", slotDatum.CalDate).Include(s => s.Denom); else query = _context.SlotData.FromSqlRaw("EXEC sp_GetSlotsData {0}", DateOnly.FromDateTime(DateTime.Now)).Include(s => s.Denom); return View(await query.ToListAsync()); } catch (Exception ex) { Problem("Entity set " + ex.Message + " is null."); return View(await _context.SlotData.FromSqlRaw("sp_GetLastSlotsData").Include(s => s.Denom).ToListAsync()); }
注意:使用参数化查询
{0}替代字符串拼接,避免SQL注入风险,这是最佳实践。
方案2:手动映射存储过程返回的关联字段
若存储过程已返回SlotDenom的字段(如Description、Status),可在获取数据后手动将这些字段值赋值给SlotDatum的Denom属性。此方式无需EF额外查询SlotDenom表。
步骤:
- 先给SlotDatum模型添加对应存储过程返回的SlotDenom字段属性:
public string? DenomDescription { get; set; } public string? DenomStatus { get; set; } public int? DenomReportOrder { get; set; } - 修改控制器代码:
var slotDataList = await _context.SlotData.FromSqlRaw("EXEC sp_GetSlotData {0}", slotDatum.CalDate).ToListAsync(); foreach(var item in slotDataList) { item.Denom = new SlotDenom { Denom_Id = item.Denom_Id, Description = item.DenomDescription, Status = item.DenomStatus, ReportOrder = item.DenomReportOrder }; } return View(slotDataList);
方案3:用DTO接收完整结果
创建包含SlotData和SlotDenom所有所需字段的DTO类,直接用FromSqlRaw查询到DTO中,一次性获取所有数据后再映射到实体。
示例DTO:
public class SlotDataWithDenomDto { public int Id { get; set; } public DateOnly CalDate { get; set; } public int? Denom_Id { get; set; } public decimal? Handle { get; set; } public decimal? Drop { get; set; } public decimal? Win { get; set; } public int? SlotCount { get; set; } public string? Description { get; set; } public string? Status { get; set; } public int? ReportOrder { get; set; } }
控制器查询代码:
var dtoList = await _context.Set<SlotDataWithDenomDto>().FromSqlRaw("EXEC sp_GetSlotData {0}", slotDatum.CalDate).ToListAsync(); var slotDataList = dtoList.Select(dto => new SlotDatum { Id = dto.Id, CalDate = dto.CalDate, Denom_Id = dto.Denom_Id, Handle = dto.Handle, Drop = dto.Drop, Win = dto.Win, SlotCount = dto.SlotCount, Denom = new SlotDenom { Denom_Id = dto.Denom_Id, Description = dto.Description, Status = dto.Status, ReportOrder = dto.ReportOrder } }).ToList(); return View(slotDataList);
注意事项
- 方案1会触发一次额外的批量查询(根据Denom_Id加载SlotDenom),数据量大时需考虑性能。
- 方案2、3无需额外查询,但需手动处理字段映射,适合存储过程已返回所有所需数据的场景。
- 始终使用参数化查询,避免SQL注入风险。
内容的提问来源于stack exchange,提问作者Doug

