为何调用FirstOrDefault前需先枚举?SQL语法报错问题解析
不清楚为何以下代码在FirstOrDefault前不执行ToList()(枚举操作)时会抛出异常,未找到相关解释,希望有人解答问题所在。
public DateTime? GetOldestUpdDate(int field1Id, DateTime threshold) { var query = @"SELECT MIN(updated_at) AS updated_at FROM my_table WHERE field_1_id = @field_1_id AND updated_at < @threshold;"; return _context.Database.SqlQueryRaw<DateTime?>( query, new MySqlParameter("field_1_id", field1Id), new MySqlParameter("threshold", threshold.DateString())) //.ToList() 不添加此方法时,会抛出SQL查询错误 .FirstOrDefault(); }
异常信息
消息:您的SQL语法存在错误;请查看与您的MariaDB服务器版本对应的手册,了解在第3行'; ) AS
tLIMIT 2'附近使用的正确语法
堆栈跟踪:
在 MySqlConnector.Core.ResultSet.ReadResultSetHeaderAsync(IOBehavior ioBehavior)(位于 //src/MySqlConnector/Core/ResultSet.cs:第43行)
在 MySqlConnector.MySqlDataReader.ActivateResultSet(CancellationToken cancellationToken)(位于 //src/MySqlConnector/MySqlDataReader.cs:第130行)
在 MySqlConnector.MySqlDataReader.CreateAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary2 cachedProcedures, IMySqlCommand command, CommandBehavior behavior, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken)(位于 /_/src/MySqlConnector/MySqlDataReader.cs:第468行) 在 MySqlConnector.Core.CommandExecutor.ExecuteReaderAsync(IReadOnlyList1 commands, ICommandPayloadCreator payloadCreator, CommandBehavior behavior, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken)(位于 //src/MySqlConnector/Core/CommandExecutor.cs:第56行)
在 MySqlConnector.MySqlCommand.ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)(位于 //src/MySqlConnector/MySqlCommand.cs:第344行)
在 MySqlConnector.MySqlCommand.ExecuteDbDataReader(CommandBehavior behavior)(位于 /_/src/MySqlConnector/MySqlCommand.cs:第278行)
...(后续内容省略)
解答
核心原因是EF Core处理SqlQueryRaw返回的IQueryable<T>时,若后续调用FirstOrDefault()这类LINQ方法,会尝试将原SQL包装成子查询,但你的原SQL结尾带了分号,导致拼接后的SQL语法失效。
从异常信息的错误提示near '; ) AS t LIMIT 2'能看出,EF Core生成的最终SQL是把你的原查询(带分号)套进了(...) AS t LIMIT 2的结构里,分号破坏了子查询的语法结构,因此触发错误。
而调用ToList()时,会直接执行原SQL并将结果加载到内存,不会触发EF Core的LINQ查询转换逻辑,也就不会出现拼接错误。
解决方法
- 方法一:移除原SQL结尾的分号
修改SQL字符串,去掉末尾的分号:var query = @"SELECT MIN(updated_at) AS updated_at FROM my_table WHERE field_1_id = @field_1_id AND updated_at < @threshold"; - 方法二:先转为内存枚举再取结果
用AsEnumerable()先将IQueryable<T>转为内存中的枚举集合,再调用FirstOrDefault(),避免EF Core修改原SQL:return _context.Database.SqlQueryRaw<DateTime?>(query, new MySqlParameter("field_1_id", field1Id), new MySqlParameter("threshold", threshold.DateString())).AsEnumerable().FirstOrDefault();
内容的提问来源于stack exchange,提问作者Andre

