【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 方法以返回关联数据
目录
相关文章
|
18天前
|
SQL 存储 人工智能
Vanna:开源 AI 检索生成框架,自动生成精确的 SQL 查询
Vanna 是一个开源的 Python RAG(Retrieval-Augmented Generation)框架,能够基于大型语言模型(LLMs)为数据库生成精确的 SQL 查询。Vanna 支持多种 LLMs、向量数据库和 SQL 数据库,提供高准确性查询,同时确保数据库内容安全私密,不外泄。
86 7
Vanna:开源 AI 检索生成框架,自动生成精确的 SQL 查询
|
25天前
|
SQL Java
使用java在未知表字段情况下通过sql查询信息
使用java在未知表字段情况下通过sql查询信息
35 8
|
1月前
|
SQL 缓存 Java
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
本文详细介绍了MyBatis的各种常见用法MyBatis多级缓存、逆向工程、分页插件 包括获取参数值和结果的各种情况、自定义映射resultMap、动态SQL
【详细实用のMyBatis教程】获取参数值和结果的各种情况、自定义映射、动态SQL、多级缓存、逆向工程、分页插件
|
1月前
|
SQL 安全 PHP
PHP开发中防止SQL注入的方法,包括使用参数化查询、对用户输入进行过滤和验证、使用安全的框架和库等,旨在帮助开发者有效应对SQL注入这一常见安全威胁,保障应用安全
本文深入探讨了PHP开发中防止SQL注入的方法,包括使用参数化查询、对用户输入进行过滤和验证、使用安全的框架和库等,旨在帮助开发者有效应对SQL注入这一常见安全威胁,保障应用安全。
54 4
|
1月前
|
SQL 监控 关系型数据库
SQL语句当前及历史信息查询-performance schema的使用
本文介绍了如何使用MySQL的Performance Schema来获取SQL语句的当前和历史执行信息。Performance Schema默认在MySQL 8.0中启用,可以通过查询相关表来获取详细的SQL执行信息,包括当前执行的SQL、历史执行记录和统计汇总信息,从而快速定位和解决性能瓶颈。
|
1月前
|
SQL 存储 缓存
如何优化SQL查询性能?
【10月更文挑战第28天】如何优化SQL查询性能?
131 10
|
1月前
|
SQL 关系型数据库 MySQL
|
3月前
|
关系型数据库 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)")
|
5月前
|
SQL 存储 监控
SQL Server的并行实施如何优化?
【7月更文挑战第23天】SQL Server的并行实施如何优化?
134 13