【Entity Framework】你必须了解的之自定义SQL查询

简介: 【Entity Framework】你必须了解的之自定义SQL查询


一、概述


通过 Entity Framework Core 可以在使用关系数据库时下降到 SQL 查询。 如果所需查询无法使用 LINQ 表示,或者 LINQ 查询导致 EF 生成效率低下的 SQL,则可使用 SQL 查询。 SQL 查询可返回一般实体类型或者模型中的无键实体类型。


二、基本SQL查询

可使用FromSql 基于SQL查询开始LINQ查询:

var blogs = context.Blogs
    .FromSql($"SELECT * FROM dbo.Blogs")
    .ToList();

EF Core 7.0中引入FromSql。使用更旧的版本时,请改用FromSqlInterpolated

SQL 查询可用于执行返回实体数据的存储过程:

var blogs = context.Blogs
    .FromSql($"EXECUTE dbo.GetMostPopularBlogs")
    .ToList();

FromSql只能直接在DbSet 上使用。不能在任意Linq查询的基础上组合使用它。


三、快速参考

请在使用 SQL 查询时密切关注参数化

向 SQL 查询引入任何用户提供的值时,必须注意防范 SQL 注入攻击。 如果程序将用户提供的字符串值集成到 SQL 查询中,而用户提供的值被创建用来终止字符串并执行另一个恶意 SQL 操作,则表明发生了 SQL 注入。


FromSql 和 FromSqlInterpolated方法可以防止 SQL 注入,始终将参数数据作为单独的 SQL 参数进行集成。 但是,如果不当使用,FromSqlRaw方法可能易受 SQL 注入攻击。


示例通过在 SQL 查询字符串中包含参数占位符并提供额外的自变量,将单个参数传递到存储过程:


var user = "johndoe";
var blogs = context.Blogs
    .FromSql($"EXECUTE dbo.GetMostPopularBlogsForUser {user}")
    .ToList();


虽然此语法可能看上去像常规 C# 字符串内插,但提供的值包装在 DbParameter 中,且生成的参数名称插入到指定了 {0} 占位符的位置。 这使得 FromSql 可以免受 SQL 注入攻击,可以将值高效且正确地发送到数据库。


执行存储过程时,在 SQL 查询字符串中使用命名参数很有用,尤其是在存储过程具有可选参数的情况下:

var user = new SqlParameter("user", "johndoe");
var blogs = context.Blogs
    .FromSql($"EXECUTE dbo.GetMostPopularBlogsForUser @filterByUser={user}")
    .ToList();

如果需要对要发送的数据库参数进行更多控制,还可以构造 DbParameter 并将其作为参数值提供。 这样就可以设置参数的精确数据库类型,或 facet(例如其大小、精度或长度):

var user = new SqlParameter("user", "johndoe");
var blogs = context.Blogs
    .FromSql($"EXECUTE dbo.GetMostPopularBlogsForUser {user}")
    .ToList();

传递的参数必须与存储过程定义完全匹配。 请特别注意参数的排序,注意不要缺失或错放任何参数,也可以考虑使用命名参数表示法。 此外,请确保参数类型对应,并根据需要设置其 facet(大小、精度、规模)。


四、动态SQL和参数

应尽可能使用 FromSql及其参数化。 但在某些情况下,需要将 SQL 动态拼凑在一起,并且无法使用数据库参数。 例如,假设 C# 变量包含要通过其进行筛选的属性的名称。 你可能会迫不及待地想要使用 SQL 查询,例如:

var propertyName = "User";
var propertyValue = "johndoe";

var blogs = context.Blogs
    .FromSql($"SELECT * FROM [Blogs] WHERE {propertyName} = {propertyValue}")
    .ToList();

此代码无效,因为数据库不允许将列名(或架构的任何其他部分)参数化。


首先,请务必考虑通过 SQL 或其他方式动态构造查询的影响。 接受用户提供的列名时,用户可能会选择一个未编制索引的列,使查询运行起来极慢且让数据库过载;用户也可能选择一个包含你不希望公开的数据的列。 除了真正动态的方案外,通常情况下,最好是让两个查询使用两个列名,而不是使用参数化将它们折叠到单个查询中。

如果决定要动态构造 SQL,则必须使用 FromSqlRaw,这样就可以直接将变量数据内插到 SQL 字符串中,而不使用数据库参数:

var columnName = "Url";
var columnValue = new SqlParameter("columnValue", "http://SomeURL");
var blogs = context.Blogs
    .FromSqlRaw($"SELECT * FROM [Blogs] WHERE {columnName} = @columnValue", columnValue)
    .ToList();

在上面的代码中,我们使用 C# 字符串内插将列名直接插入到 SQL 中。 你有责任确保此字符串值是安全的,如果它的来源不安全,请对其进行清理;这意味着检测特殊字符(如分号、注释和其他 SQL 构造),并正确地将这些字符进行转义或拒绝此类输入。


另一方面,列值通过 DbParameter 发送,因此在遇到 SQL 注入时是安全的。


使用FromSqlRaw时要非常小心,始终确保值来自安全的源,或者经过正确清理。SQL注入攻击可能会为应用程序带来灾难后果。


五、使用LINQ编写

可以使用 LINQ 运算符在初始 SQL 查询的基础上进行组合;EF Core 会将 SQL 视为子查询,在数据库中以它为基础进行组合。 下面的示例使用 SQL 查询,该查询从表值函数 (TVF) 中进行选择。 然后,使用 LINQ 进行筛选和排序,从而对其进行组合。

var searchTerm = "Lorem ipsum";

var blogs = context.Blogs
    .FromSql($"SELECT * FROM dbo.SearchBlogs({searchTerm})")
    .Where(b => b.Rating > 3)
    .OrderByDescending(b => b.Rating)
    .ToList();

上面的查询生成以下 SQL:

SELECT [b].[BlogId], [b].[OwnerId], [b].[Rating], [b].[Url]
FROM (
    SELECT * FROM dbo.SearchBlogs(@p0)
) AS [b]
WHERE [b].[Rating] > 3
ORDER BY [b].[Rating] DESC

包含关联数据

Include 运算符可用于加载相关数据,就像对其他 LINQ 查询那样:

var searchTerm = "Lorem ipsum";

var blogs = context.Blogs
    .FromSql($"SELECT * FROM dbo.SearchBlogs({searchTerm})")
    .Include(b => b.Posts)
    .ToList();

使用 LINQ 进行组合要求 SQL 查询是可组合的,因为 EF Core 会将提供的 SQL 视为子查询。 可组合 SQL 查询通常以 SELECT 关键字开头,不能包含子查询中无效的 SQL 功能,例如:

  • 结尾分号
  • 在 SQL Server 上,结尾处的查询级提示(例如,OPTION (HASH JOIN)
  • 在 SQL Server 上,SELECT 子句中不与 OFFSET 0TOP 100 PERCENT 配合使用的 ORDER BY 子句

SQL Server 不允许对存储过程调用进行组合,因此任何尝试向此类调用应用其他查询运算符的操作都将导致无效的 SQL。


请在FromSql或FromSqlRaw之后立即使用AsEnumerable或AsAsyncEnumerable,确保EF Core不会尝试对存储过程进行组合。


六、更改跟踪

使用 FromSql 或 FromSqlRaw 的查询遵循与 EF Core 中所有其他 LINQ 查询完全相同的更改跟踪规则。 例如,如果该查询投影实体类型,默认情况下会跟踪结果。


下面的示例使用 SQL 查询,该查询从表值函数 (TVF) 中进行选择,然后禁用通过调用AsNoTracking进行的更改跟踪:


var searchTerm = "Lorem ipsum";
var blogs = context.Blogs
    .FromSql($"SELECT * FROM dbo.SearchBlogs({searchTerm})")
    .AsNoTracking()
    .ToList();

查询标量(非实体)类型

虽然可以使用 FromSql来查询模型中定义的实体,但如果使用 SqlQuery,你就可以通过 SQL 轻松查询非实体标量类型,无需下降到较低级别的数据访问 API。 例如,以下查询从 Blogs 表中提取所有 ID:

var ids = context.Database
    .SqlQuery<int>($"SELECT [BlogId] FROM [Blogs]")
    .ToList();

还可以在 SQL 查询的基础上组合使用 LINQ 运算符。 但是,由于 SQL 成为子查询,其输出列需要由 SQL EF 添加项来引用,因此必须为输出列 Value 命名。 例如,以下查询返回的 ID 高于 ID 平均值:

var overAverageIds = context.Database
    .SqlQuery<int>($"SELECT [BlogId] AS [Value] FROM [Blogs]")
    .Where(id => id > context.Blogs.Average(b => b.BlogId))
    .ToList();

FromSql可与数据库提供程序支持的任何标量类型配合使用。如果想要使用数据库提供程序不支持的类型,可以使用约定前配置为其定义值转换。

SqlQueryRaw允许动态构造SQL查询,就像FromSqlRaw 对实体类型所做的那样。


七、执行非查询 SQL

在某些情况下,可能需要执行不返回任何数据的 SQL,通常用于修改数据库中的数据或调用不返回任何结果集的存储过程。 可以通过ExcuteSql完成此操作:

using (var context = new BloggingContext())
{
    var rowsModified = context.Database.ExecuteSql($"UPDATE [Blogs] SET [Url] = NULL");
}

它会执行提供的 SQL 并返回修改的行的数目。ExcuteSql使用安全的参数化来防止SQL注入,就像FromSql一样,而ExcuteSqlRaw允许动态构造SQL查询,就像FromSqlRaw对查询所做的那样。


在 EF Core 7.0 之前,有时必须使用 ExecuteSql API 对数据库执行“批量更新”,如上所示;这比在查询所有匹配行后使用 SaveChanges 来修改它们要高效得多。 EF Core 7.0 引入了ExceuteUpdate和ExecuteDelete,因此可以通过LINQ表达高效的批量更新操作。建议尽可能使用这些API而非ExecuteSql。


八、总结

从 SQL 查询返回实体类型时,需注意以下几个限制:

  • SQL 查询必须返回实体类型的所有属性的数据。
  • 结果集中的列名必须与属性映射到的列名称匹配。 请注意,此行为与 EF6 不同;
  • EF6 忽略了 SQL 查询的属性-列映射,只需结果集列名与这些属性名相匹配即可。
  • SQL 查询不能包含关联数据。 但是,在许多情况下你可以在查询后面紧跟着使用 Include 方法以返回关联数据
目录
相关文章
|
22天前
|
SQL 存储 缓存
高基数 GroupBy 在 SLS SQL 中的查询加速
本文详细介绍了SLS中的高基数GroupBy查询加速技术。
|
21天前
|
SQL 运维 程序员
一个功能丰富的SQL审核查询平台
一个功能丰富的SQL审核查询平台
|
3天前
|
SQL
SQL: 巧妙使用CASE WHEN实现查询
文章演示了如何利用SQL中的CASE WHEN语句来有效地进行条件性聚合查询,通过具体示例展示了CASE WHEN在统计分析中的应用技巧。
8 0
|
27天前
|
SQL 数据库 Java
HQL vs SQL:谁将统治数据库查询的未来?揭秘Hibernate的神秘力量!
【8月更文挑战第31天】Hibernate查询语言(HQL)是一种面向对象的查询语言,它模仿了SQL的语法,但操作对象为持久化类及其属性,而非数据库表和列。HQL具有类型安全、易于维护等优点,支持面向对象的高级特性,内置大量函数,可灵活处理查询结果。下面通过示例对比HQL与SQL,展示HQL在实际应用中的优势。例如,HQL查询“从员工表中筛选年龄大于30岁的员工”只需简单地表示为 `FROM Employee e WHERE e.age &gt; 30`,而在SQL中则需明确指定表名和列名。此外,HQL在处理关联查询时也更为直观易懂。然而,对于某些复杂的数据库操作,SQL仍有其独特优势。
33 0
|
27天前
|
SQL 关系型数据库 MySQL
|
27天前
|
API Java 数据库连接
从平凡到卓越:Hibernate Criteria API 让你的数据库查询瞬间高大上,彻底告别复杂SQL!
【8月更文挑战第31天】构建复杂查询是数据库应用开发中的常见需求。Hibernate 的 Criteria API 以其强大和灵活的特点,允许开发者以面向对象的方式构建查询逻辑,同时具备 SQL 的表达力。本文将介绍 Criteria API 的基本用法并通过示例展示其实际应用。此 API 通过 API 构建查询条件而非直接编写查询语句,提高了代码的可读性和安全性。无论是简单的条件过滤还是复杂的分页和连接查询,Criteria API 均能胜任,有助于提升开发效率和应用的健壮性。
59 0
|
27天前
|
Java UED 开发者
当错误遇上Struts 2:一场优雅的异常处理盛宴,如何让错误信息成为用户体验的救星?
【8月更文挑战第31天】在Web应用开发中,异常处理对确保用户体验和系统稳定性至关重要。Struts 2 提供了完善的异常处理机制,包括 `exception` 拦截器、`ActionSupport` 类以及 OGNL 表达式,帮助开发者优雅地捕获和展示错误信息。本文详细介绍了 Struts 2 的异常处理策略,涵盖拦截器配置、错误信息展示及自定义全局异常处理器的实现方法,使应用程序更加健壮和用户友好。
35 0
|
1天前
|
关系型数据库 MySQL 网络安全
5-10Can't connect to MySQL server on 'sh-cynosl-grp-fcs50xoa.sql.tencentcdb.com' (110)")
5-10Can't connect to MySQL server on 'sh-cynosl-grp-fcs50xoa.sql.tencentcdb.com' (110)")
|
2月前
|
SQL 存储 监控
SQL Server的并行实施如何优化?
【7月更文挑战第23天】SQL Server的并行实施如何优化?
65 13