如何在EF Core中实现Select For Update

时间:2016-06-23 06:57:36

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

据我所知,EF(和EF Core)中没有明确锁定我正在查询的资源的选项,但是我会经常需要这个功能并且不会真的感觉像是摔倒了每次我需要时回到编写select语句。

因为我只需要它用于postgres而according to the spec FOR UPDATE是查询中的最后一项,我想到的最简单的方法是获取如下所述的select语句:{{3}并附加FOR UPDATE并直接执行它。然而,这将给我一个带参数占位符的查询,或者不是一个准备好的查询,这意味着执行计划的缓存不会真正适用于postgres,所以无论如何都是不行的。

Linq to SQL有方法In Linq to Entities can you convert an IQueryable into a string of SQL?但是在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支持的最后一个选项,之后不再有任何其他选项了)

2 个答案:

答案 0 :(得分:2)

这项工作对我来说使用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
}

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

答案 1 :(得分:0)

根据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;
            }