如何根据rowversion/timestamp值查询Code First实体

本文关键字:查询 Code First 实体 timestamp 何根 rowversion | 更新日期: 2023-09-27 18:08:49

我遇到过这样一个情况,在LINQ to SQL中工作得很好的东西在实体框架中似乎非常迟钝(或者可能不可能)。具体来说,我有一个包含rowversion属性(用于版本控制和并发控制)的实体。比如:

public class Foo
{
  [Key]
  [MaxLength(50)]
  public string FooId { get; set; }
  [Timestamp]
  [ConcurrencyCheck]
  public byte[] Version { get; set; }
}

我希望能够将实体作为输入,并找到最近更新的所有其他实体。比如:

Foo lastFoo = GetSomeFoo();
var recent = MyContext.Foos.Where(f => f.Version > lastFoo.Version);

现在,在数据库中这将工作:两个rowversion值可以相互比较而没有任何问题。在使用LINQ到SQL之前,我做了类似的事情,它将rowversion映射到System.Data.Linq.Binary,可以进行比较。(至少在表达式树可以映射回数据库的程度上)

但是在Code First中,属性的类型必须是byte[]。两个数组不能用常规的比较操作符进行比较。是否有一些其他的方法来编写数组的比较,LINQ到实体将理解?还是将数组强制转换为其他类型,以便编译器可以通过比较?

如何根据rowversion/timestamp值查询Code First实体

找到一个工作完美的解决方案!在实体框架6.1.3上测试。

无法对字节数组使用<操作符,因为c#类型系统不允许这样做(它应该这样做)。但是您可以做的是使用表达式构建完全相同的语法,并且有一个漏洞允许您完成此操作。

第一步

如果你不想要完整的解释,你可以跳到解决方案部分。

如果你不熟悉表达式,这里是MSDN的速成班。

基本上,当你输入queryable.Where(obj => obj.Id == 1)时,编译器实际上输出的东西和你输入的一样:

var objParam = Expression.Parameter(typeof(ObjType));
queryable.Where(Expression.Lambda<Func<ObjType, bool>>(
    Expression.Equal(
        Expression.Property(objParam, "Id"),
        Expression.Constant(1)),
    objParam))

该表达式是数据库提供程序解析以创建查询的内容。这显然比原来的代码要冗长得多,但它也允许您进行元编程,就像您进行反射一样。冗长是这种方法唯一的缺点。这是一个比其他答案更好的缺点,比如必须编写原始SQL或不能使用参数。

在我的情况下,我已经使用表达式,但在你的情况下,第一步是用表达式重写你的查询:

Foo lastFoo = GetSomeFoo();
var fooParam = Expression.Parameter(typeof(Foo));
var recent = MyContext.Foos.Where(Expression.Lambda<Func<Foo, bool>>(
    Expression.LessThan(
        Expression.Property(fooParam, nameof(Foo.Version)),
        Expression.Constant(lastFoo.Version)),
    fooParam));

这就是我们如何解决编译错误,如果我们试图在byte[]对象上使用<。现在,我们得到的不是编译错误,而是运行时异常,因为Expression.LessThan试图找到byte[].op_LessThan,但运行时失败了。

漏洞

为了消除这个运行时错误,我们将告诉Expression.LessThan使用什么方法,这样它就不会试图找到不存在的默认方法(byte[].op_LessThan):

var recent = MyContext.Foos.Where(Expression.Lambda<Func<Foo, bool>>(
    Expression.LessThan(
        Expression.Property(fooParam, nameof(Foo.Version)),
        Expression.Constant(lastFoo.Version),
        false,
        someMethodThatWeWrote), // So that Expression.LessThan doesn't try to find the non-existent default operator method
    fooParam));

很棒!现在我们所需要的是用一个签名为bool (byte[], byte[])的静态方法创建MethodInfo someMethodThatWeWrote,以便在运行时与其他表达式匹配。

解决方案

你需要一个小的DbFunctionExpressions.cs。以下是删节版:

public static class DbFunctionExpressions
{
    private static readonly MethodInfo BinaryDummyMethodInfo = typeof(DbFunctionExpressions).GetMethod(nameof(BinaryDummyMethod), BindingFlags.Static | BindingFlags.NonPublic);
    private static bool BinaryDummyMethod(byte[] left, byte[] right)
    {
        throw new NotImplementedException();
    }
    public static Expression BinaryLessThan(Expression left, Expression right)
    {
        return Expression.LessThan(left, right, false, BinaryDummyMethodInfo);
    }
}
使用

var recent = MyContext.Foos.Where(Expression.Lambda<Func<Foo, bool>>(
    DbFunctionExpressions.BinaryLessThan(
        Expression.Property(fooParam, nameof(Foo.Version)),
        Expression.Constant(lastFoo.Version)),            
    fooParam));
指出

不能在Entity Framework Core 1.0.0上工作,但我在那里打开了一个问题,以便在不需要表达式的情况下提供更全面的支持。(EF Core不工作,因为它经历了一个阶段,它复制LessThan表达式与leftright参数,但不复制MethodInfo参数我们使用的漏洞。)

您可以使用SqlQuery来编写原始SQL,而不是让它生成。

MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));

可以在EF - 6代码中首先通过将c#函数映射到数据库函数来实现这一点。这需要一些调整,并不能产生最有效的SQL,但它完成了工作。

首先,在数据库中创建一个函数来测试更新的行版本。我是

CREATE FUNCTION [common].[IsNewerThan]
(
    @CurrVersion varbinary(8),
    @BaseVersion varbinary(8)
) ...
在构造EF上下文时,必须在存储模型中手动定义函数,如下所示:
private static DbCompiledModel GetModel()
{
    var builder = new DbModelBuilder();
    ... // your context configuration
    var model = builder.Build(...); 
    EdmModel store = model.GetStoreModel();
    store.AddItem(GetRowVersionFunctionDef(model));
    DbCompiledModel compiled = model.Compile();
    return compiled;
}
private static EdmFunction GetRowVersionFunctionDef(DbModel model)
{
    EdmFunctionPayload payload = new EdmFunctionPayload();
    payload.IsComposable = true;
    payload.Schema = "common";
    payload.StoreFunctionName = "IsNewerThan";
    payload.ReturnParameters = new FunctionParameter[]
    {
        FunctionParameter.Create("ReturnValue", 
            GetStorePrimitiveType(model, PrimitiveTypeKind.Boolean), ParameterMode.ReturnValue)
    };
    payload.Parameters = new FunctionParameter[]
    {
        FunctionParameter.Create("CurrVersion",  GetRowVersionType(model), ParameterMode.In),
        FunctionParameter.Create("BaseVersion",  GetRowVersionType(model), ParameterMode.In)
    };
    EdmFunction function = EdmFunction.Create("IsRowVersionNewer", "EFModel",
        DataSpace.SSpace, payload, null);
    return function;
}
private static EdmType GetStorePrimitiveType(DbModel model, PrimitiveTypeKind typeKind)
{
    return model.ProviderManifest.GetStoreType(TypeUsage.CreateDefaultTypeUsage(
        PrimitiveType.GetEdmPrimitiveType(typeKind))).EdmType;
}
private static EdmType GetRowVersionType(DbModel model)
{
    // get 8-byte array type
    var byteType = PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Binary);
    var usage = TypeUsage.CreateBinaryTypeUsage(byteType, true, 8);
    // get the db store type
    return model.ProviderManifest.GetStoreType(usage).EdmType;
}

用DbFunction属性修饰静态方法,为方法创建代理。EF使用它将方法与存储模型中的命名方法关联起来。使它成为一个扩展方法可以产生更简洁的LINQ。

[DbFunction("EFModel", "IsRowVersionNewer")]
public static bool IsNewerThan(this byte[] baseVersion, byte[] compareVersion)
{
    throw new NotImplementedException("You can only call this method as part of a LINQ expression");
}
<标题> 例子

最后,从LINQ调用方法到标准表达式中的实体。

    using (var db = new OrganizationContext(session))
    {
        byte[] maxRowVersion = db.Users.Max(u => u.RowVersion);
        var newer = db.Users.Where(u => u.RowVersion.IsNewerThan(maxRowVersion)).ToList();
    }

生成T-SQL来实现你想要的,使用你定义的上下文和实体集。

WHERE ([common].[IsNewerThan]([Extent1].[RowVersion], @p__linq__0)) = 1',N'@p__linq__0 varbinary(8000)',@p__linq__0=0x000000000001DB7B

我扩展了jnm2的答案,将丑陋的表达式代码隐藏在扩展方法

用法:

ctx.Foos.WhereVersionGreaterThan(r => r.RowVersion, myVersion);

扩展方法:

public static class RowVersionEfExtensions
{

    private static readonly MethodInfo BinaryGreaterThanMethodInfo = typeof(RowVersionEfExtensions).GetMethod(nameof(BinaryGreaterThanMethod), BindingFlags.Static | BindingFlags.NonPublic);
    private static bool BinaryGreaterThanMethod(byte[] left, byte[] right)
    {
        throw new NotImplementedException();
    }
    private static readonly MethodInfo BinaryLessThanMethodInfo = typeof(RowVersionEfExtensions).GetMethod(nameof(BinaryLessThanMethod), BindingFlags.Static | BindingFlags.NonPublic);
    private static bool BinaryLessThanMethod(byte[] left, byte[] right)
    {
        throw new NotImplementedException();
    }
    /// <summary>
    /// Filter the query to return only rows where the RowVersion is greater than the version specified
    /// </summary>
    /// <param name="query">The query to filter</param>
    /// <param name="propertySelector">Specifies the property of the row that contains the RowVersion</param>
    /// <param name="version">The row version to compare against</param>
    /// <returns>Rows where the RowVersion is greater than the version specified</returns>
    public static IQueryable<T> WhereVersionGreaterThan<T>(this IQueryable<T> query, Expression<Func<T, byte[]>> propertySelector, byte[] version)
    {
        var memberExpression = propertySelector.Body as MemberExpression;
        if (memberExpression == null) { throw new ArgumentException("Expression should be of form r=>r.RowVersion"); }
        var propName = memberExpression.Member.Name;
        var fooParam = Expression.Parameter(typeof(T));
        var recent = query.Where(Expression.Lambda<Func<T, bool>>(
            Expression.GreaterThan(
                Expression.Property(fooParam, propName),
                Expression.Constant(version),
                false,
                BinaryGreaterThanMethodInfo),
            fooParam));
        return recent;
    }

    /// <summary>
    /// Filter the query to return only rows where the RowVersion is less than the version specified
    /// </summary>
    /// <param name="query">The query to filter</param>
    /// <param name="propertySelector">Specifies the property of the row that contains the RowVersion</param>
    /// <param name="version">The row version to compare against</param>
    /// <returns>Rows where the RowVersion is less than the version specified</returns>
    public static IQueryable<T> WhereVersionLessThan<T>(this IQueryable<T> query, Expression<Func<T, byte[]>> propertySelector, byte[] version)
    {
        var memberExpression = propertySelector.Body as MemberExpression;
        if (memberExpression == null) { throw new ArgumentException("Expression should be of form r=>r.RowVersion"); }
        var propName = memberExpression.Member.Name;
        var fooParam = Expression.Parameter(typeof(T));
        var recent = query.Where(Expression.Lambda<Func<T, bool>>(
            Expression.LessThan(
                Expression.Property(fooParam, propName),
                Expression.Constant(version),
                false,
                BinaryLessThanMethodInfo),
            fooParam));
        return recent;
    }

}

此方法适用于我,并避免篡改原始SQL:

var recent = MyContext.Foos.Where(c => BitConverter.ToUInt64(c.RowVersion.Reverse().ToArray(), 0) > fromRowVersion);

我猜原始SQL会更有效。

我发现这个方法很有用:

byte[] rowversion = BitConverter.GetBytes(revision);
var dbset = (DbSet<TEntity>)context.Set<TEntity>();
string query = dbset.Where(x => x.Revision != rowversion).ToString()
    .Replace("[Revision] <> @p__linq__0", "[Revision] > @rowversion");
return dbset.SqlQuery(query, new SqlParameter("rowversion", rowversion)).ToArray();

我最终执行了一个原始查询:
ctx.Database。SqlQuery("SELECT * FROM [TABLENAME] WHERE(CONVERT(bigint,@@DBTS)>" + X)). tolist ();

这是最好的解决方案,但是有性能问题。参数@ver将被强制转换。将where子句中的列强制转换到数据库

表达式中的类型转换可能影响查询计划选择中的"SeekPlan"

MyContext.Foos。SqlQuery("SELECT * FROM foo WHERE Version> @ver", new SqlParameter("ver", lastFoo.Version));

没有。MyContext.Foos。SqlQuery("SELECT * FROM foo WHERE Version> @ver",新建SqlParameter("ver", lastFoo.Version)。

这是EF 6可用的另一个解决方案。它不需要在数据库中创建函数,而是使用模型定义的函数。

函数定义(在CSDL文件的节中,如果使用EDMX文件,则在节中):

<Function Name="IsLessThan" ReturnType="Edm.Boolean" >
  <Parameter Name="source" Type="Edm.Binary" MaxLength="8" />
  <Parameter Name="target" Type="Edm.Binary" MaxLength="8" />
  <DefiningExpression>source &lt; target</DefiningExpression>
</Function>
<Function Name="IsLessThanOrEqualTo" ReturnType="Edm.Boolean" >
  <Parameter Name="source" Type="Edm.Binary" MaxLength="8" />
  <Parameter Name="target" Type="Edm.Binary" MaxLength="8" />
  <DefiningExpression>source &lt;= target</DefiningExpression>
</Function>
<Function Name="IsGreaterThan" ReturnType="Edm.Boolean" >
  <Parameter Name="source" Type="Edm.Binary" MaxLength="8" />
  <Parameter Name="target" Type="Edm.Binary" MaxLength="8" />
  <DefiningExpression>source &gt; target</DefiningExpression>
</Function>
<Function Name="IsGreaterThanOrEqualTo" ReturnType="Edm.Boolean" >
  <Parameter Name="source" Type="Edm.Binary" MaxLength="8" />
  <Parameter Name="target" Type="Edm.Binary" MaxLength="8" />
  <DefiningExpression>source &gt;= target</DefiningExpression>
</Function>

请注意,我还没有编写代码来使用code First中提供的api来创建函数,但是类似于Drew提出的代码或我不久前为udf https://github.com/divega/UdfCodeFirstSample编写的模型约定,应该可以工作

方法定义(在你的c#源代码中):

using System.Collections;
using System.Data.Objects.DataClasses;
namespace TimestampComparers
{
    public static class TimestampComparers
    {
        [EdmFunction("TimestampComparers", "IsLessThan")]
        public static bool IsLessThan(this byte[] source, byte[] target)
        {
            return StructuralComparisons.StructuralComparer.Compare(source, target) == -1;
        }
        [EdmFunction("TimestampComparers", "IsGreaterThan")]
        public static bool IsGreaterThan(this byte[] source, byte[] target)
        {
            return StructuralComparisons.StructuralComparer.Compare(source, target) == 1;
        }
        [EdmFunction("TimestampComparers", "IsLessThanOrEqualTo")]
        public static bool IsLessThanOrEqualTo(this byte[] source, byte[] target)
        {
            return StructuralComparisons.StructuralComparer.Compare(source, target) < 1;
        }
        [EdmFunction("TimestampComparers", "IsGreaterThanOrEqualTo")]
        public static bool IsGreaterThanOrEqualTo(this byte[] source, byte[] target)
        {
            return StructuralComparisons.StructuralComparer.Compare(source, target) > -1;
        }
    }
}

还请注意,我已经将这些方法定义为byte[]的扩展方法,尽管这是不必要的。我还提供了这些方法的实现,以便在查询之外对它们求值时它们可以工作,但是您也可以选择抛出NotImplementedException。当你在LINQ to Entities查询中使用这些方法时,我们将永远不会真正调用它们。也不是说我已经为EdmFunctionAttribute"TimestampComparers"做了第一个参数。这必须与概念模型部分中指定的名称空间匹配。

用法:

using System.Linq;
namespace TimestampComparers
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new OrdersContext())
            {
                var stamp = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, };
                var lt = context.OrderLines.FirstOrDefault(l => l.TimeStamp.IsLessThan(stamp));
                var lte = context.OrderLines.FirstOrDefault(l => l.TimeStamp.IsLessThanOrEqualTo(stamp));
                var gt = context.OrderLines.FirstOrDefault(l => l.TimeStamp.IsGreaterThan(stamp));
                var gte = context.OrderLines.FirstOrDefault(l => l.TimeStamp.IsGreaterThanOrEqualTo(stamp));
            }
        }
    }
}

(以下是Damon Warren的回答):

下面是我们解决这个问题的方法:

使用如下的比较扩展:

public static class EntityFrameworkHelper
    {
        public static int Compare(this byte[] b1, byte[] b2)
        {
            throw new Exception("This method can only be used in EF LINQ Context");
        }
    }

然后你可以做

byte[] rowversion = .....somevalue;
_context.Set<T>().Where(item => item.RowVersion.Compare(rowversion) > 0);

在没有c#实现的情况下工作的原因是因为从来没有实际调用比较扩展方法,EF LINQ将x.compare(y) > 0简化为x > y