postgresql 如何在EF Core中实现Select For Update

hmae6n7t  于 2023-01-12  发布在  PostgreSQL
关注(0)|答案(3)|浏览(258)

据我所知,EF(和EF核心)中没有显式锁定我正在查询的资源的选项,但我经常需要这个功能,而且不想每次需要它时都写选择语句。
因为我只需要在postgres中使用它,并且according to the specFOR 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和它们的批处理更新/删除,但由于它们必须将选择语句转换为不同的语句,因此它们需要处理的复杂性比我大得多,所以我希望有一个更简单的解决方案。

    • 更新日期:**

如果在描述中还不清楚,我想创建一个扩展方法,如下所示:

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,就像所有其他过程一样,他们可以运行.ForUpdate(),而不是运行.ToList()。(For Update特意执行查询,以使实现更容易,而且因为FOR UPDATE是postgres支持的最后一个选项,所以之后不应该再有任何其他操作)

7qhs6swi

7qhs6swi1#

这是我使用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
}

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

xoshrz7s

xoshrz7s2#

根据this issue,在ef内核中没有简单的方法来实现锁提示和其他面向数据库的调用
我在我的项目中用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;
    }
}
ttp71kqs

ttp71kqs3#

@gustavo-rossi-muller的答案很有用,但缺乏线程安全性,因此不能与EF Core提供的async methods(如DbContext.SaveChangesAsync())一起使用,因为没有覆盖ScalarExecutingAsync()ReaderExecutingAsync()
公共静态字段HintValue上的[ThreadStatic]属性对于强制每个线程使用它们自己的变量值HintInterceptor.HintValue而不是在所有线程之间共享相同的值(又称全局变量)是必需的。
Document of interceptor已经发现
拦截器通常是无状态的,这意味着单个拦截器示例可以用于所有DbContext示例。
如果你想在每个DbContext的拦截器示例中保留一些状态,你必须:
此拦截器是有状态的:它存储最近查询的每日消息的ID和消息文本,以及执行该查询的时间。由于这种状态,我们还需要一个锁,因为缓存要求同一拦截器必须由多个上下文示例使用。
但是我们需要的是针对每个查询命令来控制拦截器的状态,因为我们只需要查询某些带有FOR UPDATE后缀的SELECT命令,而不是针对所有会引起很多语法错误的命令。
到目前为止,我们只能通过X1 E6 F1 X向某些查询命令中提供一些额外的信息,然后在X1 E7 F1 X的覆盖中检测由标记添加的注解,以便为这些查询附加X1 M11 N1 X提示。
事实上,该文件已经提供了这样做的例子。
我对该示例进行了修改,以便使用MySQL语法附加FOR UPDATE

private class SelectForUpdateCommandInterceptor : DbCommandInterceptor
{ // https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#example-command-interception-to-add-query-hints
    public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result)
    {
        ManipulateCommand(command);
        return result;
    }

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

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

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

    private static void ManipulateCommand(IDbCommand command)
    {
        if (command.CommandText.StartsWith("-- ForUpdate", StringComparison.Ordinal))
        {
            command.CommandText += " FOR UPDATE";
        }
    }
}

然后在配置DbContext时插入此拦截器:

private static readonly SelectForUpdateCommandInterceptor SelectForUpdateCommandInterceptorInstance = new();
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.AddInterceptors(SelectForUpdateCommandInterceptorInstance);
}

最后,我们可以做到:

var results = (from e in db.Set<SomeEntity>.TagWith("ForUpdate")
    where e.SomeField == someValue
    select e.SomeField).ToList();
db.Set<SomeEntity>.Add(new SomeEntity {SomeField = 1});
db.SaveChanges();
db.SaveChangesAsync(); // thread safe

在EF Core 7中,他们还没有计划实施此查询提示后缀:https://github.com/dotnet/efcore/issues/26042,但另一个名为linq2db的linq 2sql表达式翻译器已经完成了这一操作:
https://github.com/linq2db/linq2db/issues/1276
https://github.com/linq2db/linq2db/pull/3297
https://github.com/linq2db/linq2db/issues/3905

相关问题