Consultas con el Proveedor EF Core de Azure Cosmos DB
Conceptos básicos de consulta
Las consultas LINQ de EF Core se pueden ejecutar en Azure Cosmos DB de la misma manera que para otros proveedores de bases de datos. Por ejemplo:
public class Session
{
public Guid Id { get; set; }
public string Category { get; set; }
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
}
var stringResults = await context.Sessions
.Where(
e => e.Category.Length > 4
&& e.Category.Trim().ToLower() != "disabled"
&& e.Category.TrimStart().Substring(2, 2).Equals("xy", StringComparison.OrdinalIgnoreCase))
.ToListAsync();
Nota:
El proveedor de Azure Cosmos DB no traduce el mismo conjunto de consultas LINQ que otros proveedores.
Por ejemplo, el operador Include()
EF no se admite en Azure Cosmos DB, ya que las consultas entre documentos no se admiten en la base de datos.
Claves de partición
La ventaja de la partición es que las consultas se ejecutan únicamente en la partición en la que se encuentran los datos relevantes, lo que ahorra costes y garantiza una mayor velocidad de los resultados. Las consultas que no especifican claves de partición se ejecutan en todas las particiones, lo que puede resultar bastante costoso.
A partir de EF 9.0, EF detecta y extrae automáticamente las comparaciones de claves de partición en los operadores Where
de su consulta LINQ. Supongamos que ejecutamos la siguiente consulta contra nuestro tipo de entidad Session
, que está configurada con una clave de partición jerárquica:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Session>()
.HasPartitionKey(b => new { b.TenantId, b.UserId, b.SessionId })
}
var tenantId = "Microsoft";
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var username = "scott";
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId > 0
&& e.Username == username)
.ToListAsync();
Examinando los registros generados por EF, vemos que esta consulta se ejecuta de la siguiente manera:
Executed ReadNext (166.6985 ms, 2.8 RU) ActivityId='312da0d2-095c-4e73-afab-27072b5ad33c', Container='test', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE ((c["SessionId"] > 0) AND CONTAINS(c["Username"], "a"))
En estos registros, observamos lo siguiente:
- Las dos primeras comparaciones, en
TenantId
yUserId
, se han eliminado, y aparecen en la "Partición"ReadNext
en lugar de en la cláusulaWHERE
; esto significa que la consulta solo se ejecutará en las subparticiones para esos valores. SessionId
también forma parte de la clave de partición jerárquica, pero en lugar de una comparación de igualdad, usa un operador mayor que (>
), por lo que no puede eliminarse. Forma parte de la cláusulaWHERE
como cualquier propiedad normal.Username
es una propiedad normal (no forma parte de la clave de partición) y, por tanto, también permanece en la cláusulaWHERE
.
Tenga en cuenta que aunque no se proporcionen algunos de los valores de la clave de partición, las claves de partición jerárquicas siguen permitiendo seleccionar solo las subparticiones que corresponden a las dos primeras propiedades. Aunque esto no es tan eficiente como seleccionar una única partición (identificada por las tres propiedades), sigue siendo mucho más eficiente que seleccionar todas las particiones.
En lugar de hacer referencia a las propiedades de la clave de partición en un operador Where
, puede especificarlas explícitamente usando el operador WithPartitionKey:
var sessions = await context.Sessions
.WithPartitionKey(tenantId, userId)
.Where(e => e.SessionId > 0 && e.Username.Contains("a"))
.ToListAsync();
Esto se ejecuta de la misma forma que la consulta anterior, y puede ser preferible si desea que las claves de partición sean más explícitas en sus consultas. El uso de WithPartitionKey puede ser necesario en versiones de EF anteriores a la 9.0. Eche un vistazo a los registros para asegurarse de que las consultas usan las claves de partición según lo previsto.
Lecturas puntuales
Aunque Azure Cosmos DB permite realizar consultas potentes a través de SQL, dichas consultas pueden ser bastante costosas. Azure Cosmos DB también admite lecturas puntuales, que deben utilizarse para recuperar un único documento si se conocen tanto la propiedad id
como la clave de partición completa. Las lecturas puntuales identifican directamente un documento específico en una partición específica, y se ejecutan de forma extremadamente eficiente y con costes reducidos en comparación con la recuperación del mismo documento con una consulta. Se recomienda diseñar el sistema para aprovechar las lecturas puntuales con la mayor frecuencia posible. Para obtener más información, consulte la documentación de Azure Cosmos DB.
En la sección anterior, vimos cómo EF identificaba y extraía comparaciones de claves de partición de la cláusula Where
para realizar consultas más eficientes, restringiendo el procesamiento solo a las particiones relevantes. Es posible ir un paso más allá y proporcionar también la propiedad id
en la consulta. Examinemos la siguiente consulta:
var session = await context.Sessions.SingleAsync(
e => e.Id == someId
&& e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId);
En esta consulta, se proporciona un valor para la propiedad Id
(que se asigna a la propiedad id
de Azure Cosmos DB), así como valores para todas las propiedades de clave de partición. Además, no hay componentes adicionales en la consulta. Cuando se cumplen todas estas condiciones, EF puede ejecutar la consulta como una lectura puntual:
Executed ReadItem (46 ms, 1 RU) ActivityId='d7391311-2266-4811-ae2d-535904c42c43', Container='test', Id='9', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",10.0]'
Tenga en cuenta el ReadItem
, que indica que la consulta se ejecutó como una lectura puntual eficaz: no hay ninguna consulta SQL implicada.
Tenga en cuenta que, al igual que con la extracción de claves de partición, se han realizado mejoras significativas en este mecanismo en EF 9.0; las versiones anteriores no detectan ni usan lecturas puntuales de forma fiable.
Paginación
Nota:
Esta función se introdujo en EF Core 9.0 y sigue siendo experimental. Por favor, háganos saber cómo le funciona y si tiene algún comentario.
La paginación se refiere a la recuperación de resultados en páginas, en lugar de todos a la vez; esto se hace típicamente para grandes conjuntos de resultados, donde se muestra una interfaz de usuario, permitiendo a los usuarios navegar a través de las páginas de los resultados.
Una manera común de implementar la paginación con bases de datos es usar los operadores LINQ Skip
y Take
(OFFSET
y LIMIT
en SQL). Dado un tamaño de página de 10 resultados, la tercera página se puede capturar con EF Core de la siguiente manera:
var position = 20;
var nextPage = await context.Session
.OrderBy(s => s.Id)
.Skip(position)
.Take(10)
.ToListAsync();
Por desgracia, esta técnica es bastante ineficiente y puede aumentar considerablemente los costes de consulta. Azure Cosmos DB proporciona un mecanismo especial para paginar a través del resultado de una consulta, mediante el uso de tokens de continuación:
CosmosPage firstPage = await context.Sessions
.OrderBy(s => s.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
string continuationToken = firstPage.ContinuationToken;
foreach (var session in firstPage.Values)
{
// Display/send the sessions to the user
}
En lugar de terminar la consulta LINQ con ToListAsync
o similar, usamos el método ToPageAsync
, ordenándole que obtenga como máximo 10 elementos en cada página (ten en cuenta que puede haber menos elementos en la base de datos). Dado que esta es nuestra primera consulta, nos gustaría obtener resultados desde el principio, y pasar null
como token de continuación. ToPageAsync
devuelve un CosmosPage
, que expone un token de continuación y los valores de la página (hasta 10 elementos). Su programa normalmente enviará esos valores al cliente, junto con el token de continuación. Esto permitirá reanudar la consulta más tarde y obtener más resultados.
Supongamos que el usuario hace clic en el botón "Siguiente" de su interfaz de usuario y solicita los 10 elementos siguientes. A continuación, puede ejecutar la consulta de la siguiente manera:
CosmosPage nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
string continuationToken = nextPage.ContinuationToken;
foreach (var session in nextPage.Values)
{
// Display/send the sessions to the user
}
Ejecutamos la misma consulta, pero esta vez pasamos el token de continuación recibido de la primera ejecución; esto indica al motor de consultas que continúe la consulta donde la dejó y obtenga los siguientes 10 elementos. Una vez que se obtiene la última página y no hay más resultados, el token de continuación será null
, y el botón "Siguiente" puede aparecer en gris. Este método de paginación es extremadamente eficiente y rentable comparado con el uso de Skip
y Take
.
Para obtener más información sobre la paginación en Azure Cosmos DB, consulte esta página.
Nota:
Azure Cosmos DB no es compatible con la paginación hacia atrás, y no proporciona un recuento del total de páginas o elementos.
ToPageAsync
está actualmente anotada como experimental, ya que puede ser sustituida por una API de paginación de EF más genérica que no sea específica de Azure Cosmos DB. Aunque el uso de la API actual generará una advertencia de compilación (EF9102
1), hacerlo debería ser seguro. Los cambios futuros pueden requerir pequeños ajustes en la forma de la API.
FindAsync
FindAsync
es una API útil para obtener una entidad por su clave primaria, y evitar un viaje de ida y vuelta a la base de datos cuando la entidad ya ha sido cargada y es rastreada por el contexto.
Los desarrolladores familiarizados con las bases de datos relacionales están acostumbrados a la clave primaria de un tipo de entidad consistente, por ejemplo, en una propiedad Id
. Cuando se usa el proveedor EF Azure Cosmos DB, la clave primaria contiene las propiedades de la clave de partición además de la propiedad asignada a la propiedad JSON id
; esto es así porque Azure Cosmos DB permite que diferentes particiones contengan documentos con la misma propiedad JSON id
, y así solo la clave combinada id
y la de partición identifican de forma única un único documento en un contenedor:
public class Session
{
public Guid Id { get; set; }
public string PartitionKey { get; set; }
...
}
var mySession = await context.FindAsync(id, pkey);
Si tiene una clave de partición jerárquica, debe pasar todos los valores de clave de partición a FindAsync
, en el orden en que se configuraron.
Nota:
Use FindAsync
solo cuando el contexto ya realice el seguimiento de la entidad y quiera evitar el recorrido de ida y vuelta de la base de datos.
De lo contrario, simplemente use SingleAsync
. No hay diferencia de rendimiento entre los dos cuando la entidad necesita ser cargada desde la base de datos.
Consultas SQL
Las consultas también se pueden escribir directamente en SQL. Por ejemplo:
var rating = 3;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Rating > {rating}")
.ToListAsync();
Esta consulta da como resultado la ejecución de la consulta siguiente:
SELECT VALUE s
FROM (
SELECT VALUE c FROM root c WHERE c.Angle1 <= @p0
) s
Tenga en cuenta que FromSql
se introdujo en EF 9.0. En versiones anteriores, se puede usar FromSqlRaw
en su lugar, aunque tenga en cuenta que ese método es vulnerable a ataques de inyección SQL.
Para obtener más información sobre consultas SQL, consulte la Documentación relacional sobre consultas SQL. La mayor parte de ese contenido también es relevante para el proveedor Azure Cosmos DB.
Asignaciones de funciones
Esta sección muestra qué métodos y miembros .NET se convierten en qué funciones SQL al realizar consultas con el proveedor Azure Cosmos DB.
Funciones de fecha y hora
.NET | SQL | Agregado en |
---|---|---|
DateTime.UtcNow | GetCurrentDateTime() | |
DateTimeOffset.UtcNow | GetCurrentDateTime() | |
dateTime.Year1 | DateTimePart("aaaa", dateTime) | EF Core 9.0 |
dateTimeOffset.Year1 | DateTimePart("aaaa", dateTimeOffset) | EF Core 9.0 |
dateTime.AddYears(years)1 | DateTimeAdd("aaaa", dateTime) | EF Core 9.0 |
dateTimeOffset.AddYears(years)1 | DateTimeAdd("aaaa", dateTimeOffset) | EF Core 9.0 |
1 Los demás miembros del componente también se traducen (mes, día...).
Funciones numéricas
.NET | SQL | Agregado en |
---|---|---|
double.DegreesToRadians(x) | RADIANS(@x) | EF Core 8.0 |
double.RadiansToDegrees(x) | DEGREES(@x) | EF Core 8.0 |
EF.Functions.Random() | RAND() | |
Math.Abs(value) | ABS(@value) | |
Math.Acos(d) | ACOS(@d) | |
Math.Asin(d) | ASIN(@d) | |
Math.Atan(d) | ATAN(@d) | |
Math.Atan2(y, x) | ATN2(@y, @x) | |
Math.Ceiling(d) | CEILING(@d) | |
Math.Cos(d) | COS(@d) | |
Math.Exp(d) | EXP(@d) | |
Math.Floor(d) | FLOOR(@d) | |
Math.Log(a, newBase) | LOG(@a, @newBase) | |
Math.Log(d) | LOG(@d) | |
Math.Log10(d) | LOG10(@d) | |
Math.Pow(x, y) | POWER(@x, @y) | |
Math.Round(d) | ROUND(@d) | |
Math.Sign(value) | SIGN(@value) | |
Math.Sin(a) | SIN(@a) | |
Math.Sqrt(d) | SQRT(@d) | |
Math.Tan(a) | TAN(@a) | |
Math.Truncate(d) | TRUNC(@d) |
Sugerencia
Además de los métodos enumerados aquí, también se traducen las implementaciones matemáticas genéricas correspondientes y los métodos MathF. Por ejemplo, Math.Sin
, MathF.Sin
, double.Sin
y float.Sin
todos se asignan a la función SIN
en SQL.
Funciones de cadena
.NET | SQL | Agregado en |
---|---|---|
Regex.IsMatch(input, pattern) | RegexMatch(@pattern, @input) | EF Core 7.0 |
Regex.IsMatch(input, pattern, options) | RegexMatch(@input, @pattern, @options) | EF Core 7.0 |
string.Concat(str0, str1) | @str0 + @str1 | |
string.Equals(a, b, StringComparison.Ordinal) | STRINGEQUALS(@a, @b) | |
string.Equals(a, b, StringComparison.OrdinalIgnoreCase) | STRINGEQUALS(@a, @b, true) | |
stringValue.Contains(value) | CONTAINS(@stringValue, @value) | |
stringValue.Contains(value, StringComparison.Ordinal) | CONTAINS(@stringValue, @value, false) | EF Core 9.0 |
stringValue.Contains(value, StringComparison.OrdinalIgnoreCase) | CONTAINS(@stringValue, @value, true) | EF Core 9.0 |
stringValue.EndsWith(value) | ENDSWITH(@stringValue, @value) | |
stringValue.EndsWith(value, StringComparison.Ordinal) | ENDSWITH(@stringValue, @value, false) | EF Core 9.0 |
stringValue.EndsWith(value, StringComparison.OrdinalIgnoreCase) | ENDSWITH(@stringValue, @value, true) | EF Core 9.0 |
stringValue.Equals(value, StringComparison.Ordinal) | STRINGEQUALS(@stringValue, @value) | |
stringValue.Equals(value, StringComparison.OrdinalIgnoreCase) | STRINGEQUALS(@stringValue, @value, true) | |
stringValue.FirstOrDefault() | LEFT(@stringValue, 1) | |
stringValue.IndexOf(value) | INDEX_OF(@stringValue, @value) | |
stringValue.IndexOf(value, startIndex) | INDEX_OF(@stringValue, @value, @startIndex) | |
stringValue.LastOrDefault() | RIGHT(@stringValue, 1) | |
stringValue.Length | LENGTH(@stringValue) | |
stringValue.Replace(oldValue, newValue) | REPLACE(@stringValue, @oldValue, @newValue) | |
stringValue.StartsWith(value) | STARTSWITH(@stringValue, @value) | |
stringValue.StartsWith(value, StringComparison.Ordinal) | STARTSWITH(@stringValue, @value, false) | EF Core 9.0 |
stringValue.StartsWith(value, StringComparison.OrdinalIgnoreCase) | STARTSWITH(@stringValue, @value, true) | EF Core 9.0 |
stringValue.Substring(startIndex) | SUBSTRING(@stringValue, @startIndex, LENGTH(@stringValue)) | |
stringValue.Substring(startIndex, length) | SUBSTRING(@stringValue, @startIndex, @length) | |
stringValue.ToLower() | LOWER(@stringValue) | |
stringValue.ToUpper() | UPPER(@stringValue) | |
stringValue.Trim() | TRIM(@stringValue) | |
stringValue.TrimEnd() | RTRIM(@stringValue) | |
stringValue.TrimStart() | LTRIM(@stringValue) |
Funciones varias
.NET | SQL | Notas |
---|---|---|
collection.Contains(item) | @item EN @collection | |
EF.Functions.CoalesceUndefined(x, y)1 | x ?? y | Añadido en EF Core 9.0 |
EF.Functions.IsDefined(x) | IS_DEFINED(x) | Añadido en EF Core 9.0 |
EF.Functions.VectorDistance(vector1, vector2)2 | VectorDistance(vector1, vector2) | Añadido en EF Core 9.0, Experimental |
EF.Functions.VectorDistance(vector1, vector2, bruteForce)2 | VectorDistance(vector1, vector2, bruteForce) | Añadido en EF Core 9.0, Experimental |
EF.Functions.VectorDistance(vector1, vector2, bruteForce, distanceFunction)2 | VectorDistance(vector1, vector2, bruteForce, distanceFunction) | Añadido en EF Core 9.0, Experimental |
1 Tenga en cuenta que EF.Functions.CoalesceUndefined
combina con undefined
, no con null
. Para combinar null
, use el operador ??
de C# normal.
2 Consulte la documentación para obtener información sobre el uso del vector de búsqueda en Azure Cosmos DB, que es experimental. Las API están sujetas a cambios.