21

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

これで、データベースでこれが機能します。2つのrowversion値を問題なく相互に比較できます。また、LINQ to SQLを使用する前に同様のことを行いました。これにより、をにマップrowversionSystem.Data.Linq.Binary、比較することができます。(少なくとも、式ツリーをデータベースにマップして戻すことができる範囲で。)

ただし、Code Firstでは、プロパティのタイプはである必要がありますbyte[]。また、2つの配列を通常の比較演算子と比較することはできません。LINQ to Entitiesが理解する配列の比較を書く他の方法はありますか?または、配列を他の型に強制変換して、比較がコンパイラーを通過できるようにしますか?

4

10 に答える 10

13

完全に機能する回避策を見つけました!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 は、 andパラメーターを使用してLessThan式をコピーする段階を通過するため、機能しませんが、抜け穴に使用するパラメーターはコピーしません)。leftrightMethodInfo

于 2016-07-01T18:38:01.617 に答える
5

SqlQueryを使用して、生のSQLを生成する代わりに作成できます。

MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
于 2011-09-16T17:19:04.890 に答える
3

C# 関数をデータベース関数にマッピングすることにより、EF 6 コード ファーストでこれを実現できます。微調整が必​​要で、最も効率的な 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
于 2013-11-26T19:07:04.237 に答える
2

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



}
于 2017-06-22T14:00:14.583 に答える
1

この方法は私にとってはうまくいき、生の SQL の改ざんを回避します。

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

ただし、生のSQLの方が効率的だと思います。

于 2014-04-03T06:57:03.643 に答える
0

データベースに関数を作成する必要はなく、代わりにモデルで定義された関数を使用する 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>

Code First で利用可能な API を使用して関数を作成するためのコードを書いていないことに注意してください。ただし、Drew が提案したコードや、UDF 用に以前に書いたモデル規則に似ています 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 クエリで使用する場合、実際に呼び出すことはありません。また、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));

            }
        }
    }
}
于 2016-07-21T21:56:06.963 に答える
0

生のクエリを実行することになりました:
ctx.Database.SqlQuery("SELECT * FROM [TABLENAME] WHERE(CONVERT(bigint,@@DBTS) >" + X)).ToList();

于 2014-11-12T12:11:27.280 に答える
0

この回避策が役立つことがわかりました:

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();
于 2012-09-09T12:12:54.473 に答える