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

时间:2011-09-15 22:06:29

标签: c# sql-server entity-framework ef-code-first rowversion

我遇到过一个案例,在这个案例中,与LINQ to SQL相当不错的东西似乎对于Entity Framework来说是非常迟钝的(或者可能是不可能的)。具体来说,我有一个包含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 to SQL之前我做了类似的事情,它将rowversion映射到System.Data.Linq.Binary,可以进行比较。 (至少在表达式树可以映射回数据库的程度上。)

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

10 个答案:

答案 0 :(得分:9)

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

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

第一步

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

如果您不熟悉表达式,请MSDN's crash course

基本上,当你输入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));

大!现在我们需要的是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<Func<Foo, bool>>(
    DbFunctionExpressions.BinaryLessThan(
        Expression.Property(fooParam, nameof(Foo.Version)),
        Expression.Constant(lastFoo.Version)),            
    fooParam));
  • 享受。

注释

不适用于Entity Framework Core 1.0.0,但我opened an issue可以提供更全面的支持而无需表达式。 (EF Core不起作用,因为它经历了LessThan表达式与leftright参数复制但不复制{{1}的阶段我们用于漏洞的参数。)

答案 1 :(得分:5)

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

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

答案 2 :(得分:3)

您可以通过将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

答案 3 :(得分:1)

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

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

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

答案 4 :(得分:0)

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

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();

答案 5 :(得分:0)

我最终执行了原始查询:
ctx.Database.SqlQuery(&#34; SELECT * FROM [TABLENAME] WHERE(CONVERT(bigint,@@ DBTS)&gt;&#34; + X))。ToList();

答案 6 :(得分:0)

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

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

MyContext.Foos.SqlQuery(“SELECT * FROM Foos WHERE Version&gt; @ver”,new SqlParameter(“ver”,lastFoo.Version));

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

答案 7 :(得分:0)

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

函数定义(这可以在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));

            }
        }
    }
}

答案 8 :(得分:0)

我扩展jmn2s answer以隐藏扩展方法中的丑陋表达式代码

用法:

data.isnull().sum(1)

扩展方法:

1

答案 9 :(得分:0)

(达蒙·沃伦(Damon Warren)的以下回答是从here复制而来的):

这是我们要解决的问题:

使用这样的比较扩展名:

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#实现的情况下可以工作的原因是,从未真正调用过compare扩展方法,并且EF LINQ将x.compare(y) > 0简化为x > y