Wykonywanie zapytań za pomocą dostawcy EF Core Azure Cosmos DB

Podstawowe informacje dotyczące wykonywania zapytań

Zapytania EF Core LINQ można wykonywać względem usługi Azure Cosmos DB w taki sam sposób, jak w przypadku innych dostawców baz danych. Na przykład:

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

Uwaga

Dostawca usługi Azure Cosmos DB nie tłumaczy tego samego zestawu zapytań LINQ co inni dostawcy. Na przykład operator EF Include() nie jest obsługiwany w usłudze Azure Cosmos DB, ponieważ zapytania między dokumentami nie są obsługiwane w bazie danych.

Klucze partycji

Zaletą partycjonowania jest wykonywanie zapytań tylko względem partycji, w której znajdują się odpowiednie dane, co pozwala zaoszczędzić koszty i przyspieszyć szybkość wyników. Zapytania, które nie określają kluczy partycji są wykonywane na wszystkich partycjach, co może być dość kosztowne.

Począwszy od programu EF 9.0, program EF automatycznie wykrywa i wyodrębnia porównania kluczy partycji w operatorach zapytania Where LINQ. Załóżmy, że wykonamy następujące zapytanie względem typu Session jednostki skonfigurowanego przy użyciu klucza partycji hierarchicznej:

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("00aa00aa-bb11-cc22-dd33-44ee44ee44ee");
var username = "scott";

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId > 0
             && e.Username == username)
    .ToListAsync();

Sprawdzając dzienniki wygenerowane przez platformę EF, widzimy, że to zapytanie zostało wykonane w następujący sposób:

Executed ReadNext (166.6985 ms, 2.8 RU) ActivityId='312da0d2-095c-4e73-afab-27072b5ad33c', Container='test', Partition='["Microsoft","00aa00aa-bb11-cc22-dd33-44ee44ee44ee"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE ((c["SessionId"] > 0) AND CONTAINS(c["Username"], "a"))

W tych dziennikach zauważymy następujące kwestie:

  • Pierwsze dwa porównania - na TenantId i UserId - zostały wyciągnięte i pojawiają się w partycji ReadNext zamiast w klauzuli WHERE; oznacza to, że zapytanie będzie wykonywane tylko na częściach podrzędnych dla tych wartości.
  • SessionId jest również częścią hierarchicznego klucza partycji, ale zamiast porównania równości używa operatora większy niż (>), dlatego nie można go wyodrębnić. Jest częścią klauzuli WHERE podobnie jak każda zwykła właściwość.
  • Username jest zwykłą właściwością , a nie częścią klucza partycji , a zatem pozostaje również w klauzuli WHERE .

Należy pamiętać, że mimo że niektóre wartości klucza partycji nie są podane, hierarchiczne klucze partycji nadal zezwalają na określanie wartości docelowych tylko części podrzędnych odpowiadających dwóm pierwszym właściwościom. Chociaż nie jest to tak wydajne, jak kierowanie do pojedynczej partycji (zidentyfikowanej przez wszystkie trzy właściwości), nadal jest o wiele bardziej wydajne niż kierowanie wszystkich partycji.

Zamiast odwoływać się do właściwości klucza partycji w Where operatorze, można je jawnie określić za pomocą WithPartitionKey operatora :

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

To wykonywane jest w taki sam sposób jak powyższe zapytanie i można to preferować, jeśli chcesz, aby klucze partycji były bardziej jednoznaczne w zapytaniach. Użycie WithPartitionKey może być konieczne w wersjach programu EF przed wersją 9.0 – monitoruj dzienniki, aby upewnić się, że zapytania używają kluczy partycji zgodnie z oczekiwaniami.

Odczyt punktu

Chociaż usługa Azure Cosmos DB umożliwia zaawansowane wykonywanie zapytań za pośrednictwem języka SQL, takie zapytania mogą być dość kosztowne. Usługa Azure Cosmos DB również obsługuje odczyty punktowe, które powinny być używane podczas pobierania pojedynczego dokumentu, jeśli jest znana zarówno id właściwość, jak i cały klucz partycji. Bezpośrednie odczyty punktowe identyfikują konkretny dokument w określonej partycji oraz działają niezwykle wydajnie i tanio w porównaniu z pobieraniem tego samego dokumentu za pomocą zapytania. Zaleca się zaprojektowanie systemu tak, aby jak najczęściej korzystać z odczytów punktowych. Aby dowiedzieć się więcej, zobacz dokumentację usługi Azure Cosmos DB.

W poprzedniej sekcji widzieliśmy, jak EF identyfikuje i wyodrębnia porównania kluczy partycji z klauzuli Where w celu bardziej efektywnego zapytania, co ogranicza przetwarzanie do adekwatnych partycji. Można pójść o krok dalej i określić id właściwość także w zapytaniu. Przeanalizujmy następujące zapytanie:

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

W tym zapytaniu podano wartość właściwości Id (która jest mapowana na właściwość id usługi Azure Cosmos DB), a także wartości dla wszystkich właściwości klucza partycji. Ponadto w zapytaniu nie ma żadnych dodatkowych składników. Po spełnieniu wszystkich tych warunków program EF może wykonać zapytanie jako odczyt punktowy.

Executed ReadItem (46 ms, 1 RU) ActivityId='d7391311-2266-4811-ae2d-535904c42c43', Container='test', Id='9', Partition='["Microsoft","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",10.0]'

Zwróć uwagę na element ReadItem, który wskazuje, że zapytanie zostało wykonane jako wydajny odczyt punktowy — nie jest zaangażowane żadne zapytanie SQL.

Należy pamiętać, że podobnie jak w przypadku wyodrębniania klucza partycji, w programie EF 9.0 wprowadzono znaczące ulepszenia tego mechanizmu; starsze wersje nie wykrywają i nie wykorzystują niezawodnie odczytów punktowych.

Paginacja

Uwaga

Ta funkcja została wprowadzona w programie EF Core 9.0 i jest nadal eksperymentalna. Poinformuj nas, jak to działa dla Ciebie i jeśli masz jakieś opinie.

Stronicowanie odnosi się do pobierania wyników na stronach, a nie wszystkich jednocześnie; Zazwyczaj jest to wykonywane w przypadku dużych zestawów wyników, w których jest wyświetlany interfejs użytkownika, co umożliwia użytkownikom nawigowanie po stronach wyników.

Typowym sposobem implementacji stronicowania z bazami danych jest użycie SkipTake operatorów LINQ (OFFSET i LIMIT w języku SQL). Biorąc pod uwagę rozmiar strony 10 wyników, trzecią stronę można pobrać za pomocą programu EF Core w następujący sposób:

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

Niestety, ta technika jest dość nieefektywna i może znacznie zwiększyć koszty zapytań. Usługa Azure Cosmos DB udostępnia specjalny mechanizm stronicowania za pośrednictwem wyniku zapytania przy użyciu tokenów kontynuacji:

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
}

Zamiast przerywać zapytanie LINQ za pomocą ToListAsync lub podobnej, używamy metody ToPageAsync, nakazując jej uzyskanie maksymalnie 10 elementów na każdej stronie (należy pamiętać, że w bazie danych może znajdować się mniej elementów). Ponieważ jest to nasze pierwsze zapytanie, chcemy uzyskać wyniki od początku i przekazać null jako token kontynuacji. ToPageAsync Zwraca element CosmosPage<T>, który uwidacznia token kontynuacji i wartości na stronie (maksymalnie 10 elementów). Program zazwyczaj wysyła te wartości do klienta wraz z tokenem kontynuacji; pozwoli to wznowić zapytanie później i pobrać więcej wyników.

Załóżmy, że użytkownik kliknie teraz przycisk "Dalej" w interfejsie użytkownika z prośbą o kolejne 10 elementów. Następnie możesz wykonać zapytanie w następujący sposób:

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
}

Wykonamy to samo zapytanie, ale tym razem przekazujemy token kontynuacji otrzymany z pierwszego wykonania; spowoduje to, że silnik zapytań będzie kontynuować zapytanie, gdzie zakończyło się, żeby pobrać następne 10 elementów. Po pobraniu ostatniej strony i gdy nie ma już więcej wyników, token kontynuacji będzie miał wartość null, a przycisk "Dalej" może zostać wyszarzony. Ta metoda stronicowania jest niezwykle wydajna i ekonomiczna w porównaniu z użyciem Skip i Take.

Aby dowiedzieć się więcej na temat stronicowania w usłudze Azure Cosmos DB, zobacz tę stronę.

Uwaga

Usługa Azure Cosmos DB nie obsługuje stronicowania wstecznego i nie udostępnia liczby wszystkich stron ani elementów.

ToPageAsync jest obecnie oznaczona jako eksperymentalna, ponieważ może zostać zastąpiona bardziej ogólnym interfejsem API stronicowania EF, który nie jest specyficzny dla usługi Azure Cosmos DB. Mimo że użycie bieżącego interfejsu API spowoduje wygenerowanie ostrzeżenia o kompilacji (EF9102), powinno to być bezpieczne — przyszłe zmiany mogą wymagać drobnych poprawek w kształcie interfejsu API.

FindAsync

FindAsync jest przydatnym interfejsem API do pobierania encji przez klucz główny i unikania dodatkowych zapytań do bazy danych, gdy encja została już załadowana i jest śledzona przez kontekst.

Deweloperzy zaznajomieni z relacyjnymi bazami danych są przyzwyczajeni do klucza podstawowego typu jednostki składającego się np. z Id właściwości. W przypadku korzystania z dostawcy usługi EF Azure Cosmos DB klucz podstawowy zawiera właściwości klucza partycji oprócz właściwości zamapowanej na właściwość JSON id . Dzieje się tak, ponieważ usługa Azure Cosmos DB umożliwia różnym partycjom zawieranie dokumentów z tą samą właściwością JSON id , a więc tylko klucz połączony id i klucz partycji jednoznacznie identyfikują pojedynczy dokument w kontenerze:

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

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

Jeśli masz hierarchiczny klucz partycji, musisz przekazać wszystkie wartości klucza partycji do elementu FindAsync w kolejności, w której zostały skonfigurowane.

Uwaga

Używaj FindAsync tylko wtedy, gdy jednostka może być już śledzona przez kontekst i chcesz uniknąć dwukierunkowego przejścia bazy danych. W przeciwnym razie po prostu użyj SingleAsync — nie ma różnicy w wydajności, gdy obiekt musi zostać załadowany z bazy danych.

Zapytania SQL

Zapytania można również zapisywać bezpośrednio w języku SQL. Na przykład:

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

To zapytanie powoduje wykonanie następującego zapytania:

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

Należy pamiętać, że FromSql wprowadzono w programie EF 9.0. W poprzednich wersjach zamiast tego można użyć FromSqlRaw, chociaż należy pamiętać, że ta metoda jest podatna na ataki polegające na wstrzyknięciu kodu SQL.

Aby uzyskać więcej informacji na temat wykonywania zapytań SQL, zobacz dokumentację relacyjną dotyczącą zapytań SQL. Większość tej zawartości jest również odpowiednia dla dostawcy usługi Azure Cosmos DB.

Mapowania funkcji

W tej sekcji pokazano, które metody i elementy członkowskie platformy .NET są tłumaczone na funkcje SQL podczas wykonywania zapytań za pomocą dostawcy usługi Azure Cosmos DB.

Funkcje daty i godziny

.NET SQL Dodano w
DateTime.UtcNow GetCurrentDateTime()
DateTimeOffset.UtcNow GetCurrentDateTime()
dateTime.Year1 DateTimePart("yyyy", dateTime) EF 9
dateTimeOffset.Year1 DateTimePart("yyyy", dateTimeOffset) EF 9
dateTime.AddYears(years)1 DateTimeAdd("yyyy", dateTime) EF 9
dateTimeOffset.AddYears(years)1 DateTimeAdd("yyyy", dateTimeOffset) EF 9

1 Pozostałe składowe są również tłumaczone (Miesiąc, Dzień...).

Funkcje liczbowe

.NET SQL
podwójny. DegreesToRadians(x) RADIANS (@x)
podwójny. RadiansToDegrees(x) DEGREES(@x)
EF.Functions.Random() RAND()
Math.Abs(wartość) 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(wartość) SIGN(@value)
Math.Sin(a) SIN(@a)
Math.Sqrt(d) SQRT(@d)
Math.Tan(a) TAN(@a)
Math.Truncate(d) TRUNC(@d)

Wskazówka

Oprócz metod wymienionych tutaj, odpowiednie ogólne implementacje matematyczne i metody MathF są również tłumaczone. Na przykład, Math.Sin, MathF.Sin, double.Sin i float.Sin wszystkie mapują do funkcji SIN w języku SQL.

Funkcje ciągów

.NET SQL Dodano w
Regex.IsMatch(input, pattern) (sprawdza dopasowanie wzorca w danych wejściowych) RegexMatch(@pattern, @input)
Regex.IsMatch(dane wejściowe, wzorzec, opcje) RegexMatch(@input, @pattern, @options)
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(wartość) CONTAINS(@stringValue, @value)
stringValue.Contains(value, StringComparison.Ordinal) CONTAINS(@stringValue, @value, false) EF 9
stringValue.Contains(value, StringComparison.OrdinalIgnoreCase) // Sprawdza, czy stringValue zawiera 'value', ignorując wielkość liter CONTAINS(@stringValue, @value, true) EF 9
stringValue.EndsWith(value) ENDSWITH(@stringValue, @value)
stringValue.EndsWith(value, StringComparison.Ordinal) ENDSWITH(@stringValue, @value, false) EF 9
stringValue.EndsWith(wartość, StringComparison.OrdinalIgnoreCase) ENDSWITH(@stringValue, @value, true) EF 9
stringValue.Equals(value, StringComparison.Ordinal) STRINGEQUALS(@stringValue, @value)
stringValue.Equals(wartość, StringComparison.OrdinalIgnoreCase) STRINGEQUALS(@stringValue, @value, true)
stringValue.FirstOrDefault() LEFT(@stringValue, 1)
stringValue.IndexOf(wartość) 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 9
stringValue.StartsWith(value, StringComparison.OrdinalIgnoreCase) // Sprawdza, czy 'stringValue' zaczyna się od 'value', ignorując różnice w wielkości liter. STARTSWITH(@stringValue, @value, true) EF 9
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)
.NET SQL Dodano w
VectorDistance(wektor1, wektor2). VectorDistance(vector1, vector2) EF 9
VectorDistance(vector1, vector2, bruteForce) VectorDistance(vector1, vector2, bruteForce) EF 9
VectorDistance(vector1, vector2, bruteForce, funkcja odległości) VectorDistance(vector1, vector2, bruteForce, distanceFunction) EF 9
FullTextContains(właściwość, słowo kluczowe) FullTextContains(właściwość, słowo kluczowe) EF 10
FullTextContainsAll(właściwość, słowo kluczowe1, słowo kluczowe2) FullTextContainsAll(właściwość, słowo kluczowe1, słowo kluczowe2) EF 10
FullTextContainsAny(właściwość, słowo kluczowe1, słowo kluczowe2) FullTextContainsAny(atrybut, słowo kluczowe1, słowo kluczowe2) EF 10
FullTextScore(właściwość, słowo kluczowe1, słowo kluczowe2) FullTextScore(właściwość, słowo kluczowe1, słowo kluczowe2) EF 10
Rrf(search1, search2) RRF(właściwość, search1, search2). EF 10
Rrf(nowe[] { search1, search2 }, wagi) RRF(właściwość, search1, search2, wagi) EF 10

Aby uzyskać więcej informacji na temat wyszukiwania wektorowego, zobacz dokumentację. Aby uzyskać więcej informacji na temat wyszukiwania pełnotekstowego, zobacz dokumentację.

Różne funkcje

.NET SQL Dodano w
kolekcja.Zawiera(element) @item W @collection
CoalesceUndefined(x, y)1 X?? y EF 9
IsDefined(x) IS_DEFINED(x) EF 9

1 Należy pamiętać, że CoalesceUndefined scala undefined, a nie null. Aby połączyć null, użyj zwykłego operatora C# ??.