【问题标题】:How to implement Select For Update in EF Core如何在 EF Core 中实现 Select For Update
【发布时间】:2016-06-23 06:57:35
【问题描述】:

据我了解,在 EF(和 EF Core)中没有选项可以显式锁定我正在查询的资源,但我会经常需要这个功能,而且我真的不想摔倒回到每次我需要它时编写选择语句。

因为我只需要它用于 postgres 并且 according to the spec FOR UPDATE 是查询中的最后一项,所以我想到实现它的最简单方法是获取如下所述的 select 语句:In Linq to Entities can you convert an IQueryable into a string of SQL? 并附加 FOR UPDATE并直接执行。但是,这会给我一个带有参数占位符的查询,或者不是一个准备好的查询,这意味着执行计划的缓存在 postgres 上不会真正起作用,所以无论哪种方式都是不行的。

Linq to SQL 有 DataContext.GetCommand 方法,但在 EF 和特别是 EF Core 中似乎没有任何等效项。我还查看了 EntityFramework.Extended 及其批量更新/删除,但由于他们必须将 select 语句转换为不同的语句,因此他们需要处理比我更复杂的语句,因此我希望有一个更简单的解决方案。

更新:

如果描述不清楚,我想创建一个这样的扩展方法:

public static IList<T> ForUpdate (this IQueryable<T> me)
{
    // this line is obviously what is missing for me :)
    var theUnderlyingCommand = me.GetTheUnderlyingDbCommandOrSimilar();

    theUnderlyingCommand.Text += "FOR UPDATE";
    return me.ToList();
}

这样,其他开发人员可以通过 Linq 将 EF 用于所有其他过程,而不是运行 .ToList(),而是运行 .ForUpdate()。 (对于 Update 执行查询是为了让实现更容易,也因为 FOR UPDATE 是 postgres 支持的最后一个选项,之后应该不会再有其他任何东西了)

【问题讨论】:

  • EFCore 的主要方法是乐观并发而不是锁定,即使用某个列作为并发标记(或行版本)以确保该行在被读取后没有更新。请注意,目前 Npgsql 提供程序不支持此功能,但可能很快就会支持 (github.com/npgsql/Npgsql.EntityFrameworkCore.PostgreSQL/issues/…)。 SELECT FOR UPDATE 似乎需要在 EFCore 级别上付出很多努力,所以我怀疑它是否会很快完成,但请尝试向他们提出问题。
  • 如果你只是想执行你自己的原始 SQL,你可以在其中追加 FOR UPDATE,EFCore 提供了 FromSql 方法,或者你可以下拉到 ADO.NET 并做任何你想做的事情想要(这里有一篇好文章:elanderson.net/2016/04/execute-raw-sql-in-entity-framework-core
  • @ShayRojansky 我已经更新了这个问题,因为它可能不够清楚。我知道我可以执行原始 SQL,但我不想在我的应用程序的其余部分处理原始 SQL - 不过,我可以使用专用且经过良好测试的扩展方法来处理它。
  • 我有一个事务,但我想在我的所有表上读取,除了一个
  • @GertArnold 如果两个并发操作读取一个项目并稍后尝试更新它,则可序列化事务的工作方式完全不同,其中一个操作将简单地失败 - 但是,如果我已读取提交(或可序列化)并且都用FOR UPDATE阅读我得到了预期的结果,没有任何问题

标签: entity-framework postgresql entity-framework-core npgsql


【解决方案1】:

这项工作适合我使用 SQLServer(没有经过测试的异步方法):

首先,创建一个 DbCommandInterceptor(我叫 HintInterceptor.cs)

using System;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Text.RegularExpressions;

public class HintInterceptor : DbCommandInterceptor
{
    private static readonly Regex _tableAliasRegex = new Regex(@"(?<tableAlias>FROM +(\[.*\]\.)?(\[.*\]) AS (\[.*\])(?! WITH \(*HINT*\)))", RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled);

    [ThreadStatic]
    public static string HintValue;

    private static string Replace(string input)
    {
        if (!String.IsNullOrWhiteSpace(HintValue))
        {
            if (!_tableAliasRegex.IsMatch(input))
            {
                throw new InvalidProgramException("Não foi possível identificar uma tabela para ser marcada para atualização(forupdate)!", new Exception(input));
            }
            input = _tableAliasRegex.Replace(input, "${tableAlias} WITH (*HINT*)");
            input = input.Replace("*HINT*", HintValue);
        }
        HintValue = String.Empty;
        return input;
    }

    public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
    {
        command.CommandText = Replace(command.CommandText);
    }

    public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        command.CommandText = Replace(command.CommandText);
    }
}

所以在 Web.config 中注册你的拦截器类

<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
<providers>
  <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
</providers>
<interceptors> 
  <interceptor type="Full.Path.Of.Class.HintInterceptor, Dll.Name" />
</interceptors>
</entityFramework>

现在我创建一个名为 HintExtension 的静态类

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

就是这样,我可以在数据库事务中使用,例如:

using(var trans = context.Database.BeginTransaction())
{
        var query = context.mydbset.Where(a => a.name == "asd").ForUpdate();
        // not locked yet
        var mylist = query.ToList();
        // now are locked for update
        // update the props, call saveChanges() and finally call commit ( or rollback)
        trans.Commit();
        // now are unlocked
}

对不起我的英语,我希望我的例子会有所帮助。

【讨论】:

    【解决方案2】:

    根据this issue,在 ef core 中实现锁提示和其他面向数据库的调用没有简单的方法

    我以这种方式在我的项目中使用 MsSQL 和 ef 核心实现了 UPDLOCK:

    public static class DbContextExtensions
    {
        public static string GetUpdLockSqlForEntity<T>(this DbContext dbContext, int entityPk, bool pkContainsTableName = true) where T : class
        {
            var mapping = dbContext.Model.FindEntityType(typeof(T)).Relational();
            var tableName = mapping.TableName;
            var entityPkString = entityPk.ToString();
            string idPrefix = pkContainsTableName ? tableName.Substring(0, tableName.Length - 1) : string.Empty;
            return $"Select 1 from {tableName} with (UPDLOCK) where {idPrefix}Id = {entityPkString}";
        }
    }
    

    我们在数据库事务中使用这种方法作为原始sql调用(提交或回滚后将释放锁):

    using (var dbTran = await DataContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted))
    {
        try
        {
            await DataContext.Database.ExecuteSqlCommandAsync(DataContext.GetUpdLockSqlForEntity<Deposit>(entityId));
            dbTran.Commit();
        }
        catch (Exception e)
        {
            dbTran.Rollback();
            throw;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-28
      • 1970-01-01
      • 2022-07-14
      • 2014-11-29
      • 1970-01-01
      • 2013-12-18
      相关资源
      最近更新 更多