[英]Does EF Core cache command text provided via FromSql method?
EF 核心版本 2.2
我们有一个包含原始 SQL 和表值参数(下面的实现)的扩展方法ContainsIds
,我们使用它来仅过滤具有特定标识符的记录。
但是我们注意到有时查询执行失败并出现错误
System.Data.SqlClient.SqlException:必须声明表变量“@Id_4d9da9993ecd4917a9a58aa9b06628c9”。
可以重现问题的测试用例(我们正在针对真实的 SQL 服务器数据库运行测试用例)
[Fact]
public async Task Can_execute_multiple_queries()
{
await InsertTestOrdersWithIds(1, 2, 3, 4);
var results = await Task.WhenAll(
LoadOrders("First", 1, 2),
LoadOrders("Second", 3, 4)
);
foreach (var result in results)
{
result.Should().HaveCount(2);
}
}
private async Task<Order[]> LoadOrders(string name, params int[] ids)
{
_output.WriteLine($"LOAD ORDERS {name}");
using var context = contextFactory.Create();
return await context.DeliveryProducts.ContainsIds(ids).ToArrayAsync();
}
测试失败并出现上面已经提到的错误。 日志显示生成的 SQL 在 SQL 文本和传递给查询执行的参数中具有不同的 SQL 参数名称。
注意“LOAD ORDERS 2”中的不同参数名称Parameters=[@Id_3b31368fcc3b4afba6e08c153b2c92ed='?'
对比SELECT * FROM @Id_c99c5200c7454d4fbb8f5e31ceaf5546
当我们比较第一次和第二次生成的 SQL 命令查询时,我们可以看到第二次执行使用了之前执行的查询字符串。
因为我们将相同的字符串查询传递给.FromSql
方法
"SELECT * FROM [Order] o WHERE EXISTS (SELECT * FROM {0} i WHERE i.Id = o.Id"
我可以假设 EF Core 以某种方式缓存此查询字符串,并在“注入”参数之前使用字符串作为键,但在“注入”之后使用字符串作为缓存值。
请注意,每个查询执行都使用DbContext
的不同实例。
但是,当我们执行一次查询时 - 它有效。
我们还发现将参数名称插入命令文本将起作用,并且将使用正确的参数和参数名称。 请参阅此问题以了解工作实施: How to use Table-Valued parameter with EF Core
问题:
使用先前执行的命令文本的 EF Core 在幕后发生了什么?
日志:
--- LOAD ORDERS First ---
[08:52:07.1462 Information]
Microsoft.EntityFrameworkCore.Infrastructure
Entity Framework Core 2.2.4-servicing-10062 initialized 'MyDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: CommandTimeout=60
[08:52:07.3641 Information] Microsoft.EntityFrameworkCore.Database.Command
Executed DbCommand (41ms) [Parameters=[@Id_c99c5200c7454d4fbb8f5e31ceaf5546='?' (DbType = Object)], CommandType='Text', CommandTimeout='30']
SELECT * FROM [dbo].[Order] o WHERE EXISTS (SELECT * FROM @Id_c99c5200c7454d4fbb8f5e31ceaf5546 i WHERE i.Id = o.Id)
--- LOAD ORDERS Second ---
[08:52:07.3653 Information] Microsoft.EntityFrameworkCore.Infrastructure
Entity Framework Core 2.2.4-servicing-10062 initialized 'MyDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: CommandTimeout=30
[08:52:07.3763 Error] Microsoft.EntityFrameworkCore.Database.Command
Failed executing DbCommand (8ms) [Parameters=[@Id_3b31368fcc3b4afba6e08c153b2c92ed='?' (DbType = Object)], CommandType='Text', CommandTimeout='60']
SELECT * FROM [dbo].[Order] o WHERE EXISTS (SELECT * FROM @Id_c99c5200c7454d4fbb8f5e31ceaf5546 i WHERE i.Id = o.Id)
[08:52:07.3827 Error] Microsoft.EntityFrameworkCore.Query
An exception occurred while iterating over the results of a query for context type 'MyProject.MyDbContext'.
System.Data.SqlClient.SqlException (0x80131904): Must declare the table variable "@Id_c99c5200c7454d4fbb8f5e31ceaf5546".
at System.Data.SqlClient.SqlCommand.<>c.<ExecuteDbDataReaderAsync>b__122_0(Task`1 result)
at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke()
at System.Threading.Tasks.Task.<>c.<.cctor>b__277_0(Object obj)
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location ---
at Microsoft.EntityFrameworkCore.Storage.Internal.RelationalCommand.ExecuteAsync(IRelationalConnection connection, DbCommandMethod executeMethod, IReadOnlyDictionary`2 parameterValues, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.AsyncQueryingEnumerable`1.AsyncEnumerator.BufferlessMoveNext(DbContext _, Boolean buffer, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.AsyncQueryingEnumerable`1.AsyncEnumerator.MoveNext(CancellationToken cancellationToken)
at System.Linq.AsyncEnumerable.SelectEnumerableAsyncIterator`2.MoveNextCore(CancellationToken cancellationToken) in D:\a\1\s\Ix.NET\Source\System.Interactive.Async\Select.cs:line 106
at System.Linq.AsyncEnumerable.AsyncIterator`1.MoveNext(CancellationToken cancellationToken) in D:\a\1\s\Ix.NET\Source\System.Interactive.Async\AsyncIterator.cs:line 98
at Microsoft.EntityFrameworkCore.Query.Internal.AsyncLinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext(CancellationToken cancellationToken)
扩展方法实现:
public static IQueryable<Order> ContainsIds(this IQueryable<Order> source, params int[] ids)
{
var parameter = ids.ToSqlParameter();
return source.FromSql(
"SELECT * FROM [Order] o WHERE EXISTS (SELECT * FROM {0} i WHERE i.Id = o.Id",
parameter
);
}
public static DataTable ToDataTable(this int[] ids)
{
var table = new DataTable();
table.Columns.Add("Id", typeof(int));
foreach (var id in ids)
{
var row = table.NewRow();
row.SetField("Id", id);
table.Rows.Add(row);
}
return table;
}
public static SqlParameter ToSqlParameter(this int[] ids)
{
return new SqlParameter
{
ParameterName = $"Id_{Guid.NewGuid():N}", // generate unique parameter name
SqlDbType = SqlDbType.Structured,
TypeName = "dbo.MyIdType",
Value = ids.ToDataTable()
};
}
执行查询时可以查看EF core的日志
https://eamonkeane.dev/3-ways-to-view-sql-generated-by-entity-framework-core-5/
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.