[英]Execute custom SQL before running FromSqlRaw in Entity Framework Core 6 or above in SQL Server
我只需要它为 SQL 服务器工作。 这是一个例子。 问题是关于一般方法。
https://entityframework-extensions.net有一个很好的扩展方法,叫做WhereBulkContains
。 它有点好,只是这个库中的方法代码被混淆了,并且当应用这些扩展方法在IQueryable<T>
上调用.ToQueryString()
时,它们不会产生有效的 SQL。
随后,我无法在生产代码中使用此类方法,因为由于业务原因,我没有“被允许”信任此类代码。 当然,我可以编写大量测试来确保WhereBulkContains
按预期工作,除了在某些复杂情况下WhereBulkContains
的性能远低于一流水平,而正确编写的 SQL 眨眼间就可以工作。 并且(阅读上文),由于这个库的代码被混淆了,如果不花费大量时间,就无法弄清楚那里出了什么问题。 如果库没有被混淆,我们会购买许可证(因为这不是免费软件)。 为了我们的目的,所有这些基本上扼杀了图书馆。
这就是有趣的地方。 我可以轻松地创建和填充一个临时表,例如(我有一个名为AgentId
的表,在数据库中有一个名为EFAgents
的 int PK):
private string GetTmpAgentSql(IEnumerable<int> agentIds) => @$"
drop table if exists #tmp_Agents;
create table #tmp_Agents (AgentId int not null, primary key clustered (AgentId asc));
{(agentIds
.Chunk(1_000)
.Select(e => $@"
insert into #tmp_Agents (AgentId)
values
({e.JoinStrings("), (")});
")
.JoinStrings(""))}
select 0 as Result
";
private const string AgentSql = @"
select a.* from EFAgents a inner join #tmp_Agents t on a.AgentID = t.AgentId";
其中GetContext
返回 EF Core 数据库上下文,而JoinStrings
来自Unity.Interception.Utilities
,然后按如下方式使用它:
private async Task<List<EFAgent>> GetAgents(List<int> agentIds)
{
var tmpSql = GetTmpAgentSql(agentIds);
using var ctx = GetContext();
// This creates a temporary table and populates it with the ids.
// This is a proprietary port of EF SqlQuery code, but I can post the whole thing if necessary.
var _ = await ctx.GetDatabase().SqlQuery<int>(tmpSql).FirstOrDefaultAsync();
// There is a DbSet<EFAgent> called Agents.
var query = ctx.Agents
.FromSqlRaw(AgentSql)
.Join(ctx.Agents, t => t.AgentId, a => a.AgentId, (t, a) => a);
var sql = query.ToQueryString() + Environment.NewLine;
// This should provide a valid SQL; https://entityframework-extensions.net does NOT!
// WriteLine - writes to console or as requested. This is irrelevant to the question.
WriteLine(sql);
var result = await query.ToListAsync();
return result;
}
所以,基本上,我可以分两步做我需要的事情:
using var ctx = GetContext();
// 1. Create a temp table and populate it - call GetTmpAgentSql.
...
// 2. Build the join starting from `FromSqlRaw` as in example above.
这是可行的,半手动的,它会起作用。
问题是如何一步完成,例如,调用:
.WhereMyBulkContains(aListOfIdConstraints, whateverElseIsneeded, ...)
就这样。
如果我需要在每种情况下传递多个参数以指定约束,我很好。
为了澄清为什么我需要 go 进入所有这些麻烦。 我们必须与第三方数据库交互。 我们无法控制那里的架构和数据。 数据库很大,设计很差。 这导致了一些丑陋的 EFC LINQ 查询。 为了解决这个问题,其中一些丑陋被封装到一个方法中,该方法接受IQueryable<T>
(以及更多参数)并返回IQueryable<T>
。 在后台,此方法调用WhereBulkContains
。 我需要将这个WhereBulkContains
替换为WhereMyBulkContains
,它能够提供正确ToQueryString
表示(用于调试目的)并具有高性能。 后者意味着 SQL不应包含具有数百(甚至有时数千)元素in
子句。 如果我在纯 SQL 中这样做,则使用带有 PK 的 [temp] 表和在 FK 字段上具有索引的inner join
似乎可以解决问题。 但是,...我需要在 C# 中执行此操作,并且在两个 LINQ 方法调用之间有效。 重构一切也不是一种选择,因为这种方法在很多地方都使用过。
非常感谢!
我认为您真的想使用表值参数。
从枚举中创建一个SqlParameter
有点繁琐,但要做到正确并不难;
CREATE TYPE [IntValue] AS TABLE (
Id int NULL
)
private IEnumerable<SqlDataRecord> FromValues(IEnumerable<int> values)
{
var meta = new SqlMetaData(
"Id",
SqlDbType.Int
);
foreach(var value in values)
{
var record = new SqlDataRecord(
meta
);
record.SetInt32(0, value);
yield return record;
}
}
public SqlParameter ToIntTVP(IEnumerable<int> values){
return new SqlParameter()
{
TypeName = "IntValue",
SqlDbType = SqlDbType.Structured,
Value = FromValues(values)
};
}
我个人会在 EF Core 中定义一个查询类型来表示 TVP。 然后,您可以使用原始 sql 返回IQueryable
。
public class IntValue
{
public int Id { get; set; }
}
modelBuilder.Entity<IntValue>(e =>
{
e.HasNoKey();
e.ToView("IntValue");
});
IQueryable<IntValue> ToIntQueryable(DbContext ctx, IEnumerable<int> values)
{
return ctx.Set<IntValue>()
.FromSqlInterpolated($"select * from {ToIntTVP(values)}");
}
现在您可以使用 Linq 编写查询的 rest。
var ids = ToIntQueryable(ctx, agentIds);
var query = ctx.Agents
.Where(a => ids.Any(i => i.Id == a.Id));
我建议使用linq2db.EntityFrameworkCore (请注意,我是创建者之一)。 它具有内置的临时表支持。
我们可以创建简单且可重用的 function 过滤任何类型的记录:
public static class HelperMethods
{
private class KeyHolder<T>
{
[PrimaryKey]
public T Key { get; set; } = default!;
}
public static async Task<List<TEntity>> GetRecordsByIds<TEntity, TKey>(this IQueryable<TEntity> query, IEnumerable<TKey> ids, Expression<Func<TEntity, TKey>> keyFunc)
{
var ctx = LinqToDBForEFTools.GetCurrentContext(query) ??
throw new InvalidOperationException("Query should be EF Core query");
// based on DbContext options, extension retrieves connection information
using var db = ctx.CreateLinqToDbConnection();
// create temporary table and BulkCopy records into that table
using var tempTable = await db.CreateTempTableAsync(ids.Select(id => new KeyHolder<TKey> { Key = id }), tableName: "temporaryIds");
var resultQuery = query.Join(tempTable, keyFunc, t => t.Key, (q, t) => q);
// we use ToListAsyncLinqToDB to avoid collission with EF Core async methods.
return await resultQuery.ToListAsyncLinqToDB();
}
}
然后我们可以将您的 function GetAgents
重写为以下内容:
private async Task<List<EFAgent>> GetAgents(List<int> agentIds)
{
using var ctx = GetContext();
var result = await ctx.Agents.GetRecordsByIds(agentIds, a => a.AgentId);
return result;
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.