簡體   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