EF Core 原生 SQL 执行方法对比(ExecuteSqlRawAsync vs FromSqlRaw)
场景:C# 全栈(EF Core + ABP)开发中,需要直接执行原生 SQL 的两种方法。
一、源码与结论速览
csharp
// 方法一:RunSql ------ 执行 DML,返回受影响行数
public async Task<int> RunSql(string sql)
{
var dbContext = await this.GetDbContextAsync();
try
{
int affectRows = await dbContext.Database.ExecuteSqlRawAsync(sql);
if (affectRows < 0)
{
return -1;
}
return affectRows;
}
catch (Exception ex)
{
return -1; // ⚠️ 吞掉异常,UoW 不会回滚
}
}
// 方法二:GetAllSolutionTrains ------ 执行 SELECT,返回实体列表
public async Task<List<AllSolutionTrains>> GetAllSolutionTrains(string sql)
{
var dbContext = await GetDbContextAsync();
try
{
var query = dbContext.AllSolutionTrains.FromSqlRaw(sql);
var viewList = await query.ToListAsync();
return viewList;
}
catch (Exception ex)
{
throw new Exception(ex.Message); // 包装后抛出
}
}
一句话区别:
RunSql GetAllSolutionTrains
底层 API ExecuteSqlRawAsync FromSqlRaw
适用 SQL INSERT / UPDATE / DELETE / DDL SELECT 查询
返回值 int(受影响行数) List<AllSolutionTrains>(结果集)
实体映射 不需要实体 必须匹配 DbSet 实体,字段对齐
二、逐点对比
2.1 用途 & EF Core API 不同
RunSql → ExecuteSqlRawAsync
适用:INSERT / UPDATE / DELETE / DDL(create table 等)
返回值:受影响行数
不能用来做 SELECT,该 API 不返回数据行,只返回被修改的行数。
执行语句,不追踪实体,也不会加载数据到内存。
GetAllSolutionTrains → FromSqlRaw
适用:SELECT 查询
返回:数据库返回的行映射成 C# 实体 List<AllSolutionTrains>
不能用于增删改,FromSqlRaw 只能跟 SELECT 语句。
限制:
SQL 返回的列名必须与 AllSolutionTrains 实体属性一一对应;
必须返回该实体所有映射字段,少字段会报错;
只能从对应实体的 DbSet 调用(dbContext.AllSolutionTrains)。
2.2 异常处理逻辑差异(重点)
RunSql
csharp
catch (Exception ex)
{
return -1; // 吞掉异常,返回 -1,调用方靠返回值判断失败
}
⚠️ 坑:ABP 工作单元 UoW 下,如果只是 return -1 而没有抛出异常,事务不会自动回滚。SQL 报错后异常被吃掉,UoW 认为代码正常执行,事务会继续提交 ------ 数据一致性隐患很大。
GetAllSolutionTrains
csharp
catch (Exception ex)
{
throw new Exception(ex.Message); // 包装后重新抛出
}
捕获异常后重新抛出,上层可捕获。
⚠️ 小问题:new Exception(ex.Message) 会丢失原始异常类型(如 DbUpdateException、SqlException),建议直接 throw; 保留原始堆栈。
2.3 实体追踪差异
ExecuteSqlRawAsync:完全不追踪实体,只是执行 SQL 命令。
FromSqlRaw 查询出的实体:默认被 EF ChangeTracker 追踪,后续修改实体属性后 SaveChanges 会更新数据库。若不想追踪可加 .AsNoTracking()。
2.4 SQL 注入风险(两者一致)
两个方法都是 Raw 原生 SQL,若直接拼接字符串传参,都存在 SQL 注入风险。
正确做法:使用参数化,不要字符串拼接。
csharp
// 参数化示例
dbContext.Database.ExecuteSqlRawAsync("UPDATE Table SET Name={0}", name);
三、完整对比表
对比项 RunSql(ExecuteSqlRawAsync) GetAllSolutionTrains(FromSqlRaw)
SQL 类型 INSERT / UPDATE / DELETE / DDL SELECT 查询
返回值 int:受影响行数 List<实体>:查询结果集
实体映射 不需要实体 必须匹配 DbSet 实体,字段对齐
实体追踪 无 默认开启追踪
异常处理 吞异常返回 -1,⚠️ 事务不会回滚 重新抛出异常,UoW 可回滚
能否读数据 ❌ 不行 ✅ 可以
四、你现有代码的两个严重坑
坑 1:RunSql 捕获异常 return -1
数据库报错(主键冲突、语法错误)时异常被吞,ABP UoW 感知不到失败,事务继续提交,造成数据不一致。
建议:异常直接往上抛,交由 UoW 自动回滚;调用方自行 try-catch 处理。
坑 2:FromSqlRaw 实体字段必须完全匹配
若 SELECT 返回列少了、列名与实体不一致,直接报错。若想返回自定义字段,不能用此方法,应改用 dbContext.Database.SqlQuery<T>(EF Core 7+)返回自定义 DTO。
五、优化后的建议代码
csharp
// RunSql 优化:不吞异常,交由上层 UoW 处理
public async Task<int> RunSql(string sql)
{
var dbContext = await this.GetDbContextAsync();
int affectRows = await dbContext.Database.ExecuteSqlRawAsync(sql);
return affectRows;
}
// 查询优化:保留原始异常堆栈 + 只读不追踪
public async Task<List<AllSolutionTrains>> GetAllSolutionTrains(string sql)
{
var dbContext = await GetDbContextAsync();
try
{
var query = dbContext.AllSolutionTrains.FromSqlRaw(sql);
var viewList = await query.AsNoTracking().ToListAsync();
return viewList;
}
catch
{
throw; // 保留原始异常堆栈,不要 new Exception
}
}
六、延伸建议
若需执行原生 SQL 返回自定义 DTO(非 DbSet 实体),建议增加通用方法:
csharp
// EF Core 7+ 支持 SqlQuery<T> 返回自定义 DTO
public async Task<List<T>> RunSqlQuery<T>(string sql) where T : class
{
var dbContext = await GetDbContextAsync();
return await dbContext.Database.SqlQuery<T>(sql).ToListAsync();
}
说明:
SqlQuery<T>需Microsoft.EntityFrameworkCore.Relational7.0+ 支持;T为自定义 DTO,列名需与属性对应。