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