Partager via


Requête avec le fournisseur EF Core Azure Cosmos DB

Notions de base sur les requêtes

Les requêtes LINQ EF Core peuvent être exécutées sur Azure Cosmos DB de la même façon que pour d’autres fournisseurs de base de données. Par exemple :

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

Remarque

Le fournisseur Azure Cosmos DB ne traduit pas le même ensemble de requêtes LINQ que d’autres fournisseurs. Par exemple, l'opérateur EF Include() n'est pas pris en charge par Cosmos, car les requêtes inter-documents ne sont pas prises en charge dans la base de données.

Clés de partition

L'avantage du partitionnement est que vos requêtes ne s'exécutent que sur la partition où se trouvent les données pertinentes, ce qui permet d'économiser des coûts et d'obtenir des résultats plus rapidement. Les requêtes qui ne spécifient pas de clés de partition sont exécutées sur toutes les partitions, ce qui peut être assez coûteux.

Depuis EF 9.0, EF détecte et extrait automatiquement les comparaisons de clés de partition dans les opérateurs Where de votre requête LINQ. Supposons que nous exécutions la requête suivante sur notre type d'entité Session, qui est configuré avec une clé de partition hiérarchique :

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

En examinant les journaux générés par EF, nous constatons que cette requête est exécutée comme suit :

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

Dans ces journaux, nous remarquons ce qui suit :

  • Les deux premières comparaisons - sur TenantId et UserId - ont été supprimées et apparaissent dans la ReadNext « Partition » plutôt que dans la clause WHERE ; cela signifie que la requête ne s'exécutera que sur les sous-partitions pour ces valeurs.
  • SessionId fait également partie de la clé de partition hiérarchique, mais au lieu d'une comparaison d'égalité, elle utilise un opérateur plus grand que (>) et ne peut donc pas être supprimée. Elle fait partie de la clause WHERE comme n'importe quelle propriété ordinaire.
  • Username est une propriété ordinaire - ne faisant pas partie de la clé de partition - et reste donc également dans la clause WHERE.

Notez que même si certaines valeurs de la clé de partition ne sont pas fournies, les clés de partition hiérarchiques permettent toujours de cibler uniquement les sous-partitions qui correspondent aux deux premières propriétés. Bien que cela ne soit pas aussi efficace que de cibler une seule partition (identifiée par les trois propriétés), c'est toujours beaucoup plus efficace que de cibler toutes les partitions.

Plutôt que de référencer les propriétés des clés de partition dans un opérateur Where, vous pouvez les spécifier explicitement en utilisant l'opérateur WithPartitionKey :

var sessions = await context.Sessions
    .WithPartitionKey(tenantId, userId)
    .Where(e => e.SessionId > 0 && e.Username.Contains("a"))
    .ToListAsync();

Cette méthode s'exécute de la même manière que la requête ci-dessus, et peut être préférable si vous souhaitez rendre les clés de partition plus explicites dans vos requêtes. L'utilisation de WithPartitionKey peut être nécessaire dans les versions d'EF antérieures à la 9.0 - surveillez les journaux pour vous assurer que les requêtes utilisent les clés de partition comme prévu.

Lectures de points

Bien qu'Azure Cosmos DB permette d'effectuer des requêtes puissantes via SQL, ces requêtes peuvent être assez coûteuses. Cosmos DB prend également en charge les lectures ponctuelles, qui peuvent être utilisées lorsque la propriété id et la clé de partition entière sont toutes deux connues. Ces lectures ponctuelles identifient directement un document spécifique dans une partition spécifique, et s'exécutent de manière extrêmement efficace et avec des coûts réduits. Dans la mesure du possible, il vaut la peine de concevoir votre système de manière à exploiter au maximum les lectures ponctuelles. Pour en savoir plus, consultez la documentation de Cosmos DB.

Dans la section précédente, nous avons vu qu'EF identifiait et extrayait les comparaisons de clés de partition de la clause Where pour une interrogation plus efficace, en limitant le traitement aux seules partitions concernées. Il est possible d'aller plus loin et de fournir la propriété id dans la requête également. Examinons la requête suivante :

var session = await context.Sessions.SingleAsync(
    e => e.Id == someId
         && e.TenantId == tenantId
         && e.UserId == userId
         && e.SessionId == sessionId);

Dans cette requête, une valeur pour la propriété Id est fournie (qui est mappée à la propriété id de Cosmos DB), ainsi que des valeurs pour toutes les propriétés de la clé de partition. En outre, la requête ne comporte aucun élément supplémentaire. Lorsque toutes ces conditions sont remplies, EF est en mesure d'exécuter la requête en tant que lecture ponctuelle :

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]'

Notez le ReadItem, qui indique que la requête a été exécutée en tant que lecture ponctuelle efficace - aucune requête SQL n'est impliquée.

Notez que, comme pour l'extraction des clés de partition, des améliorations significatives ont été apportées à ce mécanisme dans EF 9.0 ; les versions antérieures ne détectent et n'utilisent pas de manière fiable les lectures ponctuelles.

Pagination

Remarque

Cette fonctionnalité a été introduite dans EF Core 9.0 et reste expérimentale. N'hésitez pas à nous faire savoir comment cela fonctionne pour vous et si vous avez des commentaires.

La pagination consiste à récupérer les résultats par pages, plutôt qu'en une seule fois. Cette technique est généralement utilisée pour les grands ensembles de résultats, où une interface utilisateur est affichée, permettant aux utilisateurs de naviguer à travers les pages de résultats.

Une façon courante de mettre en œuvre la pagination dans les bases de données consiste à utiliser les opérateurs LINQ Skip et Take (OFFSET et LIMIT en SQL). Étant donné une taille de page de 10 résultats, la troisième page peut être extraite avec EF Core comme suit :

var position = 20;
var nextPage = context.Session
    .OrderBy(s => s.Id)
    .Skip(position)
    .Take(10)
    .ToList();

Malheureusement, cette technique est assez inefficace et peut considérablement augmenter les coûts d'interrogation. Cosmos DB fournit un mécanisme spécial pour paginer à travers le résultat d'une requête, via l'utilisation de jetons de continuation :

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
}

Plutôt que de terminer la requête LINQ par ToListAsync ou similaire, nous utilisons la méthode ToPageAsync, en lui demandant d'obtenir au maximum 10 éléments dans chaque page (notez qu'il peut y avoir moins d'éléments dans la base de données). Comme il s'agit de notre première requête, nous aimerions obtenir les résultats depuis le début, et passer null comme jeton de continuation. ToPageAsync renvoie un CosmosPage, qui expose un jeton de continuation et les valeurs de la page (jusqu'à 10 éléments). Votre programme enverra généralement ces valeurs au client, ainsi que le jeton de continuation, ce qui permettra de reprendre la requête ultérieurement et d'obtenir davantage de résultats.

Supposons que l'utilisateur clique sur le bouton « Suivant » de l'interface utilisateur et demande les 10 éléments suivants. Vous pouvez alors exécuter la requête comme suit :

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
}

Nous exécutons la même requête, mais cette fois nous passons le jeton de continuation reçu lors de la première exécution ; cela demande à Cosmos DB de continuer la requête là où elle s'est arrêtée, et de récupérer les 10 éléments suivants. Une fois que nous avons récupéré la dernière page et qu'il n'y a plus de résultats, le jeton de continuation sera null et le bouton « Suivant » peut être grisé. Cette méthode de pagination est extrêmement efficace et rentable par rapport à l'utilisation de Skip et Take.

Pour en savoir plus sur la pagination dans Cosmos DB, consultez cette page.

Remarque

Cosmos DB ne prend pas en charge la pagination à rebours et ne fournit pas de décompte du nombre total de pages ou d'éléments.

ToPageAsync est actuellement annotée comme expérimentale, car elle pourrait être remplacée par une API de pagination EF plus générale qui ne soit pas spécifique à Cosmos. Bien que l'utilisation de l'API actuelle génère un avertissement de compilation (EF9102), cela ne devrait pas poser de problème - les modifications futures pourraient nécessiter des ajustements mineurs de la forme de l'API.

FindAsync

FindAsync est une API utile pour obtenir une entité par sa clé primaire et éviter un aller-retour dans la base de données lorsque l'entité a déjà été chargée et est suivie par le contexte.

Les développeurs familiarisés avec les bases de données relationnelles sont habitués à ce que la clé primaire d'un type d'entité consiste par exemple en une propriété Id. Lorsque vous utilisez le fournisseur EF Cosmos DB, la clé primaire contient les propriétés de la clé de partition en plus de la propriété mappée à la propriété JSON id ; c'est le cas puisque Cosmos DB permet à différentes partitions de contenir des documents avec la même propriété JSON id, et donc seule la clé combinée id et la clé de partition identifient de manière unique un document unique dans un conteneur :

public class Session
{
    public Guid Id { get; set; }
    public string PartitionKey { get; set; }
    ...
}

var mySession = await context.FindAsync(id, pkey);

Si vous disposez d'une clé de partition hiérarchique, vous devez transmettre toutes les valeurs de la clé de partition à FindAsync, dans l'ordre dans lequel elles ont été configurées.

Remarque

N'utilisez FindAsync que si l'entité est déjà suivie par votre contexte et que vous souhaitez éviter les allers-retours avec la base de données. Sinon, utilisez simplement SingleAsync - il n'y a pas de différence de performance entre les deux lorsque l'entité doit être chargée à partir de la base de données.

Requêtes SQL

Les requêtes peuvent également être écrites directement dans SQL. Par exemple :

var rating = 3;
_ = await context.Blogs
    .FromSql($"SELECT VALUE c FROM root c WHERE c.Rating > {rating}")
    .ToListAsync();

Cette requête entraîne l’exécution de la requête suivante :

SELECT VALUE s
FROM (
    SELECT VALUE c FROM root c WHERE c.Angle1 <= @p0
) s

Notez que FromSql a été introduit dans EF 9.0. Dans les versions précédentes, FromSqlRaw peut être utilisé à la place, mais notez que cette méthode est vulnérable aux attaques par injection SQL.

Pour plus d'informations sur les requêtes SQL, consultez la documentation relationnelle sur les requêtes SQL ; la majeure partie de ce contenu s'applique également au fournisseur Cosmos.

Mappages de fonctions

Cette section montre quelles méthodes et membres .NET sont traduits en fonctions SQL lorsque vous effectuez des requêtes avec le fournisseur Azure Cosmos DB.

Fonctions de date et d’heure

.NET SQL Ajouté à
DateTime.UtcNow GetCurrentDateTime()
DateTimeOffset.UtcNow GetCurrentDateTime()
dateTime.Year1 DateTimePart("yyyy", dateTime) EF Core 9.0
dateTimeOffset.Year1 DateTimePart("yyyy", dateTimeOffset) EF Core 9.0
dateTime.AddYears(years)1 DateTimeAdd("yyyy", dateTime) EF Core 9.0
dateTimeOffset.AddYears(years)1 DateTimeAdd("yyyy", dateTimeOffset) EF Core 9.0

1 Les autres membres du composant sont également traduits (Mois, Jour...).

Fonctions Numeric

.NET SQL Ajouté à
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)

Conseil

Outre les méthodes répertoriées ici, des implémentations mathématiques génériques correspondantes et des méthodes MathF sont également traduites. Par exemple, Math.Sin, MathF.Sin, double.Sin et float.Sin mappent tous vers la fonction SIN dans SQL.

Fonctions String

.NET SQL Ajouté à
Regex.IsMatch(input, pattern) RegexMatch(@pattern, @input) EF Core 7.0
Regex.IsMatch(entrée, modèle, 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)

Fonctions diverses

.NET SQL Notes
collection.Contains(item) @item IN @collection
EF.Functions.CoalesceUndefined(x, y)1 x ?? y Added in EF Core 9.0
EF.Functions.IsDefined(x) IS_DEFINED(x) Added in EF Core 9.0
EF.Functions.VectorDistance(vector1, vector2)2 VectorDistance(vector1, vector2) Ajouté dans EF Core 9.0, Expérimental
EF.Functions.VectorDistance(vector1, vector2, bruteForce)2 VectorDistance(vector1, vector2, bruteForce) Ajouté dans EF Core 9.0, Expérimental
EF.Functions.VectorDistance(vector1, vector2, bruteForce, distanceFunction)2 VectorDistance(vector1, vector2, bruteForce, distanceFunction) Ajouté dans EF Core 9.0, Expérimental

1 Notez que EF.Functions.CoalesceUndefined coalesce undefined et non null. To coalesce null, use the regular C# ?? operator.

2 See the documentation for information on using vector search in Azure Cosmos DB. La recherche vectorielle de Cosmos DB est expérimentale et les API sont susceptibles d'être modifiées.