共用方式為


使用 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 查詢組合與其他提供者不同。 例如,Azure Cosmos DB 不支援 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「分割區」中,而不是出現在 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();

這麼做的執行方式會與上述查詢相同,且如果您想讓查詢中的分割區索引鍵更明確,這種作法更應優先。 9.0 之前的 EF 版本可能需要使用 WithPartitionKey;請留意記錄,以便確保查詢如預期使用分割區索引鍵。

點讀取

雖然 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 中引進,但仍為實驗性。 請不吝向我們指教它的運作成效,以及您的任何意見。

分頁是指擷取頁面的結果,而不是一次擷取全部結果;這種作法通常針對大量結果集執行,在顯示的使用者介面中讓使用者瀏覽結果頁面。

使用資料庫實作分頁的常見方式是使用 SkipTake LINQ 運算子 (SQL 的 OFFSETLIMIT)。 假設頁面大小為 10 個結果,您可以使用 EF Core 擷取第三頁,如下所示:

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

可惜的是,這項技術效率相當低,而且可能會大幅增加查詢成本。 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
}

與其使用 ToListAsync 或類似選向來終止 LINQ 查詢,我們會使用 ToPageAsync 方法指示它在每個頁面最多取得 10 個項目 (請注意,資料庫的項目可能更少)。 由於這是第一次查詢,我們想要從開頭取得結果,並將 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
}

我們會執行相同的查詢,但這次我們會傳入從第一次執行收到的接續令牌;這會指示查詢引擎繼續其離開位置的查詢,並擷取接下來 10 個專案。 只要我們擷取到最後一頁且沒有任何結果,接續權杖就會是 null,而 [下一頁] 按鈕可能會呈現灰色。比起使用 SkipTake,這種分頁方法非常有效率且符合成本效益。

若要深入瞭解 Azure Cosmos DB 中的分頁, 請參閱此頁面

注意

Azure Cosmos DB 不支援回溯分頁,而且不提供總頁面或專案的計數。

ToPageAsync 目前已標註為實驗性,因為它可能會取代為較泛型的 EF 分頁 API,但不是 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 提供者相關。

函式對應

本節說明使用 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 其他元件成員也會經過轉譯 (Month、Day...)。

數值函數

.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 中使用向量搜尋的相關信息,請參閱檔 ,這是實驗性的。 API 可能會變更。