如何根据 rowversion/timestamp 值查询 Code First 实体?
我遇到过这样的情况:在 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之前也做过类似的事情,将rowversion映射到System.Data.Linq.Binary,可以进行比较。 (至少在表达式树可以映射回数据库的范围内。)
但是在 Code First 中,属性的类型必须是 byte[]
。并且两个数组无法使用常规比较运算符进行比较。是否有其他方法来编写 LINQ to Entities 能够理解的数组比较?或者将数组强制转换为其他类型以便比较可以通过编译器?
I've run into a case where something that worked fairly well with LINQ to SQL seems to be very obtuse (or maybe impossible) with the Entity Framework. Specifically, I've got an entity that includes a rowversion
property (both for versioning and concurrency control). Something like:
public class Foo
{
[Key]
[MaxLength(50)]
public string FooId { get; set; }
[Timestamp]
[ConcurrencyCheck]
public byte[] Version { get; set; }
}
I would like to be able to take a entity as input, and find all of the other entities that are more recently updated. Something like:
Foo lastFoo = GetSomeFoo();
var recent = MyContext.Foos.Where(f => f.Version > lastFoo.Version);
Now, in the database this would work: two rowversion
values can be compared to one another without any problems. And I've done a similar thing before using LINQ to SQL, which maps the rowversion
to System.Data.Linq.Binary
, which can be compared. (At least to the extent that the expression tree can be mapped back to the database.)
But in Code First, the type of the property must be byte[]
. And two arrays can't be compared with the regular comparison operators. Is there some other way to write the comparison of the arrays that LINQ to Entities will understand? Or to coerce the arrays into other types so that the comparison can get past the compiler?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(10)
找到了一个完美的解决方法!在实体框架 6.1.3 上测试。
无法对字节数组使用
<
运算符,因为 C# 类型系统会阻止这种情况(它应该这样做)。但是您可以做的是使用表达式构建完全相同的语法,并且有一个漏洞可以让您实现这一点。第一步
如果您不需要完整的解释,可以跳至解决方案部分。
如果您不熟悉表达式,请参阅MSDN 速成课程。
基本上,当您输入
queryable.Where(obj => obj.Id == 1)
时,编译器实际上会输出与您输入相同的内容:该表达式是数据库提供程序解析的内容创建您的查询。这显然比原来的冗长得多,但它也允许您像进行反射一样进行元编程。冗长是此方法的唯一缺点。与这里的其他答案相比,这是一个更好的缺点,例如必须编写原始 SQL 或无法使用参数。
就我而言,我已经在使用表达式,但在您的情况下,第一步是使用表达式重写您的查询:
这就是我们如何解决如果我们尝试使用
<
时出现的编译器错误在byte[]
对象上。现在,我们得到的不再是编译器错误,而是运行时异常,因为Expression.LessThan
尝试查找byte[].op_LessThan
但在运行时失败。 这就是漏洞出现的地方。漏洞
为了消除该运行时错误,我们将告诉
Expression.LessThan
使用什么方法,这样它就不会尝试查找默认值 (byte[].op_LessThan
) 不存在:太棒了!现在我们需要的是从带有签名 bool (byte[], byte[]) 的静态方法创建的
MethodInfo someMethodThatWeWrote
,以便类型在运行时与我们的其他表达式匹配。解决方案
您需要一个小的 DbFunctionExpressions.cs。这是一个删节版本:
Usage
注释
不适用于 Entity Framework Core 1.0.0,但我打开了一个问题无论如何,无需表达即可获得更全面的支持。 (EF Core 不起作用,因为它经历了一个阶段,它使用
left
和right
参数复制LessThan
表达式,但不复制我们用于漏洞的MethodInfo
参数。)Found a workaround that works perfectly! Tested on Entity Framework 6.1.3.
There's no way to use the
<
operator with byte arrays because the C# type system prevents that (as it should). But what you can do is build the exact same syntax using expressions, and there is a loophole that allows you to pull this off.First step
If you don't want the full explanation, you can skip to the Solution section.
If you aren't familiar with expressions, here is MSDN's crash course.
Basically, when you type
queryable.Where(obj => obj.Id == 1)
the compiler really outputs the same thing as if you had typed:And that expression is what the database provider parses to create your query. This is obviously much more verbose than the original, but it also allows you do do meta-programming just like when you do reflection. The verbosity is the only downside to this method. It's a better downside than other answers here, like having to write raw SQL or not being able to use parameters.
In my case, I was already using expressions, but in your case the first step is to rewrite your query using expressions:
This is how we get around the compiler error we get if we try to use
<
onbyte[]
objects. Now instead of a compiler error, we get a runtime exception becauseExpression.LessThan
tries to findbyte[].op_LessThan
and fails at runtime. This is where the loophole comes in.Loophole
To get rid of that runtime error, we will tell
Expression.LessThan
what method to use so that it doesn't try to find the default one (byte[].op_LessThan
) which doesn't exist:Great! Now all we need is
MethodInfo someMethodThatWeWrote
created from a static method with the signaturebool (byte[], byte[])
so that the types match at runtime with our other expressions.Solution
You need a small DbFunctionExpressions.cs. Here's a truncated version:
Usage
Notes
Does not work on Entity Framework Core 1.0.0, but I opened an issue there for fuller support without the need for expressions anyway. (EF Core doesn't work because it goes through a stage where it copies the
LessThan
expression with theleft
andright
parameters but doesn't copy theMethodInfo
parameter we use for the loophole.)您可以使用 SqlQuery 编写原始 SQL,而不是生成它。
You can use SqlQuery to write the raw SQL instead of having it generated.
您可以通过将 C# 函数映射到数据库函数,在 EF 6 代码优先中完成此操作。它进行了一些调整,并没有产生最有效的 SQL,但它完成了工作。
首先,在数据库中创建一个函数来测试较新的行版本。我的是
在构建 EF 上下文时,您必须在存储模型中手动定义该函数,如下所示:
通过使用 DbFunction 属性装饰静态方法来创建该方法的代理。 EF 使用此方法将方法与存储模型中的命名方法关联起来。使其成为一种扩展方法可以产生更简洁的 LINQ。
示例
最后,在标准表达式中调用 LINQ toEntity 的方法。
这会使用您定义的上下文和实体集生成 T-SQL 来实现您想要的目的。
You can accomplish this in EF 6 code-first by mapping a C# function to a database function. It took some tweaking and doesn't produce the most efficient SQL, but it gets the job done.
First, create a function in the database to test for a newer rowversion. Mine is
When constructing your EF context, you'll have to manually define the function in the store model, like this:
Create a proxy for the method by decorating a static method with the DbFunction attribute. EF uses this to associate the method with the named method in the store model. Making it an extension method produces cleaner LINQ.
Example
Finally, call the method from LINQ to entities in a standard expression.
This generates the T-SQL to achieve what you want, using the context and entity sets you have defined.
我扩展了jnm2的答案以隐藏扩展方法中丑陋的表达式代码
用法:
扩展方法:
I extended jnm2’s answer to hide the ugly expression code in a extension method
Usage:
Extension Method:
这种方法对我来说很有效,并且可以避免篡改原始 SQL:
不过我猜原始 SQL 会更有效。
This method works for me and avoids tampering with the raw SQL:
I would guess however raw SQL would be more efficient.
我发现这个解决方法很有用:
I found this workaround usefull:
我最终执行了一个原始查询:
ctx.Database.SqlQuery("SELECT * FROM [TABLENAME] WHERE(CONVERT(bigint,@@DBTS) >" + X)).ToList();
I ended up executing a raw query:
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);
That is the best solution, but have a performance issue. The parameter @ver will be cast. Cast columns in where clause are bad to the database.
Type conversion in expression may affect "SeekPlan" in query plan choice
MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
Without cast. MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version).SqlDbType = SqlDbType.Timestamp);
这是 EF 6.x 可用的另一种解决方法,它不需要在数据库中创建函数,而是使用模型定义的函数。
函数定义(这位于 CSDL 文件的部分内,如果您使用 EDMX 文件,则位于部分内):
请注意,我尚未编写使用 Code First 中提供的 API 来创建函数的代码,但类似于以下代码Drew 提出的建议或我前段时间为 UDF 编写的模型约定 https://github.com/divega/UdfCodeFirstSample,应该可以工作
方法定义(这在您的 C# 源代码中):
另请注意,我已将这些方法定义为 byte[ 上的扩展方法] ],尽管这不是必要的。我还提供了这些方法的实现,以便在您在查询之外评估它们时它们可以工作,但您也可以选择抛出 NotImplementedException。当您在 LINQ to Entities 查询中使用这些方法时,我们永远不会真正调用它们。
另外,我也没有为 EdmFunctionAttribute“TimestampComparers”设置第一个参数。这必须与概念模型部分中指定的命名空间相匹配。
用法:
Here is yet another workaround available to EF 6.x that doesn't require creating functions in the database but uses model defined functions instead.
Function definitions (this goes inside the section in your CSDL file, or inside section if you are using EDMX files):
Note that I haven't written the code to create the functions using the APIs available in Code First, but similar to code to what Drew proposed or the model conventions I wrote some time ago for UDFs https://github.com/divega/UdfCodeFirstSample, should work
Method definition (this goes in your C# source code):
Note also that I have defined the methods as extension methods over byte[], although this is not necessary. I also provided implementations for the methods so that they work if you evaluate them outside queries, but you can choose as well to throw NotImplementedException. When you use these methods in LINQ to Entities queries, we will never really invoke them.
Also not that I have made the first argument for EdmFunctionAttribute “TimestampComparers”. This has to match the namespace specified in the section of your conceptual model.
Usage:
(Damon Warren 的以下回答是从此处复制过来的):
这是我们所做的解决此问题:
使用如下所示的比较扩展:
然后您可以执行
此操作无需 C# 实现即可工作的原因是因为从未实际调用比较扩展方法,并且 EF LINQ 简化了 x.compare(y) > 0 下降到
x > y
(The following answer by Damon Warren is copied over from here):
Here is what we did to solve this:
Use a compare extension like this:
Then you can do
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 tox > y