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

我遇到过一个案例,其中与LINQ to SQL相当不错的东西似乎对Entity Framework来说非常迟钝(或者可能是不可能的)。 具体来说,我有一个包含rowversion属性的实体(用于版本控制和并发控制)。 就像是:

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

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

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

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

但在Code First中,属性的类型必须是byte[] 。 并且两个数组无法与常规比较运算符进行比较。 有没有其他方法来编写LINQ to Entities将理解的数组的比较? 或者将数组强制转换为其他类型,以便比较可以通过编译器?

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

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

找到一个完美的解决方法! 在Entity Framework 6.1.3上测试。

没有办法将<运算符与字节数组一起使用,因为C#类型系统会阻止它(应该如此)。 但你可以做的是使用表达式构建完全相同的语法,并且有一个漏洞可以让你解决这个问题。

第一步

如果您不想要完整的说明,可以跳到“解决方案”部分。

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

基本上,当你输入queryable.Where(obj => obj.Id == 1) ,编译器确实输出了与输入相同的内容:

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

该表达式是数据库提供程序解析以创建查询的表达式。 这显然比原始版本更冗长,但它也允许您像进行reflection一样进行元编程。 详细程度是这种方法的唯一缺点。 这是一个比其他答案更好的缺点,比如必须编写原始SQL或不能使用参数。

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

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

如果我们尝试使用< on byte[]对象,这就是我们解决编译器错误的方法。 现在,我们得到运行时exception而不是编译器错误,因为Expression.LessThan尝试查找byte[].op_LessThan并在运行时失败。 这就是漏洞的来源。

漏洞

为了摆脱那个运行时错误,我们将告诉Expression.LessThan使用什么方法,以便它不会尝试找到不存在的默认值( byte[].op_LessThan ):

 var recent = MyContext.Foos.Where(Expression.Lambda>( 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)); 

大! 现在我们需要的是MethodInfo someMethodThatWeWrote从一个带有签名bool (byte[], byte[])的静态方法创建,以便类型在运行时与我们的其他表达式匹配。

你需要一个小的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>( DbFunctionExpressions.BinaryLessThan( Expression.Property(fooParam, nameof(Foo.Version)), Expression.Constant(lastFoo.Version)), fooParam)); 
  • 请享用。

笔记

不适用于Entity Framework Core 1.0.0,但我在那里提出了一个问题 ,无需表达式即可获得更全面的支持。 (EF Core不起作用,因为它经历了一个阶段,它使用right参数复制LessThan表达式,但不复制我们用于漏洞的MethodInfo参数。)

您可以通过将C#函数映射到数据库函数来在EF 6代码中完成此操作。 它花了一些调整,并没有产生最有效的SQL,但它完成了工作。

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

 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 

这个方法适合我,避免篡改原始SQL:

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

我猜想原始SQL会更有效率。

我发现这个解决方法很有用:

 byte[] rowversion = BitConverter.GetBytes(revision); var dbset = (DbSet)context.Set(); 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 Foos WHERE Version> @ver”,new SqlParameter(“ver”,lastFoo.Version));

没有演员。 MyContext.Foos.SqlQuery(“SELECT * FROM Foos WHERE Version> @ver”,new SqlParameter(“ver”,lastFoo.Version).SqlDbType = SqlDbType.Timestamp);

以下是EF 6.x可用的另一种解决方法,它不需要在数据库中创建函数,而是使用模型定义的函数。

函数定义(这在你的CSDL文件的部分内部,如果你使用的是EDMX文件,则在内部部分):

    source < target     source <= target     source > target     source >= target  

请注意,我没有使用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)); } } } } 

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

用法:

 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(); } ///  /// Filter the query to return only rows where the RowVersion is greater than the version specified ///  /// The query to filter /// Specifies the property of the row that contains the RowVersion /// The row version to compare against /// Rows where the RowVersion is greater than the version specified public static IQueryable WhereVersionGreaterThan(this IQueryable query, Expression> 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>( Expression.GreaterThan( Expression.Property(fooParam, propName), Expression.Constant(version), false, BinaryGreaterThanMethodInfo), fooParam)); return recent; } ///  /// Filter the query to return only rows where the RowVersion is less than the version specified ///  /// The query to filter /// Specifies the property of the row that contains the RowVersion /// The row version to compare against /// Rows where the RowVersion is less than the version specified public static IQueryable WhereVersionLessThan(this IQueryable query, Expression> 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>( Expression.LessThan( Expression.Property(fooParam, propName), Expression.Constant(version), false, BinaryLessThanMethodInfo), fooParam)); return recent; } }