クエリ実行の基本
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();
Note
Azure Cosmos DB プロバイダーでは、他のプロバイダーと同じ LINQ クエリのセットは変換されません。
たとえば、EF Include() 演算子は Azure Cosmos DBではサポートされていません。ドキュメント間クエリがデータベースでサポートされていないためです。
パーティション キー
パーティション分割の利点は、関連するデータが見つかったパーティションに対してのみクエリが実行されるため、コストを節約でき、結果の速度が速くなることです。 パーティション キーを指定しないクエリはすべてのパーティションで実行されるため、コストがかなり高くなる可能性があります。
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("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();
EF によって生成されたログを調べると、このクエリが以下のように実行されます。
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"))
これらのログでは、以下のことがわかります。
- 最初の 2 つの比較 (
TenantIdとUserId) は削除され、ReadNext句ではなくWHERE"パーティション" に表示されます。つまり、クエリはそれらの値のサブパーティションでのみ実行されます。 -
SessionIdは階層パーティション キーの一部でもありますが、等値比較の代わりに、より大きい演算子 (>) が使用されるため、リフト アウトできません。これは、通常のプロパティと同様、WHERE句の一部です。 -
Usernameは通常のプロパティであり、パーティション キーの一部ではないため、WHERE句にも残ります。
一部のパーティション キー値が指定されていない場合でも、階層パーティション キーでは、最初の 2 つのプロパティに対応するサブパーティションのみを対象にすることができます。 これは、単一のパーティション (3 つのプロパティすべてで識別される) をターゲットにするほど効率的ではありませんが、すべてのパーティションをターゲットにするよりもはるかに効率的です。
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","00aa00aa-bb11-cc22-dd33-44ee44ee44ee",10.0]'
ReadItem に注目してください。これは、クエリが効率的なポイント読み取りとして実行されたことを示しています。SQL クエリは関係していません。
パーティション キーの抽出と同様、EF 9.0 ではこのメカニズムが大幅に改善されています。それより古いバージョンでは、ポイント読み取りを確実に検出して使用することはできません。
改ページ位置の自動修正
Note
この機能は EF Core 9.0 で導入されましたが、まだ試験段階です。 それがどのように機能するかについて、またフィードバックがあればお知らせください。
ページネーションとは、結果を一度にすべて取得するのではなく、ページ単位で取得することを指します。これは通常、大規模な結果セットに対して行われ、ユーザー インターフェイスが表示され、ユーザーが結果のページ間を移動できるようになります。
データベースで改ページ位置の自動修正を実装する一般的な方法は、Skip および Take LINQ 演算子 (SQL では OFFSET および LIMIT) を使用する方法です。 10 件の結果のページ サイズの場合、次のようにして 3 ページ目を 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 は、後続トークンとページ内の値 (最大 10 項目) を公開する CosmosPage<T> を返します。 プログラムは通常、後続トークンと共にこれらの値をクライアントに送信します。これにより、後でクエリを再開し、より多くの結果をフェッチできるようになります。
ユーザーが 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 になり、[次へ] ボタンがグレー表示になります。このページネーション方法は、Skip や Take を使用する場合に比べて、非常に効率がよく、コスト効率に優れています。
Azure Cosmos DB におけるページネーションについて詳しくは、 このページをご覧ください。
Note
Azure Cosmos DB では、後方ページネーションはサポートされておらず、ページまたは項目の合計数は提供されません。
ToPageAsync は、Azure Cosmos DB 固有ではない、より汎用的な EF ページネーション API に置き換えられる可能性があるため、現在、実験的として注釈が付けられています。 現在の 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 に渡す必要があります。
Note
FindAsync は、エンティティがコンテキストによって既に追跡されていて、データベースのラウンドトリップを回避する場合のみ使用します。
それ以外の場合は、そのまま SingleAsync を使用します。エンティティをデータベースから読み込む必要がある場合、2 つのパフォーマンスに違いはありません。
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 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 他のコンポーネント メンバーも変換されます (Month, Day...)。
Numeric 関数
| .NET | SQL |
|---|---|
| double.DegreesToRadians(x) | RADIANS(@x) |
| double.RadiansToDegrees(x) | DEGREES(@x) |
| 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.Sin、MathF.Sin、double.Sin および float.Sin はすべて SQL の SIN 関数にマップされます。
String 関数
| .NET | SQL | 追加されたバージョン: |
|---|---|---|
| Regex.IsMatch(input, pattern) | RegexMatch(@pattern, @input) | |
| Regex.IsMatch(input, pattern, options) | RegexMatch(@input, @pattern, @options) | |
| 糸。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 9 |
| stringValue.Contains(value, StringComparison.OrdinalIgnoreCase) (stringValueがvalueを含むかどうかを、大文字小文字を区別せずに比較して判定します) | 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(value, StringComparison.OrdinalIgnoreCase) | ENDSWITH(@stringValue, @value, true) | EF 9 |
| 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) 関数は、文字列内の 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) | 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 | 追加されたバージョン: |
|---|---|---|
| VectorDistance(vector1、vector2)。 | VectorDistance(vector1, vector2) | EF 9 |
| VectorDistance(vector1、vector2、bruteForce) | VectorDistance(vector1, vector2, bruteForce) | EF 9 |
| VectorDistance(vector1、vector2、bruteForce、distanceFunction) | VectorDistance(vector1, vector2, bruteForce, distanceFunction) | EF 9 |
| FullTextContains(プロパティ, キーワード) | FullTextContains(property, keyword) | EF 10 |
| FullTextContainsAll(property, keyword1, keyword2) | FullTextContainsAll(property, keyword1, keyword2) | EF 10 |
| FullTextContainsAny(property, keyword1, keyword2) | FullTextContainsAny(property, keyword1, keyword2) | EF 10 |
| FullTextScore(プロパティ, keyword1, keyword2) | FullTextScore(property, keyword1, keyword2) | EF 10 |
| Rrf(search1、search2) | RRF(プロパティ, search1, search2). | EF 10 |
| Rrf(新しい配列 { search1, search2 }、重み) | RRF(プロパティ, search1, search2, weights) | EF 10 |
ベクター検索の詳細については、 ドキュメントを参照してください。 フルテキスト検索の詳細については、 ドキュメントを参照してください。
その他の関数
| .NET | SQL | 追加されたバージョン: |
|---|---|---|
| collection.Contains(item) | @item で @collection | |
| CoalesceUndefined(x,y)1 | x ?? y | EF 9 |
| IsDefined(x) | IS_DEFINED(x) | EF 9 |
1CoalesceUndefined は undefined ではなく null と合体することに注意してください。
null を合体するには、標準 C# ?? 演算子を使用します。
.NET