Condividi tramite


Esecuzione di query con il provider EF Core di Azure Cosmos DB

Nozioni di base sulle query

Le query LINQ di EF Core possono essere eseguite in Azure Cosmos DB allo stesso modo di altri provider di database. Ad esempio:

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

Il provider Azure Cosmos DB non converte lo stesso set di query LINQ di altri provider. Ad esempio, l'operatore EF Include() non è supportato in Azure Cosmos DB, perché le query tra documenti non sono supportate nel database.

Chiavi di partizione

Il vantaggio del partizionamento consiste nel fare in modo che le query vengano eseguite solo sulla partizione in cui si trovano i dati pertinenti, risparmiando i costi e garantendo una velocità dei risultati più rapida. Le query che non specificano le chiavi di partizione vengono eseguite in tutte le partizioni, che possono essere piuttosto costose.

A partire da EF 9.0, EF rileva ed estrae automaticamente i confronti delle chiavi di partizione negli operatori della query LINQ Where. Si supponga di eseguire la query seguente sul tipo di entità Session configurato con una chiave di partizione gerarchica:

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

Esaminando i log generati da Entity Framework, questa query viene eseguita nel modo seguente:

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

In questi log si noterà quanto segue:

  • I primi due confronti, su TenantId e UserId , sono stati rimossi e vengono visualizzati nella ReadNext "Partizione" anziché nella clausola . Ciò significa che la WHERE query verrà eseguita solo nelle sottopartizioni per tali valori.
  • SessionId fa anche parte della chiave di partizione gerarchica, ma invece di un confronto di uguaglianza, usa un operatore maggiore di (>) e pertanto non può essere rimosso. Fa parte della WHERE clausola come qualsiasi proprietà regolare.
  • Username è una proprietà regolare, non parte della chiave di partizione, e pertanto rimane anche nella WHERE clausola .

Si noti che, anche se alcuni dei valori della chiave di partizione non vengono forniti, le chiavi di partizione gerarchica consentono comunque la destinazione solo delle sottoparti che corrispondono alle prime due proprietà. Anche se questo non è così efficiente come la destinazione di una singola partizione (come identificato da tutte e tre le proprietà), è ancora molto più efficiente rispetto alla destinazione di tutte le partizioni.

Anziché fare riferimento alle proprietà della chiave di partizione in un Where operatore, è possibile specificarle in modo esplicito usando l'operatore WithPartitionKey :

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

Questa operazione viene eseguita nello stesso modo della query precedente e può essere preferibile se si desidera rendere più esplicite le chiavi di partizione nelle query. L'uso WithPartitionKey di può essere necessario nelle versioni di Entity Framework precedenti alla 9.0. Tenere d'occhio i log per assicurarsi che le query usino chiavi di partizione come previsto.

Letture di punti

Anche se Azure Cosmos DB consente query avanzate tramite SQL, tali query possono risultare piuttosto costose. Azure Cosmos DB supporta anche le letture dei punti, che devono essere usate durante il recupero di un singolo documento se sono note sia la id proprietà che l'intera chiave di partizione. Le letture di punti identificano direttamente un documento specifico in una partizione specifica ed eseguono in modo estremamente efficiente e con costi ridotti rispetto al recupero dello stesso documento con una query. È consigliabile progettare il sistema per sfruttare le letture dei punti il più spesso possibile. Per altre informazioni, vedere la documentazione di Azure Cosmos DB.

Nella sezione precedente è stato illustrato l'identificazione e l'estrazione di confronti tra chiavi di partizione dalla clausola Where per eseguire query più efficienti, limitando l'elaborazione solo alle partizioni pertinenti. È possibile procedere ulteriormente e specificare anche la proprietà id nella query. Esaminiamo la query seguente:

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

In questa query viene fornito un valore per la Id proprietà (mappato alla proprietà Azure Cosmos DB id ), nonché i valori per tutte le proprietà della chiave di partizione. Inoltre, non sono presenti componenti aggiuntivi per la query. Quando vengono soddisfatte tutte queste condizioni, Entity Framework è in grado di eseguire la query come punto letto:

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

Si noti ReadItem, che indica che la query è stata eseguita come punto di lettura efficiente. Non è interessata alcuna query SQL.

Si noti che, come per l'estrazione delle chiavi di partizione, sono stati apportati miglioramenti significativi a questo meccanismo in EF 9.0; Le versioni precedenti non rilevano e usano in modo affidabile le letture dei punti.

Impaginazione

Nota

Questa funzionalità è stata introdotta in EF Core 9.0 ed è ancora sperimentale. Comunicaci come funziona per te e se hai commenti e suggerimenti.

L'impaginazione si riferisce al recupero dei risultati nelle pagine, anziché contemporaneamente; questa operazione viene in genere eseguita per set di risultati di grandi dimensioni, in cui viene visualizzata un'interfaccia utente, consentendo agli utenti di spostarsi tra le pagine dei risultati.

Un modo comune per implementare la paginazione con i database consiste nell'usare gli operatori Skip e Take LINQ (OFFSET e LIMIT in SQL). Data una dimensione di pagina di 10 risultati, la terza pagina può essere recuperata con EF Core come indicato di seguito:

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

Sfortunatamente, questa tecnica è abbastanza inefficiente e può aumentare notevolmente i costi di query. Azure Cosmos DB offre un meccanismo speciale per la paginazione tramite il risultato di una query, tramite l'uso di token di continuazione:

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
}

Invece di terminare la query LINQ con ToListAsync o simile, viene usato il ToPageAsync metodo , che indica di ottenere al massimo 10 elementi in ogni pagina (si noti che potrebbero essere presenti meno elementi nel database). Poiché si tratta della prima query, si vogliono ottenere risultati dall'inizio e passare null come token di continuazione. ToPageAsync restituisce CosmosPage, che espone un token di continuazione e i valori nella pagina (fino a 10 elementi). Il programma in genere invierà tali valori al client, insieme al token di continuazione; in questo modo sarà possibile riprendere la query in un secondo momento e recuperare più risultati.

Si supponga che l'utente faccia clic sul pulsante "Avanti" nell'interfaccia utente, chiedendo i 10 elementi successivi. È quindi possibile eseguire la query nel modo seguente:

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
}

Viene eseguita la stessa query, ma questa volta si passa il token di continuazione ricevuto dalla prima esecuzione; indica al motore di query di continuare la query in cui è stata interrotta e recuperare i 10 elementi successivi. Dopo aver recuperato l'ultima pagina e non sono presenti altri risultati, il token di continuazione sarà null e il pulsante "Avanti" può essere disattivato. Questo metodo di impaginazione è estremamente efficiente e conveniente rispetto all'uso Skip di e Take.

Per altre informazioni sulla paginazione in Azure Cosmos DB, vedere questa pagina.

Nota

Azure Cosmos DB non supporta l'impaginazione all'indietro e non fornisce un conteggio delle pagine o degli elementi totali.

ToPageAsync è attualmente annotato come sperimentale, poiché può essere sostituito con un'API di impaginazione EF più generica che non è specifica di Azure Cosmos DB. Anche se l'uso dell'API corrente genererà un avviso di compilazione (EF9102), questa operazione dovrebbe essere sicura. Le modifiche future potrebbero richiedere modifiche minime nella forma API.

FindAsync

FindAsync è un'API utile per ottenere un'entità in base alla chiave primaria ed evitare un round trip del database quando l'entità è già stata caricata e viene monitorata dal contesto.

Gli sviluppatori che hanno familiarità con i database relazionali vengono usati per la chiave primaria di un tipo di entità costituito, ad esempio, da una proprietà Id. Quando si usa il provider Azure Cosmos DB di Entity Framework, la chiave primaria contiene le proprietà della chiave di partizione oltre alla proprietà mappata alla proprietà JSON id . Questo è il caso in cui Azure Cosmos DB consente a partizioni diverse di contenere documenti con la stessa proprietà JSON id e quindi solo la chiave combinata id e di partizione identifica in modo univoco un singolo documento in un contenitore:

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

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

Se si dispone di una chiave di partizione gerarchica, è necessario passare tutti i valori della chiave di partizione a FindAsync, nell'ordine in cui sono stati configurati.

Nota

Usare FindAsync solo quando l'entità potrebbe essere già rilevata dal contesto e si vuole evitare il round trip del database. In caso contrario, è sufficiente usare SingleAsync: non esiste alcuna differenza di prestazioni tra i due quando l'entità deve essere caricata dal database.

Query SQL

Le query possono anche essere scritte direttamente in SQL. Ad esempio:

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

Questa query restituisce l'esecuzione di query seguente:

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

Si noti che FromSql è stato introdotto in EF 9.0. Nelle versioni precedenti, FromSqlRaw è possibile usare invece , anche se si noti che il metodo è vulnerabile agli attacchi SQL injection.

Per altre informazioni sull'esecuzione di query SQL, vedere la documentazione relazionale sulle query SQL. La maggior parte di tale contenuto è rilevante anche per il provider Azure Cosmos DB.

Mapping delle funzioni

Questa sezione illustra i metodi e i membri .NET tradotti in quali funzioni SQL durante l'esecuzione di query con il provider Azure Cosmos DB.

Funzioni di data e ora

.NET SQL Aggiunta in
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(anni)1 DateTimeAdd("yyyy", dateTime) EF Core 9.0
dateTimeOffset.AddYears(anni)1 DateTimeAdd("yyyy", dateTimeOffset) EF Core 9.0

1 Gli altri membri del componente vengono tradotti anche (Month, Day...).

Funzioni numeriche

.NET SQL Aggiunta in
double.DegreesToRadians(x) RADIANS (@x) EF Core 8.0
double. RadiansToDegrees(x) DEGREES (@x) EF Core 8.0
EF.Functions.Random() RAND()
Math.Abs(valore) ABS(@valore)
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(valore) SIGN(@valore)
Math.Sin(a) SIN(@a)
Math.Sqrt(d) SQRT(@d)
Math.Tan(a) TAN(@a)
Math.Truncate(d) TRUNC(@d)

Suggerimento

Oltre ai metodi elencati qui, vengono tradotte anche le implementazioni matematiche generiche corrispondenti e i metodi MathF. Ad esempio, Math.Sin, MathF.Sindouble.Sin, e float.Sin tutto esegue il mapping alla SIN funzione in SQL.

Funzioni di stringa

.NET SQL Aggiunta in
Regex.IsMatch(input, modello) RegexMatch(@pattern, @input) EF Core 7.0
Regex.IsMatch(input, modello, opzioni) RegexMatch(@input, @pattern, @options) EF Core 7.0
corda. 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(valore) 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(valore) 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)

Funzioni varie

.NET SQL Note
collection.Contains(item) @item IN @collection
EF. Functions.CoalesceUndefined(x, y)1 x ?? y Aggiunta in EF Core 9.0
EF.Functions.IsDefined(x) IS_DEFINED(x) Aggiunta in EF Core 9.0
EF. Functions.VectorDistance(vector1, vector2)2 VectorDistance(vector1, vector2) Aggiunta in EF Core 9.0, Sperimentale
EF. Functions.VectorDistance(vector1, vector2, bruteForce)2 VectorDistance(vector1, vector2, bruteForce) Aggiunta in EF Core 9.0, Sperimentale
EF. Functions.VectorDistance(vector1, vector2, bruteForce, distanceFunction)2 VectorDistance(vector1, vector2, bruteForce, distanceFunction) Aggiunta in EF Core 9.0, Sperimentale

1 Si noti che EF.Functions.CoalesceUndefined unisce undefined, non null. Per unire null, usare l'operatore C# ?? normale.

2 Vedere la documentazione per informazioni sull'uso della ricerca vettoriale in Azure Cosmos DB, che è sperimentale. Le API sono soggette a modifiche.