Поделиться через


Запрос с помощью поставщика EF Core Azure Cosmos DB

Основы запроса

Запросы EF Core LINQ можно выполнять в Azure Cosmos DB так же, как и для других поставщиков баз данных. Рассмотрим пример.

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

Примечание.

Поставщик Azure Cosmos DB не преобразует тот же набор запросов LINQ, что и другие поставщики. Например, оператор EF Include() не поддерживается в Azure Cosmos DB, так как запросы между документами не поддерживаются в базе данных.

Ключи секции

Преимущество секционирования заключается в том, чтобы запросы выполнялись только в разделе, где найдены соответствующие данные, экономия затрат и обеспечение ускорения скорости результатов. Запросы, которые не указывают ключи секций, выполняются во всех секциях, что может быть довольно дорогостоящим.

Начиная с EF 9.0 EF автоматически обнаруживает и извлекает сравнения ключей секций в операторах запроса Where LINQ. Предположим, что мы выполняем следующий запрос к типу сущности Session , который настроен с помощью ключа иерархической секции:

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

Изучение журналов, созданных EF, мы видим, что этот запрос выполняется следующим образом:

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

В этих журналах мы заметим следующее:

  • Первые два сравнения - TenantId включено и UserId - были сняты и отображаются в ReadNext предложении "Секционирование", а не в WHERE предложении. Это означает, что запрос будет выполняться только в подпартиях для этих значений.
  • SessionId также является частью иерархического ключа секции, но вместо сравнения равенства он использует оператор больше, чем (>), и поэтому не может быть снят. Это часть предложения, как любое регулярное WHERE свойство.
  • Username является регулярным свойством , а не частью ключа секции , поэтому он также остается в предложении WHERE .

Обратите внимание, что хотя некоторые значения ключа секции не указаны, иерархические ключи секций по-прежнему позволяют использовать только подпартии, соответствующие первым двум свойствам. Хотя это не так эффективно, как нацеливание на одну секцию (как определено всеми тремя свойствами), это все еще гораздо эффективнее, чем назначение всех секций.

Вместо ссылки на свойства ключа секции Where в операторе можно явно указать их с помощью WithPartitionKey оператора:

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

Это выполняется так же, как и приведенный выше запрос, и может быть предпочтительнее, если вы хотите сделать ключи секции более явными в запросах. Использование WithPartitionKey может потребоваться в версиях EF до 9.0. Следите за журналами, чтобы убедиться, что запросы используют ключи секций должным образом.

Операции точечного чтения

Хотя Azure Cosmos DB позволяет эффективно запрашивать через SQL, такие запросы могут быть довольно дорогостоящими. Azure Cosmos DB также поддерживает операции чтения точек, которые следует использовать при получении одного документа, если известно как свойство, так id и весь ключ секции. Точка считывает непосредственно определенный документ в определенном разделе и выполняет чрезвычайно эффективно и с сокращением затрат по сравнению с получением того же документа с запросом. Рекомендуется разработать систему, чтобы использовать точки чтения как можно чаще. Дополнительные сведения см. в документации по Azure Cosmos DB.

В предыдущем разделе мы увидели, как EF определяет и извлекает сравнения ключей секций из Where предложения для более эффективного запроса, ограничивая обработку только соответствующими секциями. Кроме того, можно перейти к шагу и указать id свойство в запросе. Давайте рассмотрим следующий запрос:

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

В этом запросе предоставляется значение свойства Id (которое сопоставляется со свойством Azure Cosmos DB id ), а также значения для всех свойств ключа секции. Кроме того, в запросе нет дополнительных компонентов. При выполнении всех этих условий EF может выполнить запрос в виде точки чтения:

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

ReadItemОбратите внимание на то, что запрос был выполнен в качестве эффективного считывания точек. Запрос SQL не участвует.

Обратите внимание, что, как и при извлечении ключа секции, в EF 9.0 были сделаны значительные улучшения. Старые версии не надежно обнаруживают и используют операции чтения точек.

Разбиение на страницы

Примечание.

Эта функция появилась в EF Core 9.0 и по-прежнему экспериментальна. Сообщите нам, как это работает для вас, и если у вас есть какие-либо отзывы.

Разбивка на страницы ссылается на получение результатов в страницах, а не все одновременно; Обычно это делается для больших наборов результатов, где отображается пользовательский интерфейс, что позволяет пользователям перемещаться по страницам результатов.

Распространенный способ реализации разбиения на страницы с базами данных — использовать Skip Take операторы LINQ (OFFSET и LIMIT в SQL). Учитывая размер страницы 10 результатов, третью страницу можно получить с помощью EF Core следующим образом:

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

К сожалению, этот метод является довольно неэффективным и может значительно увеличить затраты на запросы. Azure Cosmos DB предоставляет специальный механизм для разбиения на страницы в результате запроса с помощью маркеров продолжения:

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
}

Вместо того, чтобы завершить запрос LINQ с ToListAsync или аналогичным образом, мы используем ToPageAsync метод, наставив его получить не более 10 элементов на каждой странице (обратите внимание, что в базе данных может быть меньше элементов). Так как это первый запрос, мы хотели бы получить результаты с самого начала и передать null в качестве маркера продолжения. ToPageAsync возвращает маркер CosmosPageпродолжения и значения на странице (до 10 элементов). Программа обычно отправляет эти значения клиенту вместе с маркером продолжения; Это позволит возобновить запрос позже и получить дополнительные результаты.

Предположим, что пользователь теперь нажимает кнопку "Далее" в пользовательском интерфейсе, запрашивая следующие 10 элементов. Затем можно выполнить запрос следующим образом:

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
}

Мы выполняем тот же запрос, но на этот раз мы передаваем маркер продолжения, полученный от первого выполнения; Это указывает обработчику запросов продолжить запрос, в котором он остался, и получить следующие 10 элементов. Когда вы получите последнюю страницу и нет дополнительных результатов, маркер продолжения будет nullиметь значение, и кнопка "Далее" может быть выделена серым цветом. Этот метод разбиения на страницы является чрезвычайно эффективным и экономичным по сравнению с использованием Skip и Take.

Дополнительные сведения о разбиении на страницы в Azure Cosmos DB см. на этой странице.

Примечание.

Azure Cosmos DB не поддерживает обратную разбивку на страницы и не предоставляет количество общих страниц или элементов.

ToPageAsync в настоящее время аннотирован как экспериментальный, так как он может быть заменен более универсальным API разбиения на страницы EF, который не является конкретным для Azure Cosmos DB. Хотя использование текущего API создаст предупреждение о компиляции (EF9102), это должно быть безопасным. Будущие изменения могут потребовать незначительных настроек в форме API.

FindAsync

FindAsync — это полезный API для получения сущности по его первичному ключу и предотвращения циклического обхода базы данных, когда сущность уже загружена и отслеживается контекстом.

Разработчики, знакомые с реляционными базами данных, используются для первичного ключа типа сущности, состоящего, например Id , свойства. При использовании поставщика EF Azure Cosmos DB первичный ключ содержит свойства ключа секции в дополнение к свойству JSON id . Это связано с тем, что Azure Cosmos DB позволяет разным секциям содержать документы с одним и тем же свойством JSON id , поэтому только объединенный id ключ и ключ секции однозначно идентифицирует один документ в контейнере:

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

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

Если у вас есть иерархический ключ секции, необходимо передать все значения FindAsyncключа секции в порядок, в котором они были настроены.

Примечание.

Используйте FindAsync только в том случае, если сущность уже может отслеживаться контекстом, и вы хотите избежать циклического обхода базы данных. В противном случае простое использование SingleAsync — нет разницы в производительности между двумя, когда сущность должна быть загружена из базы данных.

SQL-запросы

Запросы также можно записывать непосредственно в SQL. Например:

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

Этот запрос приводит к следующему выполнению запроса:

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

Обратите внимание, что FromSql было введено в EF 9.0. В предыдущих версиях можно использовать вместо этого, хотя обратите внимание, FromSqlRaw что этот метод уязвим для атак внедрения sql.

Дополнительные сведения о запросах SQL см. в реляционной документации по запросам SQL. Большая часть этого содержимого также относится к поставщику Azure Cosmos DB.

Сопоставления функций

В этом разделе показано, какие методы и члены .NET преобразуются в функции SQL при запросе с помощью поставщика Azure Cosmos DB.

Функции даты и времени

.NET SQL Добавлено в
DateTime.UTCNow GetCurrentDateTime()
DateTimeOffset.UTCNow GetCurrentDateTime()
dateTime.Year1 DateTimePart("гггг", dateTime) EF Core 9.0
dateTimeOffset.Year1 DateTimePart("гггг", dateTimeOffset) EF Core 9.0
dateTime.AddYears(years)1 DateTimeAdd("гггг", dateTime) EF Core 9.0
dateTimeOffset.AddYears(years)1 DateTimeAdd("гггг", dateTimeOffset) EF Core 9.0

1 Другие члены компонента также переводятся (месяц, день...).

Числовые функции

.NET SQL Добавлено в
двойной. DegreesToRadians(x) RADIANS(@x) EF Core 8.0
двойной. RadiansToDegrees(x) DEGREES(@x) EF Core 8.0
ЭФ. 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)

Совет

Помимо методов, перечисленных здесь, также переводятся соответствующие универсальные математические реализации и методы MathF . Например, Math.Sin, MathF.Sinи double.Sinfloat.Sin все сопоставляют функцию SIN в SQL.

Строковые функции

.NET SQL Добавлено в
Regex.IsMatch(input, pattern) RegexMatch(@pattern, @input) EF Core 7.0
Regex.IsMatch(input, pattern, options) RegexMatch(@input, @pattern, @options) EF Core 7.0
струна. Concat(str0, str1) @str0 + @str1
струна. Equals(a, b, StringComparison.Ordinal) STRINGEQUALS(@a, @b)
струна. 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)

Прочие функции

.NET SQL Примечания.
коллекция. Contains(item) @item В @collection
ЭФ. Functions.CoalesceUndefined(x, y)1 x ?? г Добавлено в EF Core 9.0
ЭФ. Functions.IsDefined(x) IS_DEFINED(x) Добавлено в EF Core 9.0
ЭФ. Functions.VectorDistance(vector1, vector2)2 VectorDistance(vector1, vector2) Добавлено в EF Core 9.0, экспериментальный
ЭФ. Functions.VectorDistance(vector1, vector2, bruteForce)2 VectorDistance(vector1, vector2, bruteForce) Добавлено в EF Core 9.0, экспериментальный
ЭФ. Functions.VectorDistance(vector1, vector2, bruteForce, distanceFunction)2 VectorDistance(vector1, vector2, bruteForce, distanceFunction) Добавлено в EF Core 9.0, экспериментальный

1 Обратите внимание, что EF.Functions.CoalesceUndefined объединения undefined, а не null. Для объединения nullиспользуйте обычный оператор C# ?? .

2 См. документацию по использованию векторного поиска в Azure Cosmos DB, которая является экспериментальной. API-интерфейсы могут изменяться.