Ответ 1
Вы можете использовать SqlQuery для записи исходного SQL вместо того, чтобы сгенерировать его.
MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
Я столкнулся с ситуацией, когда что-то, что хорошо работало с LINQ to SQL, кажется очень тупым (или, возможно, невозможным) с Entity Framework. В частности, у меня есть объект, который включает свойство rowversion
(как для управления версиями, так и для concurrency). Что-то вроде:
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 поймут? Или принуждать массивы к другим типам, чтобы сравнение могло пройти мимо компилятора?
Вы можете использовать SqlQuery для записи исходного SQL вместо того, чтобы сгенерировать его.
MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
Обнаружено обходное решение, которое отлично работает! Проверено на платформе сущностей 6.1.3.
Невозможно использовать оператор <
с байтовыми массивами, потому что система типа С# предотвращает это (как и должно). Но то, что вы можете сделать, это построить точный синтаксис с помощью выражений, и есть лазейка, которая позволяет вам снять это.
Если вы не хотите полного объяснения, вы можете перейти к разделу "Решение".
Если вы не знакомы с выражениями, выполните курс сбоя 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));
Вот как мы обходимся с ошибкой компилятора, если мы попытаемся использовать объекты <
on 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 не работает, потому что он проходит этап, где он копирует выражение LessThan
с параметрами left
и right
, но не копирует параметр MethodInfo
, который мы используем для лазейки.)
Вы можете выполнить это в кодовом коде 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<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();
Я закончил выполнение необработанного запроса:
ctx.Database.SqlQuery( "SELECT * FROM [TABLENAME] WHERE (CONVERT (bigint, @@DBTS) > " + X)). ToList();
Это лучшее решение, но проблема с производительностью. Будет изменен параметр @ver. Блокировать столбцы в том, где предложение плохо связано с базой данных.
Преобразование типов в выражении может повлиять на "SeekPlan" в выборе плана запроса
MyContext.Foos.SqlQuery( "SELECT * FROM Foos WHERE Version > @ver", новый SqlParameter ( "ver", lastFoo.Version));
Без броска. MyContext.Foos.SqlQuery( "SELECT * FROM Foos WHERE Version > @ver", новый SqlParameter ( "ver", lastFoo.Version).SqlDbType = SqlDbType.Timestamp);
Вот еще одно обходное решение, доступное 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>
Обратите внимание, что я не написал код для создания функций с использованием API, доступных в Code First, но аналогично коду, предложенному Дрю или условным обозначениям модели, которые я написал некоторое время назад для UDF https://github.com/divega/UdfCodeFirstSample, должен работать
Определение метода (это относится к исходному коду С#):
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;
}
}
}
Отметьте также, что я определил методы как методы расширения по байт [], хотя это необязательно. Я также представил реализации методов, чтобы они работали, если вы оцениваете их вне запросов, но вы также можете выбрать "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));
}
}
}
}
Я расширил ответ jnm2s, чтобы скрыть уродливый код выражения в методе расширения
Использование:
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;
}
}
(Следующий ответ Дэймона Уоррена скопирован отсюда):
Вот что мы сделали, чтобы решить эту проблему:
Используйте расширение сравнения следующим образом:
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);
Причина, по которой это работает без реализации С#, заключается в том, что метод расширения сравнения никогда не вызывается, а EF LINQ упрощает x.compare(y) > 0
до x > y