使用 EF Core Azure Cosmos DB Provider 进行查询

查询基础知识

可以用与其他数据库提供程序相同的方式对 Azure Cosmos DB 执行 EF Core LINQ 查询。 例如:

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 查询集与其他提供程序转换的不同。 例如,Cosmos 不支持 EF Include() 运算符,因为数据库中不支持跨文档查询。

分区键

分区的优点是只对找到相关数据的分区执行查询,从而节省成本并确保更快的结果速度。 不指定分区键的查询在所有分区上执行,这可能会非常昂贵。

从 EF 9.0 开始,EF 会自动检测并提取 LINQ 查询的 Where 运算符中的分区键比较。 假设我们针对 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"))

在这些日志中,我们注意到以下内容:

  • 前两个比较(关于TenantIdUserId)已被取消,并出现在 ReadNext “Partition” 中,而不是 WHERE 子句中;这意味着查询将仅在这些值的子分区上执行。
  • SessionId 也是分层分区键的一部分,但它使用大于运算符 (>) 而不是相等比较,因此无法取消。它是 WHERE 子句的一部分,就像任何常规属性一样。
  • Username 是常规属性,不是分区键的一部分,因此也保留在 WHERE 子句中。

请注意,即使未提供某些分区键值,分层分区键仍允许仅针对对应于前两个属性的子分区。 虽然这不如针对单个分区(由所有三个属性标识)有效,但它仍然比针对所有分区更有效。

可以使用 WithPartitionKey 运算符显式指定分区键属性,而不是引用 Where 运算符中的分区键属性:

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

这与上述查询执行的方式相同,如果想在查询中使分区键更显式,这可能是更好的选择。 在 9.0 之前的 EF 版本中可能需要使用 WithPartitionKey;请留意日志,以确保查询按预期使用分区键。

点读取

虽然 Azure Cosmos DB 允许通过 SQL 进行强大的查询,但此类查询可能相当昂贵。 Cosmos DB 还支持点读取,当 id 属性和整个分区键都已知时,可以使用点读取。 此类点读取直接识别特定分区中的特定文档,并以极低的成本高效执行。 如果可能的话,有必要以尽可能多地利用点读取的方式设计系统。 若要了解详细信息,请参阅 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 属性的值(映射到 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 中引入的,目前仍处于试验阶段。 如果你有任何反馈,请告诉我们它是如何为你工作的。

分页是指在页面中检索结果,而不是一次检索所有结果;这通常适用于大型结果集,其中显示了一个用户界面,允许用户浏览结果页面。

使用数据库实现分页的一种常见方法是使用 SkipTake LINQ 运算符(在 SQL 中为 OFFSETLIMIT)。 给定页面大小为 10 个结果,可以使用 EF Core 提取第三页,如下所示:

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

遗憾的是,此方法效率相当低,而且会大大增加查询成本。 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
}

我们使用 ToPageAsync 方法,指示它在每个页面中最多获取 10 个项目,而不是用 ToListAsync 或类似方法终止 LINQ 查询(请注意,数据库中的项目可能更少)。 由于这是我们的第一个查询,因此我们希望从头开始获取结果,并将 null 作为延续令牌传递。 ToPageAsync 返回 CosmosPage,它公开延续令牌和页面中的值(最多 10 个项目)。 程序通常会将这些值与延续令牌一起发送到客户端;这将允许稍后恢复查询并获取更多结果。

假设用户现在单击其 UI 中的“下一步”按钮,询问接下来的 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
}

我们执行相同的查询,但这次我们传入从第一次执行收到的延续令牌;这指示 Cosmos DB 继续其离开位置的查询,并获取接下来的 10 个项目。 一旦我们获取了最后一页并且没有更多结果,延续令牌将为 null,“下一步”按钮可能会灰显。与使用 SkipTake 相比,这种分页方法非常高效且经济高效。

若要了解有关 Cosmos DB 中的分页的详细信息,请参阅此页面

注意

Cosmos DB 不支持向后分页,也不提供总页数或项目数。

ToPageAsync 目前被标注为试验性,因为它可能被更通用的非 Cosmos 特定的 EF 分页 API 所取代。 尽管使用当前 API 将生成编译警告 (EF9102),但这样做应该是安全的,将来的更改可能需要在 API 形状中进行细微调整。

FindAsync

FindAsync 是一个有用的 API,用于按其主键获取实体,并在实体已加载并被上下文跟踪时避免数据库往返。

熟悉关系数据库的开发人员习惯于由 Id 属性组成的实体类型的主键。 使用 EF Cosmos DB 提供程序时,主键除了映射到 JSON id 属性的属性外,还包含分区键属性;这是因为 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

请注意,EF 9.0 中引入了 FromSql。 在以前的版本中,可以使用 FromSqlRaw;但请注意,该方法容易受到 SQL 注入攻击的攻击。

有关 SQL 查询的详细信息,请参阅有关 SQL 查询的关系文档;其中大部分内容也与 Cosmos 提供程序相关。

函数映射

本节介绍在使用 Azure Cosmos DB 提供程序进行查询时,哪些 .NET 方法和成员被转换为哪些 SQL 函数。

日期和时间函数

.NET SQL
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 其他组件成员也已转换(月、日...)。

数值函数

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

提示

除了此处列出的方法外,还将转换相应的泛型数学实现和 MathF 方法。 例如,Math.SinMathF.Sindouble.Sinfloat.Sin 全部映射到 SQL 中的 SIN 函数。

字符串函数

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

其他函数

.NET SQL 备注
collection.Contains(item) @item IN @collection
EF.Functions.CoalesceUndefined(x, y)1 x ?? y 已在 EF Core 9.0 中添加
EF.Functions.IsDefined(x) IS_DEFINED(x) 已在 EF Core 9.0 中添加
EF.Functions.VectorDistance(vector1, vector2)2 VectorDistance(vector1, vector2) 在 EF Core 9.0 中添加,试验性
EF.Functions.VectorDistance(vector1, vector2, bruteForce)2 VectorDistance(vector1, vector2, bruteForce) 在 EF Core 9.0 中添加,试验性
EF.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 中使用矢量搜索的信息,请参阅文档。 Cosmos DB 矢量搜索处于试验阶段,API 可能会更改。