Подтвердить что ты не робот

Как запрашивать объекты First Code на основе значения rowversion/timestamp?

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

4b9b3361

Ответ 1

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

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

Ответ 2

Обнаружено обходное решение, которое отлично работает! Проверено на платформе сущностей 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, который мы используем для лазейки.)

Ответ 3

Вы можете выполнить это в кодовом коде 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

Ответ 4

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

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

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

Ответ 5

Я нашел это обходное решение полезным:

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

Ответ 6

Я закончил выполнение необработанного запроса:
ctx.Database.SqlQuery( "SELECT * FROM [TABLENAME] WHERE (CONVERT (bigint, @@DBTS) > " + X)). ToList();

Ответ 7

Это лучшее решение, но проблема с производительностью. Будет изменен параметр @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);

Ответ 8

Вот еще одно обходное решение, доступное 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, но аналогично коду, предложенному Дрю или условным обозначениям модели, которые я написал некоторое время назад для 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));

            }
        }
    }
}

Ответ 9

Я расширил ответ 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;
    }



}

Ответ 10

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

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

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

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