21

我遇到了这样一种情况,即与 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 to SQL 之前做过类似的事情,它映射rowversionSystem.Data.Linq.Binary,可以进行比较。(至少在表达式树可以映射回数据库的范围内。)

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

4

10 回答 10

13

找到了一个完美的解决方法!在实体框架 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));

伟大的!现在我们所需要的只是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,但我在那里打开了一个问题以获得更全面的支持,而无需表达式。(EF Core 不起作用,因为它经历了一个阶段,它使用and参数复制LessThan表达式,但不复制我们用于漏洞的参数。)leftrightMethodInfo

于 2016-07-01T18:38:01.617 回答
5

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

MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
于 2011-09-16T17:19:04.890 回答
3

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

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

CREATE FUNCTION [common].[IsNewerThan]
(
    @CurrVersion varbinary(8),
    @BaseVersion varbinary(8)
) ...

在构建 EF 上下文时,您必须在 store 模型中手动定义函数,如下所示:

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
于 2013-11-26T19:07:04.237 回答
2

我扩展了 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;
    }



}
于 2017-06-22T14:00:14.583 回答
1

这种方法对我有用,并且可以避免篡改原始 SQL:

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

我猜想原始 SQL 会更有效。

于 2014-04-03T06:57:03.643 回答
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();
于 2012-09-09T12:12:54.473 回答
0

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

于 2014-11-12T12:11:27.280 回答
0

这是最好的解决方案,但存在性能问题。参数@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);

于 2015-03-18T15:59:02.257 回答
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));

            }
        }
    }
}
于 2016-07-21T21:56:06.963 回答
0

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

The reason this works without a C# implementation is because the compare extension method is never actually called, and EF LINQ simplifies x.compare(y) > 0 down to x > y

于 2019-06-24T11:14:14.040 回答