惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
G
Google Developers Blog
Latest news
Latest news
Know Your Adversary
Know Your Adversary
O
OpenAI News
腾讯CDC
月光博客
月光博客
P
Privacy International News Feed
Google Online Security Blog
Google Online Security Blog
Help Net Security
Help Net Security
L
LINUX DO - 最新话题
雷峰网
雷峰网
AI
AI
Hacker News - Newest:
Hacker News - Newest: "LLM"
有赞技术团队
有赞技术团队
N
News and Events Feed by Topic
V
Vulnerabilities – Threatpost
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Docker
Google DeepMind News
Google DeepMind News
T
Tor Project blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hacker News: Ask HN
Hacker News: Ask HN
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Heimdal Security Blog
I
Intezer
WordPress大学
WordPress大学
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy & Cybersecurity Law Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
V
V2EX
博客园 - 三生石上(FineUI控件)
G
GRAHAM CLULEY
Security Archives - TechRepublic
Security Archives - TechRepublic
F
Fortinet All Blogs
L
LangChain Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Spread Privacy
Spread Privacy
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V2EX - 技术
V2EX - 技术
Stack Overflow Blog
Stack Overflow Blog
Recent Announcements
Recent Announcements
T
Tenable Blog
Microsoft Azure Blog
Microsoft Azure Blog
V
Visual Studio Blog
SecWiki News
SecWiki News
Cisco Talos Blog
Cisco Talos Blog

博客园 - 邓磊Lei

为什么 Go 一个 HTTP 服务可以同时处理数万连接 gin怎么处理一次http请求的 我的经验:git提交信息分别用什么icon C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用 .NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 Microsoft Agent Framework + Kimi API 实战:控制台应用跑通单次与多轮 Agent 对话 开发实战:asp.net core + ef core 实现动态可扩展的分页方案 聊聊 ASP.NET Core 中间件和过滤器的区别 Python 入门:从“其他语言”到 Pythonic 思维的完整迁移手册 .NET 进阶之路:异步、并发与内存管理的系统性认知 Redis:延迟双删的适用边界与落地细节 Serilog:从结构化日志认知到 .NET 工程落地 EF Core 拦截器实战:SaveChangesInterceptor、CommandInterceptor 与审计落地 ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界 .NET .Result 避坑指南:不同框架下的死锁与线程池饥饿 EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈 EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理 EF Core 写入链路深拆:从 ChangeTracker 到 SQL Batch 的性能诊断与优化 ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地 C# 异步编程深水区:Task、ValueTask、线程池饥饿与背压设计 EF Core 查询性能黑洞:Include、投影与跟踪策略的边界 从 IApplicationBuilder 到 RequestDelegate:ASP.NET Core 请求管线的性能与可观测性实战 如何在 rider 中配置 SonarQube
EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界
邓磊Lei · 2026-03-23 · via 博客园 - 邓磊Lei

做 EF Core 一段时间后,很多人都会遇到同一个节点:常规 LINQ 能覆盖大多数查询,但一到复杂报表、视图或者历史 SQL 复用场景,就会开始考虑原生 SQL。问题不在于“能不能写 SQL”,而在于怎么写得可维护、可观测、还能和 EF Core 的映射体系配合好。这篇文章讲解 FromSqlSqlQuery 的使用边界和对象映射的一些坑。

1. 问题背景:为什么原生 SQL 常常能跑但难以长期维护

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

  • 报表查询需要 GROUP BY、聚合,LINQ 写出来可读性很差。
  • 历史系统已经有稳定 SQL,需要在新服务里复用。
  • 部分查询要精确控制执行计划,团队希望直接落 SQL。

这时候“能跑”的版本通常很快就能写出来,但过一段时间就会暴露问题:

  1. SQL 拼接字符串,参数化做得不一致,埋下注入和计划污染风险。
  2. 映射类型定义不清晰,字段一改名就出现运行时映射异常。
  3. 查询读模型和实体模型混用,导致跟踪行为和更新语义变得混乱。
  4. 慢 SQL 能看到语句,但定位不到具体业务查询意图。

所以这篇不是教你“怎么在 EF Core 里执行 SQL”,而是讲“如何把原生 SQL 纳入 EF Core 的工程边界”。

2. 原理解析:先分清 FromSql 和 SqlQuery 的职责

FromSqlSqlQuery 都能执行原生 SQL,但它们解决的是不同问题。

2.1 FromSql:面向 DbSet 的查询入口

FromSql 适合挂在 DbSetSet<T>() 上执行原生 SQL,典型用途有两类:

  • 查询实体类型(可跟踪,也可 AsNoTracking
  • 查询 Keyless 读模型(只读投影)

关键点:

  • 优先用参数化写法(如 FromSqlInterpolated),不要拼接原始字符串。
  • SQL 返回列要和映射类型属性一致,否则会在运行时出错。
  • 若用于纯读场景,建议显式 AsNoTracking() 降低跟踪开销。

2.2 SqlQuery:面向轻量读模型和标量结果

Database.SqlQuery<T> 更适合“读多写少”的轻量查询:

  • 直接映射到 DTO
  • 执行标量统计(如 count/sum)

它的定位就是查询,不承担实体生命周期管理。对报表、后台统计、运营看板这类读路径很实用。

2.3 对象映射边界的核心原则

无论用哪种方式,建议固定三条原则:

  1. 命令边界:原生 SQL 负责读模型查询,不直接承载复杂写入事务语义。
  2. 模型边界:实体模型和报表 DTO 分离,避免查询模型反向污染领域模型。
  3. 可观测边界:关键查询用 TagWith 标注业务意图,便于慢 SQL 排障。

3. 示例代码:从危险写法到可维护落地

3.1 问题写法:字符串拼 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 * 对列变化非常敏感。
  • 查询语义是“搜索结果”,却直接映射实体,后续容易和更新流程耦合。

3.2 优化写法一:FromSql + Keyless 读模型承接报表查询

先定义读模型:

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);
}

这套写法把“报表查询”明确限定在读模型上,不和实体跟踪语义混在一起。

3.3 优化写法二:SqlQuery 映射 DTO 与标量统计

定义 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);
}

3.4 最容易踩的映射坑

  1. 列名不一致:DTO 属性和 SQL 别名不一致会导致映射失败或值错位。
  2. 可空性不匹配:数据库可空列映射到不可空属性,运行时容易报错。
  3. 类型边界不清:例如数据库 bigint 映射到 int,高位数据会溢出。
  4. 把读模型当实体:查询 DTO 后又尝试走 SaveChanges,语义会混乱。

4. 总结

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