Как сделать запрос сущностей Code First на основе значения rowversion/timestamp?

Я сталкивался со случаем, когда что-то, что работало довольно хорошо с 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 поймет? Или привести массивы в другие типы, чтобы сравнение могло пройти мимо компилятора?

9 ответов

Решение

Вы можете использовать SqlQuery для написания необработанного SQL вместо его генерации.

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

Нашел обходной путь, который отлично работает! Протестировано на Entity Framework 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 не работает, потому что проходит этап, на котором копирует LessThan выражение с left а также right параметры, но не копирует MethodInfo параметр, который мы используем для лазейки.)

Вы можете сделать это в коде EF 6, сначала сопоставив функцию C# с функцией базы данных. Это заняло некоторую настройку и не дает наиболее эффективного SQL, но оно выполняет свою работу.

Сначала создайте в базе данных функцию для проверки новой версии строк. Мой

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

Я расширил ответ jmn2s, чтобы скрыть уродливый код выражения в методе расширения.

Использование:

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;
    }



}

Этот метод работает для меня и избегает вмешательства в сырой SQL:

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

Я предположил бы, однако, сырой SQL был бы более эффективным.

Вот еще один обходной путь, доступный для 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>

Обратите внимание, что я не написал код для создания функций с использованием API-интерфейсов, доступных в Code First, но он похож на код, предложенный Дрю, или условные обозначения моделей, которые я написал некоторое время назад для пользовательских функций 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, мы никогда их не вызовем. Также не то, что я сделал первый аргумент для атрибута EdmFunctionAtimestampComparers. Это должно соответствовать пространству имен, указанному в разделе вашей концептуальной модели.

Использование:

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

            }
        }
    }
}

Я закончил выполнение необработанного запроса:
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("ВЫБЕРИТЕ * ОТ Foos WHERE Version> @ver", новый SqlParameter("ver", lastFoo.Version).SqlDbType = SqlDbType.Timestamp);

Я нашел этот обходной путь полезным:

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

(Следующий ответ Дэймона Уоррена скопирован отсюда):

Вот что мы сделали, чтобы решить эту проблему:

Используйте расширение сравнения, подобное этому:

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#, заключается в том, что метод расширения сравнения никогда не вызывается, а EF LINQ упрощает x.compare(y) > 0 вплоть до x > y

Другие вопросы по тегам