繁体   English   中英

在 SQL 服务器中在 Entity Framework Core 6 或更高版本中运行 FromSqlRaw 之前执行自定义 SQL

[英]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.

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