【问题标题】:How can I specify an index hint in Entity Framework?如何在实体框架中指定索引提示?
【发布时间】:2011-12-23 06:47:40
【问题描述】:

sql

select * from table1 with(index=IX_table1_1)

Linq to sql 使用 ado.net 实体想写上面的代码。我找不到实体,特别是索引提示的使用。

linq

var querysample = from a in db.table1
select a;

【问题讨论】:

  • 你不能在 EF LINQ 中指定查询提示...你必须使用 ExecuteStoreQuery
  • @Sam 是 ExecuteStoreQuery L2S 的 EF 版本 ExecuteQuery<T>?
  • 是的......除了它慢了 2* @MarcGravell
  • 我更改了标题 - 这是否准确地代表了问题?
  • @Sam 如果有人会编写一个工具来执行这些基于 SQL 的查询,而无需通常与 ORM 相关的开销!

标签: c# sql-server linq entity-framework c#-4.0


【解决方案1】:

除了 wh1sp3r 的回答,请参见下面的另一个拦截器,它依赖于 EF query tags 而不是线程静态变量:

public class QueryHintInterceptor : DbCommandInterceptor
{
    private static readonly Regex _tableAliasRegex = new Regex("( AS [^ ]+)",
        RegexOptions.Multiline | RegexOptions.IgnoreCase);
    private readonly string _hintPrefix;

    public QueryHintInterceptor(string hintPrefix)
    {
        _hintPrefix = "-- " + hintPrefix;
    }

    public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command,
        CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        PatchCommandText(command);
        return base.ReaderExecuting(command, eventData, result);
    }

    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command, CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        PatchCommandText(command);
        return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
    }

    public override InterceptionResult<object> ScalarExecuting(DbCommand command,
        CommandEventData eventData, InterceptionResult<object> result)
    {
        PatchCommandText(command);
        return base.ScalarExecuting(command, eventData, result);
    }

    public override ValueTask<InterceptionResult<object>> ScalarExecutingAsync(
        DbCommand command, CommandEventData eventData, InterceptionResult<object> result,
        CancellationToken cancellationToken = default)
    {
        PatchCommandText(command);
        return base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
    }

    private void PatchCommandText(DbCommand command)
    {
        if (command.CommandText.StartsWith(_hintPrefix, StringComparison.Ordinal))
        {
            int index = command.CommandText.IndexOfAny(Environment.NewLine.ToCharArray(),
                _hintPrefix.Length);
            command.CommandText = _tableAliasRegex
                .Replace(command.CommandText, "${0} WITH (" +  command.CommandText
                    .Substring(_hintPrefix.Length, index - _hintPrefix.Length) + ")")
                .Substring(index);
        }
    }
}

扩展方法:

public static class QueryHintsDbContextOptionsBuilderExtensions
{
    private const string HintTag = "Use hint: ";
    public static IQueryable<T> WithHint<T>(this IQueryable<T> source,
        TableHint hint) =>
        source.TagWith(HintTag + hint);

    public static DbContextOptionsBuilder<TContext> AddQueryHints<TContext>(
        this DbContextOptionsBuilder<TContext> builder)
        where TContext : DbContext =>
        builder.AddInterceptors(new QueryHintInterceptor(HintTag));
}


public enum TableHint
{
    KeepIdentity,
    KeepDefaults,
    HoldLock,
    Ignore_Constraints,
    Ignore_Triggers,
    Nolock,
    NoWait,
    PagLock,
    ReadCommitted,
    ReadCommittedLock,
    ReadPast,
    RepeatableRead,
    RowLock,
    Serializable,
    Snapshot,
    TabLock,
    TabLockX,
    UpdLock,
    Xlock
}

示例用法:

await using var context = new TestDbContext(
    new DbContextOptionsBuilder<TestDbContext>()
        .UseSqlServer("<connection string>")
        .AddQueryHints()
        .LogTo(message => Console.WriteLine("EF: {0}", message))
        .Options);

var result = await context.SomeEntities
    .WithHint(TableHint.TabLock)
    .ToListAsync();

更新

上述自定义 SQL 生成的方式不会影响.ToQueryString(),因为命令拦截器在实际 SQL 生成之后才起作用。它会导致测试和日志记录出现问题,例如在 ASP.Net 核心中。所以另一种解决方案是使用自定义QuerySqlGenerator

public static class TableHintsDbContextOptionsBuilderExtensions
{
    public static IQueryable<T> WithHint<T>(this IQueryable<T> source,
        TableHint hint) =>
        source.TagWith(hint.ToString());

    public static DbContextOptionsBuilder UseTableHints(
        this DbContextOptionsBuilder builder) =>
        builder.ReplaceService<IQuerySqlGeneratorFactory,
            HintTagSqlServerQuerySqlGeneratorFactory>();
}

class HintTagSqlServerQuerySqlGeneratorFactory : IQuerySqlGeneratorFactory
{
    private readonly QuerySqlGeneratorDependencies dependencies;

    public HintTagSqlServerQuerySqlGeneratorFactory(
        QuerySqlGeneratorDependencies dependencies)
    {
        this.dependencies = dependencies;
    }

    public QuerySqlGenerator Create()
    {
        return new HintTagSqlServerQuerySqlGenerator(dependencies);
    }
}

class HintTagSqlServerQuerySqlGenerator : SqlServerQuerySqlGenerator
{
    private readonly HashSet<TableHint> tableHints = new();

    public HintTagSqlServerQuerySqlGenerator(
        QuerySqlGeneratorDependencies dependencies) :
        base(dependencies)
    {
    }

    protected override void GenerateTagsHeaderComment(
        SelectExpression selectExpression)
    {
        foreach (var tag in selectExpression.Tags)
            if (Enum.TryParse(typeof(TableHint), tag, out var hint))
            {
                tableHints.Add((TableHint)hint!);
                selectExpression.Tags.Remove(tag);
            }

        base.GenerateTagsHeaderComment(selectExpression);
    }

    protected override Expression VisitTable(
        TableExpression tableExpression)
    {
        var result = base.VisitTable(tableExpression);

        if (tableHints.Count > 0)
            Sql.Append($" WITH ({string.Join(", ", tableHints).ToUpperInvariant()})");

        return result;
    }
}

但是,警告表明

Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerQuerySqlGenerator 是一个内部 API,它支持 Entity Framework Core 基础结构,并且不受与公共 API 相同的兼容性标准的约束。它可能会在任何版本中更改或删除,恕不另行通知。

【讨论】:

    【解决方案2】:

    添加到 stop-crans 答案,这适用于(我在)EF Core 5.0 并支持WITH(INDEX(param)) 的参数。

    public class QueryHintInterceptor : DbCommandInterceptor
    {
        private static readonly Regex _tableAliasRegex = new Regex(@"(FROM[\s\r\n]+\S+(?:[\s\r\n]+AS[\s\r\n]+[^\s\r\n]+)?)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
        private readonly string _hintPrefix;
    
        public QueryHintInterceptor(string hintPrefix)
        {
            _hintPrefix = "-- " + hintPrefix;
        }
    
        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
        {
            PatchCommandtext(command);
            return base.ReaderExecuting(command, eventData, result);
        }
    
        public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
        {
            PatchCommandtext(command);
            return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
        }
    
        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
        {
            PatchCommandtext(command);
            return base.ScalarExecuting(command, eventData, result);
        }
    
        public override ValueTask<InterceptionResult<object>> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<object> result, CancellationToken cancellationToken = default)
        {
            PatchCommandtext(command);
            return base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
        }
    
        private void PatchCommandtext(DbCommand command)
        {
            if (command.CommandText.StartsWith(_hintPrefix, StringComparison.Ordinal))
            {
                int index = command.CommandText.IndexOfAny(Environment.NewLine.ToCharArray(), _hintPrefix.Length);
                command.CommandText = _tableAliasRegex
                    .Replace(command.CommandText, "${0} WITH (" + command.CommandText
                    .Substring(_hintPrefix.Length, index - _hintPrefix.Length) + ")")
                    .Substring(index);
            }
        }
    }
    
    public static class QueryHintsDbContextOptionsBuilderExtensions
    {
        private const string HintTag = "Use hint: ";
        public static IQueryable<T> WithHint<T>(this IQueryable<T> source, TableHint hint) =>
            source.TagWith(HintTag + hint);
        public static IQueryable<T> WithHint<T>(this IQueryable<T> source, TableHint hint, string param) =>
            source.TagWith(HintTag + hint + " (" + param+")");
    
        public static DbContextOptionsBuilder AddQueryHints(this DbContextOptionsBuilder builder) =>
            builder.AddInterceptors(new QueryHintInterceptor(HintTag));
    }
    
    public enum TableHint
    {
        Index,
        KeepIdentity,
        KeepDefaults,
        HoldLock,
        Ignore_Constraints,
        Ignore_Triggers,
        Nolock,
        NoWait,
        PagLock,
        ReadCommitted,
        ReadCommittedLock,
        ReadPast,
        RepeatableRead,
        RowLock,
        Serializable,
        Snapshot,
        TabLock,
        TabLockX,
        UpdLock,
        Xlock
    }
    

    【讨论】:

      【解决方案3】:

      解决方案很简单。 让我们添加一个拦截器!!!

          public class HintInterceptor : DbCommandInterceptor
      {
          private static readonly Regex _tableAliasRegex = new Regex(@"(?<tableAlias>AS \[Extent\d+\](?! WITH \(*HINT*\)))", RegexOptions.Multiline | RegexOptions.IgnoreCase);
      
          [ThreadStatic] public static string HintValue;
      
          public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
          {
              if (!String.IsNullOrWhiteSpace(HintValue))
              {
                  command.CommandText = _tableAliasRegex.Replace(command.CommandText, "${tableAlias} WITH (*HINT*)");
                  command.CommandText = command.CommandText.Replace("*HINT*", HintValue);
              }
      
              HintValue = String.Empty;
          }
      
          public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
          {
              if (!String.IsNullOrWhiteSpace(HintValue))
              {
                  command.CommandText = _tableAliasRegex.Replace(command.CommandText, "${tableAlias} WITH (*HINT*)");
                  command.CommandText = command.CommandText.Replace("*HINT*", HintValue);
              }
      
              HintValue = String.Empty;
          }
      }
      

      我知道,正则表达式可能会更好。 让我们在 Config 类中注册我们的拦截器

      public class PbsContextConfig : DbConfiguration
      {
          public PbsContextConfig()
          {
              this.AddInterceptor(new HintInterceptor());
          }
      }
      

      让我们为 DbSet 制作漂亮的提示扩展

      public static class HintExtension
      {
          public static DbSet<T> WithHint<T>(this DbSet<T> set, string hint) where T : class
          {
              HintInterceptor.HintValue = hint;
              return set;
          }
      }
      

      如何使用?

      context.Persons.WithHint("INDEX(XI_DOWNTIME_LOCK)").Where( x => x.ID == ....
      

      欢迎修改!

      【讨论】:

      • 有趣的想法,但是当我尝试它时,我发现在我的查询之前执行了许多上下文设置查询,并且在第一个上下文设置查询中它会清除 HintValue 并且永远不会将其应用于我的查询。此外,由于它正在执行通过 ReaderExecuting 和 ScalarExecuting 传递的上下文设置查询,因此很有可能将“提示”应用于错误的查询。不知道如何更改代码以使其特定于我的查询,如果我找到方法,我会发布更新。
      【解决方案4】:

      L2S 和 EF 都不会像定制的那样提供对 SQL 的直接支持(索引提示等),尽管使用 L2S 您可以通过ExecuteQuery&lt;T&gt;(...) 实现它(它采用原始 TSQL)。如果您需要这种级别的控制,请考虑存储过程或替代 ORM。

      这里特别的一个问题是查询提示非常特定于平台,但 EF 试图与平台无关。

      【讨论】:

      • "如果您需要这种级别的控制,请考虑存储过程或替代 ORM" => 当然,但不实用。当你知道事实是这样的时候,已经太晚了
      猜你喜欢
      • 1970-01-01
      • 2011-07-06
      • 1970-01-01
      • 1970-01-01
      • 2014-11-12
      • 1970-01-01
      • 2015-12-29
      • 1970-01-01
      相关资源
      最近更新 更多