时间:2026-03-23 17:06
人气:
作者:admin
做 EF Core 一段时间后,很多人都会遇到同一个节点:常规 LINQ 能覆盖大多数查询,但一到复杂报表、视图或者历史 SQL 复用场景,就会开始考虑原生 SQL。问题不在于“能不能写 SQL”,而在于怎么写得可维护、可观测、还能和 EF Core 的映射体系配合好。这篇文章讲解 FromSql、SqlQuery 的使用边界和对象映射的一些坑。
在系统演进到中后期后,下面这些场景非常常见:
GROUP BY、聚合,LINQ 写出来可读性很差。这时候“能跑”的版本通常很快就能写出来,但过一段时间就会暴露问题:
所以这篇不是教你“怎么在 EF Core 里执行 SQL”,而是讲“如何把原生 SQL 纳入 EF Core 的工程边界”。
FromSql 和 SqlQuery 都能执行原生 SQL,但它们解决的是不同问题。
FromSql 适合挂在 DbSet 或 Set<T>() 上执行原生 SQL,典型用途有两类:
AsNoTracking)关键点:
FromSqlInterpolated),不要拼接原始字符串。AsNoTracking() 降低跟踪开销。Database.SqlQuery<T> 更适合“读多写少”的轻量查询:
它的定位就是查询,不承担实体生命周期管理。对报表、后台统计、运营看板这类读路径很实用。
无论用哪种方式,建议固定三条原则:
TagWith 标注业务意图,便于慢 SQL 排障。public async Task<List<Order>> SearchOrdersAsync(string keyword, CancellationToken ct)
{
var sql = $"""
SELECT *
FROM Orders
WHERE CustomerName LIKE '%{keyword}%'
""";
return await _db.Orders
.FromSqlRaw(sql)
.ToListAsync(ct);
}
这段代码的风险很集中:
SELECT * 对列变化非常敏感。先定义读模型:
public sealed class MonthlyTopCustomerRow
{
public string CustomerNo { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public int OrderCount { get; set; }
}
在 OnModelCreating 中声明 Keyless:
modelBuilder.Entity<MonthlyTopCustomerRow>().HasNoKey();
查询代码:
public async Task<List<MonthlyTopCustomerRow>> GetTopCustomersAsync(
DateTime monthStart,
DateTime monthEnd,
CancellationToken ct)
{
return await _db.Set<MonthlyTopCustomerRow>()
.FromSqlInterpolated($"""
SELECT
o.CustomerNo,
SUM(o.TotalAmount) AS TotalAmount,
COUNT(1) AS OrderCount
FROM Orders o
WHERE o.CreatedAt >= {monthStart} AND o.CreatedAt < {monthEnd}
GROUP BY o.CustomerNo
""")
.TagWith("Report:MonthlyTopCustomers")
.AsNoTracking()
.OrderByDescending(x => x.TotalAmount)
.Take(20)
.ToListAsync(ct);
}
这套写法把“报表查询”明确限定在读模型上,不和实体跟踪语义混在一起。
定义 DTO:
public sealed class OrderRevenueDto
{
public long OrderId { get; set; }
public string OrderNo { get; set; } = string.Empty;
public decimal Revenue { get; set; }
}
查询 DTO:
public async Task<List<OrderRevenueDto>> GetPaidOrderRevenueAsync(CancellationToken ct)
{
var paid = 2;
return await _db.Database
.SqlQuery<OrderRevenueDto>($"""
SELECT
o.Id AS OrderId,
o.OrderNo,
SUM(i.LineAmount) AS Revenue
FROM Orders o
INNER JOIN OrderItems i ON i.OrderId = o.Id
WHERE o.Status = {paid}
GROUP BY o.Id, o.OrderNo
""")
.ToListAsync(ct);
}
查询标量:
public async Task<int> GetPendingOrderCountAsync(CancellationToken ct)
{
var pending = 1;
return await _db.Database
.SqlQuery<int>($"""
SELECT COUNT(1)
FROM Orders
WHERE Status = {pending}
""")
.SingleAsync(ct);
}
bigint 映射到 int,高位数据会溢出。SaveChanges,语义会混乱。在 EF Core 里使用原生 SQL 的关键,不是“写不写 SQL”,而是“把 SQL 放在正确边界”。FromSql 更适合承接 DbSet 维度查询,SqlQuery 更适合轻量 DTO 和标量统计。
本文来自博客园,作者:邓磊DL,转载请注明原文链接:https://www.cnblogs.com/denglei1024/p/19759009
Microsoft Agent Framework Skills 执行 Scripts(实
EF Core 原生 SQL 实战:FromSql、SqlQuery 与对