Comment interroger les premières entités du Code en fonction de la valeur rowversion/timestamp?
j'ai rencontré un cas où quelque chose qui fonctionnait assez bien avec LINQ à SQL semble être très obtus (ou peut-être impossible) avec le Framework Entity. Plus précisément, j'ai une entité qui comprend un rowversion
propriété (à la fois pour versioning et contrôle de simultanéité). Quelque chose comme:
public class Foo
{
[Key]
[MaxLength(50)]
public string FooId { get; set; }
[Timestamp]
[ConcurrencyCheck]
public byte[] Version { get; set; }
}
je voudrais pouvoir prendre une entity comme entrée, et trouver toutes les autres entities qui sont plus récemment mises à jour. Quelque chose comme:
Foo lastFoo = GetSomeFoo();
var recent = MyContext.Foos.Where(f => f.Version > lastFoo.Version);
Maintenant, dans le base de données cela fonctionnerait: deux rowversion
les valeurs peuvent être comparées les unes aux autres sans aucun problème. Et J'ai fait la même chose avant D'utiliser LINQ pour SQL, qui correspond à la rowversion
System.Data.Linq.Binary
, qui peut être comparé. (Au moins dans la mesure où l'arbre des expressions peut être mappé vers la base de données.)
mais en Code D'abord, le type de la propriété doit être byte[]
. Et deux tableaux ne peuvent pas être comparés avec les opérateurs de comparaison. Est-il une autre façon d'écrire l' comparaison des tableaux que LINQ à des entités comprendra? Ou pour forcer les tableaux dans d'autres types de sorte que la comparaison peut passer le compilateur?
9 réponses
Vous pouvez utiliser SqlQuery pour écrire le SQL brut au lieu de le faire générer.
MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
Trouvé une solution qui fonctionne parfaitement! Testé sur le cadre de L'entité 6.1.3.
Il n'y a pas moyen d'utiliser le <
opérateur avec tableaux d'octets parce que le système de type C empêche cela (comme il se doit). Mais ce que vous do est de construire la même syntaxe exacte en utilisant des expressions, et il y a une faille qui vous permet de l'enlever.
Première étape
Si vous ne voulez pas l'explication complète, vous pouvez passer à la Solution section.
si vous n'êtes pas familier avec les expressions, voici cours accéléré de MSDN.
en gros, quand vous tapez queryable.Where(obj => obj.Id == 1)
le compilateur affiche vraiment la même chose que si vous aviez tapé:
var objParam = Expression.Parameter(typeof(ObjType));
queryable.Where(Expression.Lambda<Func<ObjType, bool>>(
Expression.Equal(
Expression.Property(objParam, "Id"),
Expression.Constant(1)),
objParam))
et cette expression est ce que le fournisseur de base de données analyse pour créer votre requête. C'est évidemment beaucoup plus verbeux que l'original, mais il vous permet aussi de faire de la méta-programmation comme quand vous faites de la réflexion. Le niveau de verbosité est l' seul inconvénient à cette méthode. C'est un meilleur inconvénient que les autres réponses ici, comme devoir écrire du SQL brut ou ne pas pouvoir utiliser les paramètres.
dans mon cas, j'utilisais déjà des expressions, mais dans votre cas la première étape est de réécrire votre requête en utilisant des expressions:
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));
C'est la façon dont nous contourner l'erreur du compilateur, nous obtenons si nous essayons d'utiliser <
byte[]
objets. Maintenant, au lieu d'une erreur du compilateur, nous obtenons une exception d'exécution parce que Expression.LessThan
essaie de trouver byte[].op_LessThan
et échoue à l'exécution. C'est là que la faille vient dans.
Echappatoire
Pour se débarrasser de l'erreur d'exécution, nous dirons Expression.LessThan
quelle méthode utiliser pour ne pas essayer de trouver la méthode par défaut (byte[].op_LessThan
) qui n'existe pas:
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));
Super! Maintenant tout ce dont nous avons besoin est MethodInfo someMethodThatWeWrote
créé à partir d'une méthode statique avec la signature bool (byte[], byte[])
de sorte que les types correspondent à l'exécution avec nos autres expression.
Solution
Vous avez besoin d'un petit DbFunctionExpressions.cs. Voici une version tronquée:
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);
}
}
Utilisation
var recent = MyContext.Foos.Where(Expression.Lambda<Func<Foo, bool>>(
DbFunctionExpressions.BinaryLessThan(
Expression.Property(fooParam, nameof(Foo.Version)),
Expression.Constant(lastFoo.Version)),
fooParam));
- Profiter.
Notes
ne fonctionne pas sur le noyau du cadre de L'entité 1.0.0, mais je ouvert un problème là pour un support plus complet sans avoir de toute façon besoin d'expressions. (EF Core ne fonctionne pas parce qu'il passe par une étape où il des copies de la LessThan
expression left
et right
paramètres mais ne copie pas les MethodInfo
paramètre que nous utilisons pour la lacune.)
vous pouvez accomplir ceci dans le code EF 6-d'abord en faisant correspondre une fonction C# à une fonction de base de données. Il a fallu quelques ajustements et ne produit pas le SQL le plus efficace, mais il obtient le travail fait.
tout d'Abord, créer une fonction dans la base de données pour tester une nouvelle rowversion. Le mien est
CREATE FUNCTION [common].[IsNewerThan]
(
@CurrVersion varbinary(8),
@BaseVersion varbinary(8)
) ...
lors de la construction de votre contexte EF, vous devrez définir manuellement la fonction dans le modèle store, comme ceci:
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;
}
Créer un proxy pour la méthode par décorer une méthode statique avec l'attribut DbFunction. EF utilise pour associer la méthode avec la méthode nommée dans le modèle de magasin. Le faire une méthode d'extension produit LINQ plus propre.
[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");
}
Exemple
enfin, appelez la méthode de LINQ à entities dans une expression standard.
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();
}
génère le T-SQL pour obtenir ce que vous voulez, en utilisant le contexte et les ensembles d'entités que vous avez définis.
WHERE ([common].[IsNewerThan]([Extent1].[RowVersion], @p__linq__0)) = 1',N'@p__linq__0 varbinary(8000)',@p__linq__0=0x000000000001DB7B
cette méthode fonctionne pour moi et évite d'altérer le SQL brut:
var recent = MyContext.Foos.Where(c => BitConverter.ToUInt64(c.RowVersion.Reverse().ToArray(), 0) > fromRowVersion);
je suppose cependant SQL brut serait plus efficace.
j'ai trouvé cette solution de contournement utile:
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();
j'ai fini par exécuter une requête brute:
ctx.La base de données.SqlQuery ("SELECT * FROM [TABLENAME] WHERE(CONVERT (bigint,@DBTS) >" + X)).ToList ();
C'est la meilleure solution, mais ont un problème de performance. Le paramètre @ver sera lancé. Les colonnes de fonte dans lesquelles la clause est mauvaise pour la base de données.
conversion de Type dans l'expression susceptible d'affecter "SeekPlan" dans le plan de requête choix
MyContext.Foos.SqlQuery ("SELECT * FROM Foos WHERE Version > @ver", Nouveau SqlParameter ("ver", lastFoo.Version));
sans plâtre. MyContext.Foos.SqlQuery ("SELECT * FROM Foos WHERE Version > @ver" , Nouveau SqlParameter ("ver", lastFoo.Version.)SqlDbType = SqlDbType.Timestamp);
Voici encore une autre solution disponible pour L'EF 6.x qui ne nécessite pas la création de fonctions dans la base de données mais utilise des fonctions définies par le modèle à la place.
définition des fonctions (ceci va à l'intérieur de la section dans votre fichier CSDL, ou à l'intérieur de la section si vous utilisez des fichiers 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>
notez que je n'ai pas écrit le code pour créer les fonctions en utilisant les API disponibles dans le Code en premier, mais similaire au code à ce que Drew a proposé ou les conventions modèles I écrit il y a quelques temps pour Udf https://github.com/divega/UdfCodeFirstSample, devrait fonctionner
définition de la Méthode (ce qui se passe dans votre code source en 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;
}
}
}
Notez aussi que j'ai défini les méthodes comme des méthodes d'extension sur byte[], bien que cela ne soit pas nécessaire. J'ai également fourni des implémentations pour les méthodes de sorte qu'elles fonctionnent si vous les évaluez à l'extérieur des requêtes, mais vous pouvez aussi choisir de lancer NotImplementedException. Lorsque vous utilisez ces méthodes dans LINQ aux requêtes Entities, nous ne les invoquerons jamais vraiment. Aussi pas que j'ai fait le premier argument pour EdmFunctionAttribute "TimestampComparers". Cela doit correspondre à l'espace de noms spécifié dans la section de votre modèle conceptuel.
Utilisation:
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));
}
}
}
}
j'ai étendu jmn2s réponse pour cacher le code d'expression moche dans une méthode d'extension
Utilisation:
ctx.Foos.WhereVersionGreaterThan(r => r.RowVersion, myVersion);
Méthode D'Extension:
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;
}
}