简体   繁体   English

EF Core 是否缓存通过 FromSql 方法提供的命令文本?

[英]Does EF Core cache command text provided via FromSql method?

EF Core version 2.2 EF 核心版本 2.2

We have an extension method ContainsIds with raw SQL and Table-Valued Parameter (implementation below), which we are using to filter only records with specific identifiers.我们有一个包含原始 SQL 和表值参数(下面的实现)的扩展方法ContainsIds ,我们使用它来仅过滤具有特定标识符的记录。

However we noticed that sometimes query execution fails with the error但是我们注意到有时查询执行失败并出现错误

System.Data.SqlClient.SqlException: Must declare the table variable "@Id_4d9da9993ecd4917a9a58aa9b06628c9". System.Data.SqlClient.SqlException:必须声明表变量“@Id_4d9da9993ecd4917a9a58aa9b06628c9”。

Test case which can reproduce the issue (we are running test cases against real SQL Server database)可以重现问题的测试用例(我们正在针对真实的 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();
}

Test fails with the error already mentioned above.测试失败并出现上面已经提到的错误。 Logs shows that generated SQL has different names for SQL parameter in the SQL text and parameter passed to the query execution.日志显示生成的 SQL 在 SQL 文本和传递给查询执行的参数中具有不同的 SQL 参数名称。
Notice different parameter names in "LOAD ORDERS 2"注意“LOAD ORDERS 2”中的不同参数名称
Parameters=[@Id_3b31368fcc3b4afba6e08c153b2c92ed='?'
vs对比
SELECT * FROM @Id_c99c5200c7454d4fbb8f5e31ceaf5546

When we compare first and second generated SQL command queries, we can see that second execution using query string from previous execution.当我们比较第一次和第二次生成的 SQL 命令查询时,我们可以看到第二次执行使用了之前执行的查询字符串。
Because we passing same string query to the .FromSql method因为我们将相同的字符串查询传递给.FromSql方法

"SELECT * FROM [Order] o WHERE EXISTS (SELECT * FROM {0} i WHERE i.Id = o.Id"

I can assume that EF Core somehow cache this query string and use string before "injecting" parameter as a key, but string after "injecting" as cached value.我可以假设 EF Core 以某种方式缓存此查询字符串,并在“注入”参数之前使用字符串作为键,但在“注入”之后使用字符串作为缓存值。
Notice that every query execution use different instances of DbContext .请注意,每个查询执行都使用DbContext的不同实例。
However when we execute query once - it works.但是,当我们执行一次查询时 - 它有效。

Also we found that interpolating parameter name into the command text will work and will use correct parameter and parameter name.我们还发现将参数名称插入命令文本将起作用,并且将使用正确的参数和参数名称。 See this question for working implmentation: How to use Table-Valued parameter with EF Core请参阅此问题以了解工作实施: How to use Table-Valued parameter with EF Core

Question:问题:
What happens under the hood in EF Core which uses command text from previous execution?使用先前执行的命令文本的 EF Core 在幕后发生了什么?

Logs:日志:

--- 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)

Extension method implementation:扩展方法实现:

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()
    };
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM