Как запросить объекты Code First на основе значения rowversion / timestamp? - PullRequest
19 голосов
/ 16 сентября 2011

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

Ответы [ 10 ]

9 голосов
/ 01 июля 2016

Найден обходной путь, который отлично работает!Протестировано на 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));
  • Наслаждайтесь.

Примечания

Не работает на EntityFramework Core 1.0.0, но я открыл там проблему для более полной поддержки без выражений в любом случае.(EF Core не работает, поскольку проходит этап, на котором копирует выражение LessThan с параметрами left и right, но не копирует параметр MethodInfo, который мы используем для лазейки.)

5 голосов
/ 16 сентября 2011

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

MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
3 голосов
/ 26 ноября 2013

Вы можете сделать это в коде 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
1 голос
/ 03 апреля 2014

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

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

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

0 голосов
/ 24 июня 2019

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

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

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

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

0 голосов
/ 22 июня 2017

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

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

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



}
0 голосов
/ 22 июля 2016

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

            }
        }
    }
}
0 голосов
/ 18 марта 2015

Это лучшее решение, но есть проблема с производительностью. Параметр @ver будет приведен. Привести столбцы в где предложение плохо для базы данных.

Преобразование типов в выражении может повлиять на "SeekPlan" при выборе плана запроса

MyContext.Foos.SqlQuery ("ВЫБРАТЬ * ИЗ Foos WHERE Версия> @ver", новый SqlParameter ("ver", lastFoo.Version));

Без приведения. MyContext.Foos.SqlQuery ("ВЫБРАТЬ * ИЗ Foos WHERE Version> @ver", новый SqlParameter ("ver", lastFoo.Version) .SqlDbType = SqlDbType.Timestamp);

0 голосов
/ 12 ноября 2014

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

0 голосов
/ 09 сентября 2012

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

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