Consultas com o Provedor EF Core do Azure Cosmos DB
Noções básicas de consulta
As consultas LINQ do EF Core podem ser executadas no Azure Cosmos DB da mesma forma que para outros provedores de banco de dados. Por exemplo:
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();
Observação
O provedor do Azure Cosmos DB não converte o mesmo conjunto de consultas LINQ que outros provedores.
Por exemplo, não há suporte para o operador EF Include()
no Azure Cosmos DB, pois não há suporte para consultas entre documentos no banco de dados.
Chaves de partição
A vantagem do particionamento é fazer com que suas consultas sejam executadas apenas na partição em que os dados relevantes são encontrados, economizando custos e garantindo uma velocidade de resultado mais rápida. Consultas que não especificam chaves de partição são executadas em todas as partições, e isso pode ser bastante caro.
A partir do EF 9.0, o EF detecta e extrai automaticamente comparações de chaves de partição nos operadores Where
LINQ da consulta. Vamos supor que executamos a seguinte consulta em nosso Session
tipo de entidade, que é configurado com uma chave de partição hierá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 os logs gerados pelo EF, vemos essa consulta sendo executada da seguinte maneira:
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"))
Nesses logs, observamos o seguinte:
- As duas primeiras comparações - em
TenantId
eUserId
- foram suspensas e aparecem na "Partição"ReadNext
em vez de na cláusulaWHERE
; isso significa que a consulta só será executada nas subpartições para esses valores. SessionId
também faz parte da chave de partição hierárquica, mas em vez de uma comparação de igualdade, ela usa um operador maior que (>
) e, portanto, não pode ser removida. Faz parte da cláusulaWHERE
como qualquer propriedade regular.Username
é uma propriedade regular - não faz parte da chave de partição - e, portanto, permanece na cláusulaWHERE
também.
Observe que mesmo que alguns dos valores da chave de partição não sejam fornecidos, as chaves de partição hierárquicas ainda permitem direcionar apenas as subpartições que correspondem às duas primeiras propriedades. Embora isso não seja tão eficiente quanto direcionar uma única partição (conforme identificado por todas as três propriedades), ainda é muito mais eficiente do que direcionar todas as partições.
Em vez de fazer referência às propriedades da chave de partição em um operador Where
, você pode especificá-las explicitamente usando o operador WithPartitionKey:
var sessions = await context.Sessions
.WithPartitionKey(tenantId, userId)
.Where(e => e.SessionId > 0 && e.Username.Contains("a"))
.ToListAsync();
Isso é executado da mesma maneira que a consulta acima e pode ser o mais indicado se você quiser tornar as chaves de partição mais explícitas em suas consultas. Pode ser necessário usar o WithPartitionKey em versões do EF anteriores à 9.0 – fique de olho nos logs para garantir que as consultas estejam usando chaves de partição conforme o esperado.
Leituras de ponto
Embora o Azure Cosmos DB permita consultas avançadas por meio de SQL, essas consultas podem ser muito caras. O Azure Cosmos DB também dá suporte a leituras de ponto, que devem ser usadas ao recuperar um único documento se a propriedade id
e toda a chave de partição forem conhecidas. As leituras pontuais identificam diretamente um documento específico em uma partição específica e são executadas com muita eficiência e com custos reduzidos em comparação com a recuperação do mesmo documento com uma consulta. É recomendável projetar seu sistema para aproveitar as leituras pontuais com a maior frequência possível. Para ler mais, consulte a Documentação do Azure Cosmos DB.
Na seção anterior, vimos o EF identificando e extraindo comparações de chave de partição da cláusula Where
para consultas mais eficientes, restringindo o processamento apenas às partições relevantes. É possível ir um passo além e fornecer a propriedade id
na consulta também. Vamos examinar a seguinte consulta:
var session = await context.Sessions.SingleAsync(
e => e.Id == someId
&& e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId);
Nessa consulta, um valor para a propriedade Id
é fornecido (que é mapeado para a propriedade do Azure Cosmos DB id
), bem como valores para todas as propriedades da chave de partição. Além disso, não há componentes adicionais para a consulta. Quando todas essas condições são atendidas, o EF é capaz de executar a consulta como uma leitura de ponto:
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]'
Observe o ReadItem
, que indica que a consulta foi executada como uma leitura de ponto eficiente; nenhuma consulta SQL é envolvida.
Observe que, assim como acontece com a extração de chave de partição, melhorias consideráveis foram feitas nesse mecanismo no EF 9.0; as versões mais antigas não detectam e usam leituras de ponto de forma confiável.
Paginação
Observação
Esse recurso foi introduzido no EF Core 9.0 e ainda é experimental. Queremos saber como ele funciona para você e se você tem algum comentário.
A paginação refere-se à recuperação de resultados em páginas, em vez de todos de uma vez; isso normalmente é feito para grandes conjuntos de resultados, em que uma interface do usuário é exibida, permitindo que os usuários naveguem pelas páginas dos resultados.
Uma maneira comum de implementar a paginação com bancos de dados é usar os operadores Skip
e Take
LINQ (OFFSET
e LIMIT
no SQL). Considerando um tamanho de página de 10 resultados, a terceira página pode ser buscada com o EF Core da seguinte maneira:
var position = 20;
var nextPage = context.Session
.OrderBy(s => s.Id)
.Skip(position)
.Take(10)
.ToList();
Infelizmente, essa técnica é bastante ineficiente e pode aumentar os custos de consulta consideravelmente. O Azure Cosmos DB fornece um mecanismo especial para paginação do resultado de uma consulta por meio do uso de tokens de continuação:
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
}
Em vez de encerrar a consulta LINQ com ToListAsync
ou similar, usamos o método ToPageAsync
, instruindo-o a obter no máximo 10 itens em cada página (observe que pode haver menos itens no banco de dados). Como essa é nossa primeira consulta, queremos obter resultados desde o início e passar null
como o token de continuação. ToPageAsync
retorna um CosmosPage
, que expõe um token de continuação e os valores na página (até 10 itens). Seu programa normalmente enviará esses valores para o cliente, juntamente com o token de continuação; isso permitirá retomar a consulta mais tarde e buscar mais resultados.
Vamos supor que o usuário agora clique no botão "Avançar" em sua interface do usuário, solicitando os próximos 10 itens. Você pode executar a consulta da seguinte maneira:
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
}
Executamos a mesma consulta, mas desta vez passamos o token de continuação recebido da primeira execução; isso instrui o mecanismo de consulta a continuar a consulta de onde parou e buscar os próximos 10 itens. Assim que buscarmos a última página e não houver mais resultados, o token de continuação será null
, e o botão "Avançar" pode ficar esmaecido. Este método de paginação é extremamente eficiente e econômico em comparação com o uso de Skip
e Take
.
Para saber mais sobre paginação no Azure Cosmos DB, consulte esta página.
Observação
O Azure Cosmos DB não dá suporte à paginação reversa e não fornece uma contagem do total de páginas ou itens.
No momento, ToPageAsync
é anotado como experimental, pois pode ser substituído por uma API de paginação EF mais geral que não é específica do Azure Cosmos DB. Embora o uso da API atual gere um aviso de compilação (EF9102
), isso deve ser seguro; alterações futuras podem exigir pequenos ajustes na forma da API.
FindAsync
FindAsync
é uma API útil para obter uma entidade por sua chave primária e evitar uma viagem de ida e volta do banco de dados quando a entidade já foi carregada e é rastreada pelo contexto.
Os desenvolvedores familiarizados com bancos de dados relacionais estão acostumados com a chave primária de um tipo de entidade que consiste, por exemplo, em uma propriedade Id
. Ao usar o provedor EF Azure Cosmos DB, a chave primária contém as propriedades da chave de partição, além da propriedade mapeada para a propriedade JSON id
; esse é o caso já que o Azure Cosmos DB permite que partições diferentes contenham documentos com a mesma propriedade JSON id
e, portanto, somente a id
combinada e chave de partição identificam exclusivamente um único documento em um contêiner:
public class Session
{
public Guid Id { get; set; }
public string PartitionKey { get; set; }
...
}
var mySession = await context.FindAsync(id, pkey);
Se tiver uma chave de partição hierárquica, você deverá passar todos os valores de chave de partição para FindAsync
, na ordem em que foram configurados.
Observação
Use FindAsync
somente quando a entidade já puder ser rastreada pelo seu contexto e você quiser evitar a viagem de ida e volta do banco de dados.
Caso contrário, basta usar SingleAsync
. Não há diferença de desempenho entre os dois quando a entidade precisa ser carregada a partir do banco de dados.
Consultas SQL
As consultas também podem ser gravadas diretamente no SQL. Por exemplo:
var rating = 3;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Rating > {rating}")
.ToListAsync();
Essa consulta resulta na seguinte execução de consulta:
SELECT VALUE s
FROM (
SELECT VALUE c FROM root c WHERE c.Angle1 <= @p0
) s
Observe que FromSql
foi introduzido no EF 9.0. Nas versões anteriores, FromSqlRaw
pode ser usado em vez disso, mas observe que esse método é vulnerável a ataques de injeção de SQL.
Para obter mais informações sobre consultas SQL, consulte a documentação relacional sobre consultas SQL; a maior parte desse conteúdo também é relevante para o provedor do Azure Cosmos DB.
Mapeamentos de função
Esta seção mostra quais métodos e membros do .NET são convertidos em quais funções SQL ao fazer consultas com o provedor do Azure Cosmos DB.
Funções de data e hora
.NET | SQL | Adicionado |
---|---|---|
DateTime.UtcNow | GetCurrentDateTime() | |
DateTimeOffset.UtcNow | GetCurrentDateTime() | |
dateTime.Year1 | DateTimePart("aaaa", dateTime) | Entity Framework Core 9.0 |
dateTimeOffset.Year1 | DateTimePart("aaaa", dateTimeOffset) | Entity Framework Core 9.0 |
dateTime.AddYears(years)1 | DateTimeAdd("aaaa", dateTime) | Entity Framework Core 9.0 |
dateTimeOffset.AddYears(years)1 | DateTimeAdd("aaaa", dateTimeOffset) | Entity Framework Core 9.0 |
1 Os outros membros componentes também são traduzidos (Mês, Dia...).
Funções numéricas
.NET | SQL | Adicionado |
---|---|---|
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(@valor) | |
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(@valor) | |
Math.Sin(a) | SIN(@a) | |
Math.Sqrt(d) | SQRT(@d) | |
Math.Tan(a) | TAN(@a) | |
Math.Truncate(d) | TRUNC(@d) |
Dica
Além dos métodos listados aqui, as implementações matemáticas genéricas correspondentes e os métodos MathF também são traduzidos. Por exemplo, Math.Sin
, MathF.Sin
, double.Sin
e float.Sin
são todos mapeados para a função SIN
no SQL.
Funções de cadeia de caracteres
.NET | SQL | Adicionado |
---|---|---|
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(valor, StringComparison.Ordinal) | CONTAINS(@stringValue, @value, false) | Entity Framework Core 9.0 |
stringValue.Contains(valor, StringComparison.OrdinalIgnoreCase) | CONTAINS(@stringValue, @value, true) | Entity Framework Core 9.0 |
stringValue.EndsWith(value) | ENDSWITH(@stringValue, @value) | |
stringValue.EndsWith(valor, StringComparison.Ordinal) | ENDSWITH(@stringValue, @value, false) | Entity Framework Core 9.0 |
stringValue.EndsWith(valor, StringComparison.OrdinalIgnoreCase) | ENDSWITH(@stringValue, @value, true) | Entity Framework 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(valor, StringComparison.Ordinal) | STARTSWITH(@stringValue, @value, false) | Entity Framework Core 9.0 |
stringValue.StartsWith(valor, StringComparison.OrdinalIgnoreCase) | STARTSWITH(@stringValue, @value, true) | Entity Framework 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) |
Funções diversas
.NET | SQL | Observações |
---|---|---|
collection.Contains(item) | @item IN @collection | |
EF.Functions.CoalesceUndefined(x, y)1 | x ?? a | Adicionado no EF Core 9.0 |
EF.Functions.IsDefined(x) | IS_DEFINED(x) | Adicionado no EF Core 9.0 |
EF.Functions.VectorDistance(vector1, vector2)2 | VectorDistance(vector1, vector2) | Adicionado no EF Core 9.0, Experimental |
EF.Functions.VectorDistance(vector1, vector2, bruteForce)2 | VectorDistance(vector1, vector2, bruteForce) | Adicionado no EF Core 9.0, Experimental |
EF.Functions.VectorDistance(vector1, vector2, bruteForce, distanceFunction)2 | VectorDistance(vector1, vector2, bruteForce, distanceFunction) | Adicionado no EF Core 9.0, Experimental |
1 Observe que EF.Functions.CoalesceUndefined
une undefined
, não null
. Para unir null
, use o operador de C# ??
regular.
2 Consulte a documentação para obter informações sobre como usar a busca em vetores no Azure Cosmos DB, que é experimental. As APIs estão sujeitas a alterações.