EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界

[复制链接]
发表于 2026-3-24 06:29:34 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
做 EF Core 一段时间后,许多人都会碰到同一个节点:通例 LINQ 能覆盖大多数查询,但一到复杂报表、视图大概汗青 SQL 复用场景,就会开始思量原生 SQL。题目不在于“能不能写 SQL”,而在于怎么写得可维护、可观测可观测、还能和 EF Core 的映射体系共同好。这篇文章讲授 FromSql、SqlQuery 的利用边界和对象映射的一些坑。
1. 题目配景:为什么原生 SQL 常常能跑但难以恒久维护

在体系演进到中后期后,下面这些场景非常常见:

  • 报表查询须要 GROUP BY、聚合,LINQ 写出来可读性很差。
  • 汗青体系已经有稳固 SQL,须要在新服务里复用。
  • 部分查询要精确控制实行操持,团队渴望直接落 SQL。
这时间“能跑”的版本通常很快就能写出来,但过一段时间就会袒露题目:

  • SQL 拼接字符串,参数化做得不同等,埋下注入和操持污染风险。
  • 映射范例界说不清楚,字段一改名就出现运行时映射非常。
  • 查询读模子和实体模子混用,导致跟踪举动和更新语义变得紊乱。
  • 慢 SQL 能看到语句,但定位不到具体业务查询意图。
以是这篇不是教你“怎么在 EF Core 里实行 SQL”,而是讲“怎样把原生 SQL 纳入 EF Core 的工程边界”。
2. 原明白析:先分清 FromSql 和 SqlQuery 的职责

FromSql 和 SqlQuery 都能实行原生 SQL,但它们办理的是差异题目。
2.1 FromSql:面向 DbSet 的查询入口

FromSql 得当挂在 DbSet 或 Set() 上实行原生 SQL,典范用途有两类:

  • 查询实体范例(可跟踪,也可 AsNoTracking)
  • 查询 Keyless 读模子(只读投影)
关键点:

  • 优先用参数化写法(如 FromSqlInterpolated),不要拼接原始字符串。
  • SQL 返回列要和映射范例属性同等,否则会在运行时堕落。
  • 若用于纯读场景,发起显式 AsNoTracking() 低落跟踪开销。
2.2 SqlQuery:面向轻量读模子和标量效果

Database.SqlQuery 更得当“读多写少”的轻量查询:

  • 直接映射到 DTO
  • 实行标量统计(如 count/sum)
它的定位就是查询,不负担实体生命周期管理。对报表、配景统计、运营看板这类读路径很实用。
2.3 对象映射边界的核心原则

无论用哪种方式,发起固定三条原则:

  • 下令边界:原生 SQL 负责读模子查询,不直接承载复杂写入事故语义。
  • 模子边界:实体模子和报表 DTO 分离,制止查询模子反向污染范畴模子。
  • 可观测可观测边界:关键查询用 TagWith 标注业务意图,便于慢 SQL 排障。
3. 示例代码:从伤害写法到可维护落地

3.1 题目写法:字符串拼 SQL + 实体/读模子混用
  1. public async Task<List<Order>> SearchOrdersAsync(string keyword, CancellationToken ct)
  2. {
  3.     var sql = $"""
  4.         SELECT *
  5.         FROM Orders
  6.         WHERE CustomerName LIKE '%{keyword}%'
  7.     """;
  8.     return await _db.Orders
  9.         .FromSqlRaw(sql)
  10.         .ToListAsync(ct);
  11. }
复制代码
这段代码的风险很会合:

  • 直接拼接字符串,参数化缺失。
  • SELECT * 对列厘革非常敏感。
  • 查询语义是“搜索效果”,却直接映射实体,后续轻易和更新流程耦合。
3.2 优化写法一:FromSql + Keyless 读模子承接报表查询

先界说读模子:
  1. public sealed class MonthlyTopCustomerRow
  2. {
  3.     public string CustomerNo { get; set; } = string.Empty;
  4.     public decimal TotalAmount { get; set; }
  5.     public int OrderCount { get; set; }
  6. }
复制代码
在 OnModelCreating 中声明 Keyless:
  1. modelBuilder.Entity<MonthlyTopCustomerRow>().HasNoKey();
复制代码
查询代码:
  1. public async Task<List<MonthlyTopCustomerRow>> GetTopCustomersAsync(
  2.     DateTime monthStart,
  3.     DateTime monthEnd,
  4.     CancellationToken ct)
  5. {
  6.     return await _db.Set<MonthlyTopCustomerRow>()
  7.         .FromSqlInterpolated($"""
  8.             SELECT
  9.                 o.CustomerNo,
  10.                 SUM(o.TotalAmount) AS TotalAmount,
  11.                 COUNT(1) AS OrderCount
  12.             FROM Orders o
  13.             WHERE o.CreatedAt >= {monthStart} AND o.CreatedAt < {monthEnd}
  14.             GROUP BY o.CustomerNo
  15.         """)
  16.         .TagWith("Report:MonthlyTopCustomers")
  17.         .AsNoTracking()
  18.         .OrderByDescending(x => x.TotalAmount)
  19.         .Take(20)
  20.         .ToListAsync(ct);
  21. }
复制代码
这套写法把“报表查询”明确限定在读模子上,反面实体跟踪语义混在一起。
3.3 优化写法二:SqlQuery 映射 DTO 与标量统计

界说 DTO:
  1. public sealed class OrderRevenueDto
  2. {
  3.     public long OrderId { get; set; }
  4.     public string OrderNo { get; set; } = string.Empty;
  5.     public decimal Revenue { get; set; }
  6. }
复制代码
查询 DTO:
  1. public async Task<List<OrderRevenueDto>> GetPaidOrderRevenueAsync(CancellationToken ct)
  2. {
  3.     var paid = 2;
  4.     return await _db.Database
  5.         .SqlQuery<OrderRevenueDto>($"""
  6.             SELECT
  7.                 o.Id AS OrderId,
  8.                 o.OrderNo,
  9.                 SUM(i.LineAmount) AS Revenue
  10.             FROM Orders o
  11.             INNER JOIN OrderItems i ON i.OrderId = o.Id
  12.             WHERE o.Status = {paid}
  13.             GROUP BY o.Id, o.OrderNo
  14.         """)
  15.         .ToListAsync(ct);
  16. }
复制代码
查询标量:
  1. public async Task<int> GetPendingOrderCountAsync(CancellationToken ct)
  2. {
  3.     var pending = 1;
  4.     return await _db.Database
  5.         .SqlQuery<int>($"""
  6.             SELECT COUNT(1)
  7.             FROM Orders
  8.             WHERE Status = {pending}
  9.         """)
  10.         .SingleAsync(ct);
  11. }
复制代码
3.4 最轻易踩的映射坑


  • 列名不同等:DTO 属性和 SQL 别名不同等会导致映射失败或值错位。
  • 可空性不匹配:数据库可空列映射到不可空属性,运行时轻易报错。
  • 范例边界不清:比方数据库 bigint 映射到 int,高位数据会溢出。
  • 把读模子当实体:查询 DTO 后又实行走 SaveChanges,语义会紊乱。
4. 总结

在 EF Core 里利用原生 SQL 的关键,不是“写不写 SQL”,而是“把 SQL 放在精确边界”。FromSql 更得当承接 DbSet 维度查询,SqlQuery 更得当轻量 DTO 和标量统计。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表