我遇到过一个案例,在这个案例中,与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将理解的数组的比较?或者将数组强制转换为其他类型,以便比较可以通过编译器?
答案 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
表达式与left
和right
参数复制但不复制{{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 < 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 <= 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 > 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 >= 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)
答案 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