c# - many - entity framework relationships
¿Cómo consultar las entidades de Code First según el valor de rowversion/timestamp? (9)
Me he encontrado con un caso en el que algo que funcionó bastante bien con LINQ to SQL parece ser muy obtuso (o quizás imposible) con el Entity Framework. Específicamente, tengo una entidad que incluye una propiedad rowversion
(tanto para el control de versiones como para el control de concurrencia). Algo como:
public class Foo
{
[Key]
[MaxLength(50)]
public string FooId { get; set; }
[Timestamp]
[ConcurrencyCheck]
public byte[] Version { get; set; }
}
Me gustaría poder tomar una entidad como entrada y encontrar todas las otras entidades que se han actualizado más recientemente. Algo como:
Foo lastFoo = GetSomeFoo();
var recent = MyContext.Foos.Where(f => f.Version > lastFoo.Version);
Ahora, en la base de datos esto funcionaría: dos valores de rowversion
pueden compararse entre sí sin ningún problema. Y he hecho algo similar antes de usar LINQ to SQL, que asigna la rowversion
la rowversion
a System.Data.Linq.Binary
, que se puede comparar. (Al menos en la medida en que el árbol de expresiones pueda volver a asignarse a la base de datos).
Pero en el Código Primero, el tipo de propiedad debe ser byte[]
. Y dos matrices no pueden compararse con los operadores de comparación regulares. ¿Hay alguna otra manera de escribir la comparación de las matrices que LINQ a Entidades entenderán? ¿O para obligar a los arreglos a otros tipos para que la comparación pueda superar al compilador?
¡Encontré una solución que funciona perfectamente! Probado en Entity Framework 6.1.3.
No hay forma de usar el operador <
con matrices de bytes porque el sistema de tipo C # lo impide (como debería). Pero lo que puede hacer es construir exactamente la misma sintaxis usando expresiones, y hay una laguna que le permite lograr esto.
Primer paso
Si no desea la explicación completa, puede pasar a la sección Solución.
Si no está familiarizado con las expresiones, aquí está el curso intensivo de MSDN .
Básicamente, cuando queryable.Where(obj => obj.Id == 1)
el compilador realmente genera lo mismo que si hubieras escrito:
var objParam = Expression.Parameter(typeof(ObjType));
queryable.Where(Expression.Lambda<Func<ObjType, bool>>(
Expression.Equal(
Expression.Property(objParam, "Id"),
Expression.Constant(1)),
objParam))
Y esa expresión es lo que el proveedor de la base de datos analiza para crear su consulta. Obviamente, esto es mucho más detallado que el original, pero también te permite hacer una metaprogramación como cuando haces una reflexión. La verbosidad es el único inconveniente de este método. Es un inconveniente mejor que otras respuestas aquí, como tener que escribir SQL sin formato o no poder usar parámetros.
En mi caso, ya estaba usando expresiones, pero en su caso, el primer paso es volver a escribir su consulta usando expresiones:
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));
Así es como solucionamos el error del compilador que obtenemos si intentamos usar objetos <
en byte[]
. Ahora, en lugar de un error de compilación, obtenemos una excepción de tiempo de ejecución porque Expression.LessThan
intenta encontrar el byte[].op_LessThan
y falla en el tiempo de ejecución. Aquí es donde entra la laguna.
Escapatoria
Para deshacernos de ese error de tiempo de ejecución, le diremos a Expression.LessThan
qué método usar para que no intente encontrar el predeterminado ( byte[].op_LessThan
) que no existe:
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));
¡Genial! Ahora todo lo que necesitamos es MethodInfo someMethodThatWeWrote
creado a partir de un método estático con la firma bool (byte[], byte[])
para que los tipos coincidan en el tiempo de ejecución con nuestras otras expresiones.
Solución
Necesita un pequeño DbFunctionExpressions.cs . Aquí hay una versión truncada:
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);
}
}
Uso
var recent = MyContext.Foos.Where(Expression.Lambda<Func<Foo, bool>>(
DbFunctionExpressions.BinaryLessThan(
Expression.Property(fooParam, nameof(Foo.Version)),
Expression.Constant(lastFoo.Version)),
fooParam));
- Disfrutar.
Notas
No funciona en Entity Framework Core 1.0.0, pero abrí un problema allí para un soporte más completo sin la necesidad de expresiones de todos modos. (EF Core no funciona porque atraviesa una etapa en la que copia la expresión de LessThan
con los parámetros left
y right
, pero no copia el parámetro MethodInfo
que usamos para la laguna).
Aquí hay otra solución disponible para EF 6.x que no requiere la creación de funciones en la base de datos, sino que utiliza funciones definidas por el modelo.
Definiciones de funciones (esto va dentro de la sección en su archivo CSDL, o dentro de la sección si está usando archivos 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>
Tenga en cuenta que no he escrito el código para crear las funciones utilizando las API disponibles en Code First, pero es similar al código que propuso Drew o las convenciones del modelo que escribí hace un tiempo para las UDF https://github.com/divega/UdfCodeFirstSample , debería funcionar
Definición del método (esto va en su código fuente de 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;
}
}
}
Tenga en cuenta también que he definido los métodos como métodos de extensión sobre el byte [], aunque esto no es necesario. También proporcioné implementaciones para los métodos para que funcionen si las evalúas fuera de las consultas, pero puedes elegir también lanzar la excepción NotImplementedException. Cuando utiliza estos métodos en LINQ para consultas de Entidades, nunca los invocaremos. Tampoco he hecho el primer argumento para EdmFunctionAttribute "TimestampComparers". Esto tiene que coincidir con el espacio de nombres especificado en la sección de su modelo conceptual.
Uso:
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));
}
}
}
}
Esa es la mejor solución, pero tiene un problema de rendimiento. Se emitirá el parámetro @ver. Colocar columnas en donde la cláusula es mala para la base de datos.
La conversión de tipo en la expresión puede afectar a "SeekPlan" en la elección del plan de consulta
MyContext.Foos.SqlQuery ("SELECT * FROM Foos WHERE Version> @ver", nuevo SqlParameter ("ver", lastFoo.Version));
Sin reparto. MyContext.Foos.SqlQuery ("SELECT * FROM Foos WHERE Version> @ver", nuevo SqlParameter ("ver", lastFoo.Version) .SqlDbType = SqlDbType.Timestamp);
Este método funciona para mí y evita manipular el SQL sin formato:
var recent = MyContext.Foos.Where(c => BitConverter.ToUInt64(c.RowVersion.Reverse().ToArray(), 0) > fromRowVersion);
Supongo que sin embargo, el SQL en bruto sería más eficiente.
He encontrado útil esta solución:
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();
Puede lograr esto primero en el código de EF 6 asignando una función de C # a una función de base de datos. Tomó algunos ajustes y no produce el SQL más eficiente, pero hace el trabajo.
Primero, cree una función en la base de datos para probar una nueva versión de rowers. El mio es
CREATE FUNCTION [common].[IsNewerThan]
(
@CurrVersion varbinary(8),
@BaseVersion varbinary(8)
) ...
Al construir su contexto EF, tendrá que definir manualmente la función en el modelo de tienda, como esto:
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;
}
Cree un proxy para el método decorando un método estático con el atributo DbFunction. EF utiliza esto para asociar el método con el método nombrado en el modelo de tienda. Haciéndolo un método de extensión produce LINQ más limpio.
[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");
}
Ejemplo
Finalmente, llame al método de LINQ a las entidades en una expresión estándar.
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();
}
Esto genera el T-SQL para lograr lo que desea, utilizando el contexto y los conjuntos de entidades que ha definido.
WHERE ([common].[IsNewerThan]([Extent1].[RowVersion], @p__linq__0)) = 1'',N''@p__linq__0 varbinary(8000)'',@p__linq__0=0x000000000001DB7B
Puede usar SqlQuery para escribir el SQL sin procesar en lugar de generarlo.
MyContext.Foos.SqlQuery("SELECT * FROM Foos WHERE Version > @ver", new SqlParameter("ver", lastFoo.Version));
Terminé ejecutando una consulta en bruto:
ctx.Database.SqlQuery ("SELECT * FROM [TABLENAME] WHERE (CONVERT (bigint, @@ DBTS)>" + X)). ToList ();
Extendí la respuesta de jmn2s para ocultar el código de expresión fea en un método de extensión
Uso:
ctx.Foos.WhereVersionGreaterThan(r => r.RowVersion, myVersion);
Método de extensión:
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;
}
}