EF Core Azure Cosmos DB プロバイダーを使用したモデルの構成
コンテナーとエンティティの種類
Azure Cosmos DB では、JSON ドキュメントはコンテナーに保存されます。 リレーショナル データベースのテーブルとは異なり、Azure Cosmos DB コンテナーにはさまざまな図形を持つドキュメントを含めることができます。コンテナーでは、均一なスキーマはドキュメントに適用されません。 ただし、さまざまな構成オプションがコンテナー レベルで定義されているため、その中に含まれるすべてのドキュメントに影響を与えます。 詳しくは、 コンテナー に関する Azure Cosmos DB のドキュメント をご覧ください。
既定では、EF はすべてのエンティティ型を同じコンテナーにマップします。これは通常、パフォーマンスと価格の面で適切な既定値です。 既定のコンテナーには、.NET コンテキスト型 (この場合は OrderContext
) に従って名前が付けられます。 既定のコンテナー名を変更するには、次のように HasDefaultContainer を使用します。
modelBuilder.HasDefaultContainer("Store");
エンティティ型を別のコンテナーにマップするには、次のように ToContainer を使用します。
modelBuilder.Entity<Order>().ToContainer("Orders");
エンティティ型を異なるコンテナーにマッピングする前に、潜在的なパフォーマンスと価格への影響 (専用スループットと共有スループットなど) を理解していることを確認してください。 詳しくは、Azure Cosmos DB のドキュメントをご覧ください。
ID とキー
Azure Cosmos DB では、一意に識別される id
JSON プロパティがすべてのドキュメントに必要です。 他の EF プロバイダーと同様、EF Azure Cosmos DB プロバイダーは Id
または <type name>Id
という名前のプロパティを見つけ、そのプロパティをエンティティ型のキーとして構成して、 id
JSON プロパティにマッピングします。 HasKey を使用することにより、任意のプロパティをキー プロパティとして構成できます。詳しくは、キーに関する一般的な EF ドキュメントをご覧ください。
他のデータベースから Azure Cosmos DB にアクセスする開発者は、キー (Id
) プロパティが自動的に生成されることを期待することがあります。 たとえば、SQL Server では、EF は数値キー プロパティを IDENTITY 列に対して構成します。この列では、自動インクリメント値がデータベースに生成されます。 これに対し、Azure Cosmos DB ではプロパティの自動生成がサポートされていないため、キー プロパティを明示的に設定する必要があります。 未設定のキー プロパティを持つエンティティ型を挿入すると、そのプロパティの CLR 既定値 (たとえば、int
の場合は 0) が挿入されて、2 回目の挿入は失敗します。これを行おうとすると、EF によって警告が発行されます。
キー プロパティとして GUID を使用する場合、クライアントで一意のランダムな値を生成するよう EF を構成できます。
modelBuilder.Entity<Session>().Property(b => b.Id).HasValueGenerator<GuidValueGenerator>();
パーティション キー
Azure Cosmos DB では、パーティション分割を使用して水平方向のスケーリングが実現されます。適切なモデリングとパーティション キーを慎重に選択することは、優れたパフォーマンスを実現し、コストを抑える上で不可欠です。 パーティション分割に関する Azure Cosmos DB のドキュメント を読み、事前にパーティション分割戦略を計画することを強くお勧めします。
EF を使用してパーティション キーを構成するには、 HasPartitionKeyを呼び出し、エンティティ型で通常のプロパティを渡します。
modelBuilder.Entity<Order>().HasPartitionKey(o => o.PartitionKey);
文字列に変換される限り、任意のプロパティをパーティション キーにすることができます。 構成後、パーティション キー プロパティには必ず null 以外の値が必要です。未設定のパーティション キー プロパティを使って新しいエンティティ型を挿入しようとすると、エラーが発生します。
Azure Cosmos DB では、同じ id
プロパティを持つ 2 つのドキュメントが異なるパーティション内にある限り、コンテナー内に存在することができます。つまり、コンテナー内のドキュメントを一意に識別するには、 id
とパーティション キーのプロパティの両方を指定する必要があります。 このため、EF のエンティティ主キーの内部概念には、パーティション キーの概念がないリレーショナル データベースなどとは異なり、規則に従ってこれらの要素の両方が含まれています。 つまり、たとえば FindAsync
にはキーとパーティション キーの両方のプロパティが必要であり (詳細はドキュメントを参照)、効率的でコスト効率の高い point reads
のメリットを得るには、クエリで Where
句にこれらを指定する必要があります。
パーティション キーは、コンテナー レベルで定義されている点に注意してください。 これは特に、同じコンテナー内の複数のエンティティ タイプが異なるパーティション キー プロパティを持つことができないことを意味します。 異なるパーティション キーを定義する必要がある場合、関連するエンティティ型を異なるコンテナーにマップします。
階層パーティション キー
Azure Cosmos DB は、データ分散をさらに最適化するための 階層型 パーティション キーもサポートしています。 詳細についてはドキュメントを参照してください。 EF 9.0 では、階層パーティション キーのサポートが追加されました。これらを構成するには、最大 3 つのプロパティを HasPartitionKey に渡します。
modelBuilder.Entity<Order>().HasPartitionKey(o => new { e.TenantId, e.UserId, e.SessionId });
このような階層パーティション キーを使用すると、サブパーティションの関連するサブセットにのみクエリを簡単に送信することができます。 たとえば、特定のテナントの注文に対してクエリを実行した場合、それらのクエリは、そのテナントのサブパーティションに対してのみ実行されます。
EF でパーティション キーを構成しない場合、起動時に警告がログに記録されます。EF Core は、パーティション キーが __partitionKey
に設定されたコンテナーを作成し、項目の挿入時に値を指定しません。 パーティション キーが設定されていない場合、コンテナーは 20 GB のデータに制限されます。これは、1 つの 論理パーティションの最大ストレージです。 これは小規模な開発/テスト アプリケーションでは機能しますが、適切に構成されたパーティション キー戦略を使用せずに運用アプリケーションをデプロイすることは非常に推奨されません。
パーティション キーのプロパティが適切に構成されると、クエリでそれらの値を指定することができます。詳しくは、「パーティション キーを使用したクエリ」をご覧ください。
ディスクリミネーター
複数のエンティティ型が同じコンテナーにマップされる可能性があるため、EF Core は保存するすべての JSON ドキュメントに常に $type
ディスクリミネーター プロパティを追加します (このプロパティは EF 9.0 より前では Discriminator
と呼ばれていました)。これにより、EF はデータベースから読み込まれるドキュメントを認識し、適切な .NET 型を具体化できます。 リレーショナル データベースの経験がある開発者は、 Table-Per-Hierarchy 継承 (TPH)のコンテキストにおけるディスクリミネーターに精通している可能性があります。Azure Cosmos DBでは、ディスクリミネーターは継承マッピングのシナリオだけでなく、同じコンテナーにまったく異なるドキュメントの種類を含めることができるため、使用されます。
ディスクリミネーター プロパティの名前と値は、標準の EF API を使用して構成できます。詳しくは、次のドキュメントをご覧ください。 単一のエンティティ型をコンテナーにマッピングしていて、別のエンティティ型をマッピングしないことが確実であり、ディスクリミネーター プロパティを削除する場合は、HasNoDiscriminator を呼び出します。
modelBuilder.Entity<Order>().HasNoDiscriminator();
同じコンテナーに異なるエンティティ タイプを含めることができ、JSON id
プロパティはコンテナー パーティション内で一意である必要があるため、同じコンテナー パーティション内の異なるタイプのエンティティに同じ id
値を持つことはできません。 これをリレーショナル データベースと比較します。ここでは、各エンティティ型が異なるテーブルにマップされるため、独自の個別のキー領域が存在します。 そのため、コンテナーに挿入するドキュメントの id
一意性をユーザーの責任で確保する必要があります。 同じ主キー値を持つ異なるエンティティ型が必要な場合、次のように、ディスクリミネーターを id
プロパティに自動的に挿入するよう EF に指示できます。
modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();
これにより、id
値の操作が容易になる可能性がありますが、ドキュメントを操作する外部アプリケーションとの相互運用が難しくなる可能性があります。これは、EF の連結された id
形式と、既定では .NET 型から派生するディスクリミネーターの値を認識する必要があるためです。 これは、EF 9.0 より前の既定の動作である点に注意してください。
追加のオプションとして、階層のルート エンティティ型のディスクリミネーターであるルート ディスクリミネーターのみを id
プロパティに挿入するよう EF に指示することもできます。
modelBuilder.Entity<Session>().HasRootDiscriminatorInJsonId();
これは似ていますが、EF がより多くのシナリオで効率的なポイント読み取りを使用できるようになります。 id
プロパティにディスクリミネーターを挿入する必要がある場合、パフォーマンスを向上させるためにルート ディスクリミネーターを挿入することを検討してください。
プロビジョニング スループット
EF Core を使用して Azure Cosmos DB データベースまたはコンテナーを作成する場合は、CosmosModelBuilderExtensions.HasAutoscaleThroughput または CosmosModelBuilderExtensions.HasManualThroughput を呼び出して、データベース用にプロビジョニングされたスループットを構成できます。 次に例を示します。
modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);
コンテナー用にプロビジョニングされたスループットを構成するには、CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput または CosmosEntityTypeBuilderExtensions.HasManualThroughput を呼び出します。 次に例を示します。
modelBuilder.Entity<Family>(
entityTypeBuilder =>
{
entityTypeBuilder.HasManualThroughput(5000);
entityTypeBuilder.HasAutoscaleThroughput(3000);
});
Time-To-Live
Azure Cosmos DB モデルのエンティティ型は、既定の Time-To-Live で構成できます。 次に例を示します。
modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);
または、分析ストアの場合:
modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);
個々のエンティティの Time-to-live は、JSON ドキュメントの "ttl" にマップされたプロパティを使用して設定できます。 次に例を示します。
modelBuilder.Entity<Village>()
.HasDefaultTimeToLive(3600)
.Property(e => e.TimeToLive)
.ToJsonProperty("ttl");
Note
"ttl" を有効にするには、エンティティ型に対して既定の time-to-live を構成する必要があります。 詳細については、「Azure Cosmos DB の Time to Live (TTL)」 を参照してください。
その後、エンティティが保存される前に time-to-live プロパティが設定されます。 次に例を示します。
var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();
time-to-live プロパティは、データベースに関する問題によってドメイン エンティティが汚染されるのを避けるためにシャドウ プロパティにすることができます。 次に例を示します。
modelBuilder.Entity<Hamlet>()
.HasDefaultTimeToLive(3600)
.Property<int>("TimeToLive")
.ToJsonProperty("ttl");
次に、追跡対象エンティティにアクセスして、シャドウの time-to-live プロパティを設定します。 次に例を示します。
var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();
埋め込みエンティティ
Note
関連するエンティティ型が既定で所有されるように構成されています。 特定のエンティティ型に対してこれを防ぐには、ModelBuilder.Entity を呼び出します。
Azure Cosmos DB の場合、所有エンティティは所有者と同じアイテムに埋め込まれます。 プロパティ名を変更するには、ToJsonProperty を使います。
modelBuilder.Entity<Order>().OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.ToJsonProperty("Address");
sa.Property(p => p.Street).ToJsonProperty("ShipsToStreet");
sa.Property(p => p.City).ToJsonProperty("ShipsToCity");
});
この構成では、上記の例の順序は次のように格納されます。
{
"Id": 1,
"PartitionKey": "1",
"TrackingNumber": null,
"id": "1",
"Address": {
"ShipsToCity": "London",
"ShipsToStreet": "221 B Baker St"
},
"_rid": "6QEKAM+BOOABAAAAAAAAAA==",
"_self": "dbs/6QEKAA==/colls/6QEKAM+BOOA=/docs/6QEKAM+BOOABAAAAAAAAAA==/",
"_etag": "\"00000000-0000-0000-683c-692e763901d5\"",
"_attachments": "attachments/",
"_ts": 1568163674
}
所有されているエンティティのコレクションも埋め込まれます。 次の例では、Distributor
クラスを StreetAddress
コレクションと共に使用します。
public class Distributor
{
public int Id { get; set; }
public string ETag { get; set; }
public ICollection<StreetAddress> ShippingCenters { get; set; }
}
所有されているエンティティは、明示的なキー値を格納する必要はありません。
var distributor = new Distributor
{
Id = 1,
ShippingCenters = new HashSet<StreetAddress>
{
new StreetAddress { City = "Phoenix", Street = "500 S 48th Street" },
new StreetAddress { City = "Anaheim", Street = "5650 Dolly Ave" }
}
};
using (var context = new OrderContext())
{
context.Add(distributor);
await context.SaveChangesAsync();
}
これは、次のように永続化されます。
{
"Id": 1,
"Discriminator": "Distributor",
"id": "Distributor|1",
"ShippingCenters": [
{
"City": "Phoenix",
"Street": "500 S 48th Street"
},
{
"City": "Anaheim",
"Street": "5650 Dolly Ave"
}
],
"_rid": "6QEKANzISj0BAAAAAAAAAA==",
"_self": "dbs/6QEKAA==/colls/6QEKANzISj0=/docs/6QEKANzISj0BAAAAAAAAAA==/",
"_etag": "\"00000000-0000-0000-683c-7b2b439701d5\"",
"_attachments": "attachments/",
"_ts": 1568163705
}
EF Core には、追跡対象のすべてのエンティティに対して、内部で常に一意のキー値が必要です。 所有されている型のコレクションに対して既定で作成される主キーは、所有者を指す外部キー プロパティと、JSON 配列内のインデックスに対応する int
プロパティで構成されます。 これらの値を取得するには、エントリ API を使用します。
using (var context = new OrderContext())
{
var firstDistributor = await context.Distributors.FirstAsync();
Console.WriteLine($"Number of shipping centers: {firstDistributor.ShippingCenters.Count}");
var addressEntry = context.Entry(firstDistributor.ShippingCenters.First());
var addressPKProperties = addressEntry.Metadata.FindPrimaryKey().Properties;
Console.WriteLine(
$"First shipping center PK: ({addressEntry.Property(addressPKProperties[0].Name).CurrentValue}, {addressEntry.Property(addressPKProperties[1].Name).CurrentValue})");
Console.WriteLine();
}
ヒント
必要に応じて、所有されているエンティティ型の既定の主キーは変更できますが、その場合、キー値は明示的に指定する必要があります。
プリミティブ型のコレクション
string
や int
など、サポートされているプリミティブ型のコレクションは自動的に検出され、マップされます。 IReadOnlyList<T> または IReadOnlyDictionary<TKey,TValue> を実装するすべての型のコレクションがサポートされています。 たとえば、次のようなエンティティ型について考えます。
public class Book
{
public Guid Id { get; set; }
public string Title { get; set; }
public IList<string> Quotes { get; set; }
public IDictionary<string, string> Notes { get; set; }
}
IList
と IDictionary
に入力し、データベースに対して永続化することができます。
using var context = new BooksContext();
var book = new Book
{
Title = "How It Works: Incredible History",
Quotes = new List<string>
{
"Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
"Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
"For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
},
Notes = new Dictionary<string, string>
{
{ "121", "Fridges" },
{ "144", "Peter Higgs" },
{ "48", "Saint Mark's Basilica" },
{ "36", "The Terracotta Army" }
}
};
context.Add(book);
await context.SaveChangesAsync();
その結果、次の JSON ドキュメントが生成されます。
{
"Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
"Discriminator": "Book",
"Notes": {
"36": "The Terracotta Army",
"48": "Saint Mark's Basilica",
"121": "Fridges",
"144": "Peter Higgs"
},
"Quotes": [
"Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
"Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
"For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
],
"Title": "How It Works: Incredible History",
"id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
"_rid": "t-E3AIxaencBAAAAAAAAAA==",
"_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
"_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
"_attachments": "attachments/",
"_ts": 1630075016
}
その後、これらのコレクションを、ここでも通常の方法で更新できます。
book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";
await context.SaveChangesAsync();
制限事項:
- 文字列キーを持つディクショナリだけがサポートされます。
- EF Core 9.0 では、プリミティブ コレクションへのクエリのサポートが追加されました。
eTag を使用したオプティミスティック同時実行制御
オプティミスティック同時実行制御を使用するようにエンティティ型を構成するには、UseETagConcurrency を呼び出します。 この呼び出しによって、シャドウ状態の _etag
プロパティが作成され、同時実行制御トークンとして設定されます。
modelBuilder.Entity<Order>()
.UseETagConcurrency();
同時実行制御エラーを簡単に解決できるようにするには、IsETagConcurrency を使用して、eTag を CLR プロパティにマップします。
modelBuilder.Entity<Distributor>()
.Property(d => d.ETag)
.IsETagConcurrency();
.NET