繁体   English   中英

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

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

暂无
暂无

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

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