获取实体框架 6 在其下面的 SELECT 语句中使用 NOLOCK

Posted

技术标签:

【中文标题】获取实体框架 6 在其下面的 SELECT 语句中使用 NOLOCK【英文标题】:Get Entity Framework 6 use NOLOCK in its underneath SELECT statements 【发布时间】:2014-09-01 07:44:29 【问题描述】:

我在 MVC 5 项目中使用实体框架 6。如您所知,如果我们在其中使用WITH (NOLOCK),SQL Server 中的SELECT 查询将执行得更快、更高效。我检查了一些由 Entity Framework 6 生成的 SQL SELECT 语句,发现它们都不包含 NOLOCK。

我不想在我的提取操作中使用事务来读取未提交的事务。

如何强制 EF 6 在下面生成的 SELECT 语句中使用 NOLOCK?

【问题讨论】:

NOLOCK 不是一个神奇的快速按钮。它带来了一些特别讨厌的东西以及轻微的性能提升。您是否可以处理查询中的重复和/或缺失行?你对难以重现和修复错误感到很酷吗?阅读这篇文章了解更多详情。 blogs.msdn.com/b/davidlean/archive/2009/04/06/… Scott Hanselman 在 08 年就 LINQ to SQL 解决了 NOLOCK。它可能值得一读。 hanselman.com/blog/… 你是我 6 年前的经理,他认为 (nolock) 是一切的答案......而不是正确的数据库索引转向? 事实上,读取未提交的数据我很好。 我曾在除了最关键的查询之外的所有查询都需要 with(nolock) 的公司工作过,如果你不使用它就会被解雇。你可以争论到脸色发青。同样的人争辩说,您不能在存在子句中使用 SELECT *,尽管事实上 select 不会返回任何行......脸是蓝色的......但是,我确实理解这一点。这不是我的查询的神奇 gofast 按钮,他们不希望我的查询为其他人锁定表。 【参考方案1】:

首先...你永远不应该对每个 SQL 语句都使用 NOLOCK。它可能会损害您数据的完整性。

就像任何其他查询提示一样,这是一种机制,您应该只在您做一些不寻常的事情时使用。

没有办法告诉 EF 提供程序呈现 NoLock 提示。如果你真的需要读取未提交的数据,你有以下选择。

    编写您自己的 EntityFramework Provider。

    使用命令拦截器在语句被修改之前对其进行修改 执行。 http://msdn.microsoft.com/en-us/data/dn469464.aspx

    使用带有 IsolationLevel.ReadUncommited 的 TransactionScope。

我知道您说过您不想使用事务,但它是读取未提交数据的唯一开箱即用方式。此外,它不会产生太多开销,因为 SQL Server 中的每个语句“隐式”在事务中运行。

using (new TransactionScope(
                    TransactionScopeOption.Required, 
                    new TransactionOptions 
                     
                         IsolationLevel = IsolationLevel.ReadUncommitted 
                    )) 

        using (var db = new MyDbContext())  
            // query
        

编辑: 还需要注意的是,从 SQL Server 2016 开始,Microsoft 已弃用用于更新和删除的 NOLOCK(选择保持不变),并将在未来的“a”版本中删除。

https://docs.microsoft.com/en-us/sql/database-engine/deprecated-database-engine-features-in-sql-server-2016?view=sql-server-2017

【讨论】:

是的,我完全理解在 SELECT 语句中使用 NOLOCK 可能产生的后果。我们完全可以对保留大量数据的表使用 NOLOCK,因为 WHERE 子句知道从“已提交”部分获取数据的位置。该特定表用于在正在进行的事务中插入/更新大量数据,可能需要几分钟才能完成。但是,我们希望在事务进行时使用数据表的另一个区域。我们的底线是我们不想失去应用程序的速度。 @Arash 如果这是您的问题,为什么不禁用表级(和页面级)锁定而不是使用 NOLOCK?如果读取和写入不接触相同的数据,效果应该是一样的。 对于上面提到的“选项 3”作为可能比其他两个更容易的解决方案在其他问题中有示例:***.com/questions/926656/entity-framework-with-nolock "...NEVER EVER..." 和“任何其他查询都暗示了一种机制,你应该只在你做一些不寻常的事情时使用” - 谁说他没有做一些不寻常的事情普通的?提示必须谨慎使用并理解,但它们并不邪恶。 由于有些人似乎对我在答案中使用 NEVER EVER 有疑问,我想澄清一下我的确切意思。我使用这个短语的方式与它在以下句子中的使用方式相同:你永远不应该尝试在哈德逊河上降落飞机,除非两个引擎都停了下来,而另一种选择是坠毁在曼哈顿中城。【参考方案2】:

您可以使用不为每个查询使用事务范围的解决方法。如果您运行下面的代码,ef 将对相同的服务器进程 ID 使用相同的事务隔离级别。由于服务器进程 ID 在同一请求中不会更改,因此每个请求只需调用一次就足够了。这也适用于 EF Core。

this.Database.ExecuteSqlCommand("SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;");

【讨论】:

如果您的 SQL Server 版本早于 Sql Server 2014,则此解决方案存在很大风险,因为这为整个连接设置了隔离级别。完成连接后,它将返回连接池,并且下一个将获取此特定连接的调用也将以该隔离级别执行,这可能很糟糕。 Sql Server 2014 及更高版本在返回池连接时重置隔离级别。 axmim。感谢您的提示/提醒。我认为我的回答和“SetEndTransaction”解决了这个问题。同时,如果没有您的评论,我会迟早发现这个。【参考方案3】:

我同意 codeworx 所说的未提交读取可能很危险的说法。如果你能忍受脏读,那就去吧。

我找到了一种在不更改当前查询中的任何内容的情况下完成这项工作的方法。

您需要像这样创建一个 DbCommandInterceptor:

public class IsolationLevelInterceptor : DbCommandInterceptor

    private IsolationLevel _isolationLevel;

    public IsolationLevelInterceptor(IsolationLevel level)
    
        _isolationLevel = level;
    



    //[ThreadStatic]
    //private DbCommand _command;


    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    
        SetTransaction(command);

    

    public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    
        SetTransaction(command);
    

    public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
    
        SetTransaction(command);
    




    private void SetTransaction(DbCommand command)
    
        if (command != null)
        
            if (command.Transaction == null)
            
                var t = command.Connection.BeginTransaction(_isolationLevel);
                command.Transaction = t;
                //_command = command;
            
        
    


然后在 cctor(您的 dbcontext 的静态构造器)处,只需将拦截器添加到实体框架集合的 DbInfrastructure。

DbInterception.Add(new IsolationLevelInterceptor(System.Data.IsolationLevel.ReadUncommitted));

这将为 EF 发送到存储的每个命令,包装具有该隔离级别的事务。

在我的情况下效果很好,因为我们通过 API 写入数据,而该数据不是基于数据库的读数。 (数据可能会因为脏读而损坏),所以工作正常。

【讨论】:

我不得不更改 SetTransaction 以检查现有事务的连接,因为 EF 可以为多个命令使用相同的连接。使用此示例从连接中获取事务:***.com/a/12284267/169060 这不会使得 每个 查询都在读取未提交的行吗?您如何才能使这项工作仅适用于特定查询?【参考方案4】:

在我们的项目中,我们使用@Cem Mutlu 和@anotherNeo 建议的第二种和第三种解决方案的组合。

使用 Sql Profiler 的实验表明我们必须使用一对命令:

阅读未提交 已提交阅读

因为 NET 通过 SqlConnectionPool 重用连接

internal class NoLockInterceptor : DbCommandInterceptor

    public static readonly string SET_READ_UNCOMMITED = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
    public static readonly string SET_READ_COMMITED = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";

    public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    
        if (!interceptionContext.DbContexts.OfType<IFortisDataStoreNoLockContext>().Any())
        
            return;
        

        ExecutingBase(command);
    

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    
        if (!interceptionContext.DbContexts.OfType<IFortisDataStoreNoLockContext>().Any())
        
            return;
        

        ExecutingBase(command);
    

    private static void ExecutingBase(DbCommand command)
    
        var text = command.CommandText;
        command.CommandText = $"SET_READ_UNCOMMITED Environment.NewLine text Environment.NewLine SET_READ_COMMITED";
    

【讨论】:

【参考方案5】:

首先,请为 gds03 的回答点赞。因为没有它我就不会走到这一步。

我也为“关闭事务”做出了贡献,并为 IDataReader/DbDataReader 情况提供了正确的时机。 基本上,在 IDataReader/DbDataReader 情况下,您不会关闭“ReaderExecuted”(Async) 方法上的事务(强调 Executed 的“ed”),而是让它“通过”(覆盖)DataReaderDisposing。

但是,如果您阅读了其他一些答案(此处)(和 cmets),我认为 SetEndTransaction 是 .. 不从连接池中获取巫毒的重要部分(如果您可能不会关闭交易) (对我而言)未提交的)。

using System.Data;
using System.Data.Common;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace My.Interceptors

    public class IsolationLevelInterceptor : DbCommandInterceptor
    
        private IsolationLevel _isolationLevel;

        public IsolationLevelInterceptor(IsolationLevel level)
        
            _isolationLevel = level;
        

        //[ThreadStatic]
        //private DbCommand _command;


        public override InterceptionResult DataReaderDisposing(DbCommand command, DataReaderDisposingEventData eventData, InterceptionResult result)
        
            InterceptionResult returnItem = base.DataReaderDisposing(command, eventData, result);
            SetEndTransaction(command);
            return returnItem;
        




        public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
        
            SetStartTransaction(command);
            InterceptionResult<DbDataReader> returnItem = base.ReaderExecuting(command, eventData, result);
            return returnItem;
        


        public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
        
            DbDataReader returnItem = base.ReaderExecuted(command, eventData, result);
            //SetEndTransaction(command); // DO NOT DO THIS HERE .. datareader still open and working .. fall back on DataReaderDisposing... you don't really need this override, but left in to show the possible issue.
            return returnItem;
        


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


        public override ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default)
        
            ValueTask<DbDataReader> returnItem = base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
            //SetEndTransaction(command); // DO NOT DO THIS HERE .. datareader still open and working .. fall back on DataReaderDisposing... you don't really need this override, but left in to show the possible issue.
            return returnItem;
        


        public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> interceptionContext)
        
            SetStartTransaction(command);
            InterceptionResult<object> returnItem = base.ScalarExecuting(command, eventData, interceptionContext);
            return returnItem;
        

        public override object ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object result)
        
            SetEndTransaction(command);
            object returnItem = base.ScalarExecuted(command, eventData, result);
            return returnItem;
        


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


        public override ValueTask<object> ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object result, CancellationToken cancellationToken = default)
        
            SetEndTransaction(command);
            ValueTask<object> returnItem = base.ScalarExecutedAsync(command, eventData, result, cancellationToken);
            return returnItem;
        


        /* start maybe not needed on queries that only do "reading", but listed here anyways */

        public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result)
        
            SetStartTransaction(command);
            InterceptionResult<int> returnItem = base.NonQueryExecuting(command, eventData, result);
            return returnItem;
        

        public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)
        
            int returnValue = base.NonQueryExecuted(command, eventData, result);
            SetEndTransaction(command);
            return returnValue;
        

        public override ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default)
        
            SetStartTransaction(command);
            ValueTask<InterceptionResult<int>> returnItem = base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);
            return returnItem;
        

        public override ValueTask<int> NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default)
        
            ValueTask<int> returnValue = base.NonQueryExecutedAsync(command, eventData, result, cancellationToken);
            SetEndTransaction(command);
            return returnValue;
        

        /* end maybe not needed on queries that only do "reading", but listed here anyways */

        private void SetStartTransaction(DbCommand command)
        
            if (command != null)
            
                if (command.Transaction == null)
                
                    DbTransaction t = command.Connection.BeginTransaction(_isolationLevel);
                    command.Transaction = t;
                    //_command = command;
                
            
        

        private void SetEndTransaction(DbCommand command)
        
            if (command != null)
            
                if (command.Transaction != null)
                
                    command.Transaction.Commit();
                    //_command = command;
                

                command.Dispose();
            
        

    

这篇文章有助于“了解”所有“ing”和“ed”方法。

https://lizzy-gallagher.github.io/query-interception-entity-framework/

请注意我的答案是 EntityFrameworkCore (3.1.+),但我认为它将“反向移植”到 EF-for-DN-Framework。

我的回答中更重要的部分是某些方法的“时机”……尤其是 IDataReader/DbDataReader 方法。

【讨论】:

以上是关于获取实体框架 6 在其下面的 SELECT 语句中使用 NOLOCK的主要内容,如果未能解决你的问题,请参考以下文章

在实体框架核心中获取最新记录

如何从实体框架 6 中的 Auditlog 实体获取 id

Spring Boot Jpa框架自定义查询语句返回自定义实体

如何获取实体框架生成的sql [重复]

从 SQL SELECT 语句中获取值,即使它在表中不存在

将 TransactionScope 与实体框架 6 一起使用