EF Core Azure Cosmos DB プロバイダーEF Core Azure Cosmos DB Provider

注意

このプロバイダーは、EF Core 3.0 で新しく追加されたものです。This provider is new in EF Core 3.0.

このデータベース プロバイダーにより、Azure Cosmos DB と共に Entity Framework Core を使用できます。This database provider allows Entity Framework Core to be used with Azure Cosmos DB. このプロバイダーは、Entity Framework Core プロジェクトの一部として保守管理されています。The provider is maintained as part of the Entity Framework Core Project.

このセクションを読む前に、Azure Cosmos DB のドキュメントを理解することを強くお勧めします。It is strongly recommended to familiarize yourself with the Azure Cosmos DB documentation before reading this section.

注意

このプロバイダーは、Azure Cosmos DB の SQL API でのみ機能します。This provider only works with the SQL API of Azure Cosmos DB.

インストールInstall

Microsoft.EntityFrameworkCore.Cosmos NuGet パッケージをインストールします。Install the Microsoft.EntityFrameworkCore.Cosmos NuGet package.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

はじめにGet started

ヒント

この記事のサンプルは GitHub で確認できます。You can view this article's sample on GitHub.

他のプロバイダーと同様に、最初の手順は UseCosmos を呼び出すことです。As for other providers the first step is to call UseCosmos:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
            "https://localhost:8081",
            "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
            databaseName: "OrdersDB");

警告

ここでは、わかりやすくするためにエンドポイントとキーをハードコードしていますが、運用アプリでは、これらは安全に格納する必要があります。The endpoint and key are hardcoded here for simplicity, but in a production app these should be stored securely.

この例では、Order は、所有型 StreetAddress への参照を持つ単純なエンティティです。In this example Order is a simple entity with a reference to the owned type StreetAddress.

public class Order
{
    public int Id { get; set; }
    public int? TrackingNumber { get; set; }
    public string PartitionKey { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}

データの保存とクエリは、通常の EF のパターンに従います。Saving and querying data follows the normal EF pattern:

using (var context = new OrderContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.Add(new Order
    {
        Id = 1,
        ShippingAddress = new StreetAddress { City = "London", Street = "221 B Baker St" },
        PartitionKey = "1"
    });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.FirstAsync();
    Console.WriteLine($"First order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

重要

必須のコンテナーを作成し、モデル内にシード データが存在する場合は挿入するようにするためには、EnsureCreatedAsync を呼び出す必要があります。Calling EnsureCreatedAsync is necessary to create the required containers and insert the seed data if present in the model. ただし、EnsureCreatedAsync は、パフォーマンスの問題を引き起こす可能性があるため、通常の操作ではなく、配置時にのみ呼び出す必要があります。However EnsureCreatedAsync should only be called during deployment, not normal operation, as it may cause performance issues.

Cosmos 固有のモデルのカスタマイズCosmos-specific model customization

既定では、すべてのエンティティ型は、(この場合は "OrderContext" の) 派生コンテキストに基づいて命名された同じコンテナーにマップされます。By default all entity types are mapped to the same container, named after the derived context ("OrderContext" in this case). 既定のコンテナー名を変更するには、HasDefaultContainer を使います。To change the default container name use HasDefaultContainer:

modelBuilder.HasDefaultContainer("Store");

エンティティ型を別のコンテナーにマップするには、ToContainer を使います。To map an entity type to a different container use ToContainer:

modelBuilder.Entity<Order>()
    .ToContainer("Orders");

EF Core では、派生エンティティ型がない場合でも、特定の項目が表すエンティティ型の識別に識別子の値が追加されます。To identify the entity type that a given item represent EF Core adds a discriminator value even if there are no derived entity types. 識別子の名前と値は変更できますThe name and value of the discriminator can be changed.

他のエンティティ型が同じコンテナーに格納されることがない場合は、HasNoDiscriminator を呼び出すことで、その識別子を削除できます。If no other entity type will ever be stored in the same container the discriminator can be removed by calling HasNoDiscriminator:

modelBuilder.Entity<Order>()
    .HasNoDiscriminator();

パーティション キーPartition keys

既定では、EF Core ではパーティション キーが "__partitionKey" に設定されたコンテナーが作成されます。項目を挿入する際に、それに対して値が指定されることはありません。By default EF Core will create containers with the partition key set to "__partitionKey" without supplying any value for it when inserting items. しかし、Azure Cosmos のパフォーマンス機能を十分に活用するには、慎重に選んだパーティション キーを使用する必要があります。But to fully leverage the performance capabilities of Azure Cosmos a carefully selected partition key should be used. これを構成するには、HasPartitionKey を呼び出します。It can be configured by calling HasPartitionKey:

modelBuilder.Entity<Order>()
    .HasPartitionKey(o => o.PartitionKey);

注意

パーティション キーのプロパティは、それが文字列に変換される限り、任意の型にすることができます。The partition key property can be of any type as long as it is converted to string.

一度構成したら、パーティション キーのプロパティは常に null 以外の値を持つ必要があります。Once configured the partition key property should always have a non-null value. クエリを発行するときに、条件を追加して単一パーティションにすることができます。When issuing a query a condition can be added to make it single-partition.

using (var context = new OrderContext())
{
    context.Add(new Order
    {
        Id = 2,
        ShippingAddress = new StreetAddress { City = "New York", Street = "11 Wall Street" },
        PartitionKey = "2"
    });

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var order = await context.Orders.Where(p => p.PartitionKey == "2").LastAsync();
    Console.Write("Last order will ship to: ");
    Console.WriteLine($"{order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

埋め込みエンティティEmbedded entities

Cosmos の場合、所有エンティティは所有者と同じアイテムに埋め込まれます。For Cosmos, owned entities are embedded in the same item as the owner. プロパティ名を変更するには、ToJsonProperty を使います。To change a property name use 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");
    });

この構成では、上記の例の順序は次のように格納されます。With this configuration the order from the example above is stored like this:

{
    "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
}

所有されているエンティティのコレクションも埋め込まれます。Collections of owned entities are embedded as well. 次の例では、Distributor クラスを StreetAddress コレクションと共に使用します。For the next example we'll use the Distributor class with a collection of StreetAddress:

public class Distributor
{
    public int Id { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

所有されているエンティティは、明示的なキー値を格納する必要はありません。The owned entities don't need to provide explicit key values to be stored:

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

これは、次のように永続化されます。They will be persisted in this way:

{
    "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 には、追跡対象のすべてのエンティティに対して、内部で常に一意のキー値が必要です。Internally EF Core always needs to have unique key values for all tracked entities. 所有されている型のコレクションに対して既定で作成される主キーは、所有者を指す外部キー プロパティと、JSON 配列内のインデックスに対応する int プロパティで構成されます。The primary key created by default for collections of owned types consists of the foreign key properties pointing to the owner and an int property corresponding to the index in the JSON array. これらの値を取得するには、エントリ API を使用します。To retrieve these values entry API could be used:

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

ヒント

必要に応じて、所有されているエンティティ型の既定の主キーは変更できますが、その場合、キー値は明示的に指定する必要があります。When necessary the default primary key for the owned entity types can be changed, but then key values should be provided explicitly.

接続解除エンティティの使用Working with disconnected entities

すべてのアイテムには、特定のパーティション キーに対して一意な id 値が必要です。Every item needs to have an id value that is unique for the given partition key. 既定では、EF Core は、' | ' を区切り記号として使用して、識別子と主キーの値を連結して値を生成します。By default EF Core generates the value by concatenating the discriminator and the primary key values, using '|' as a delimiter. このキー値は、エンティティが Added 状態になったときにのみ生成されます。The key values are only generated when an entity enters the Added state. これは、.NET 型にその値を保存する id プロパティがない場合にエンティティをアタッチするときに問題になる場合があります。This might pose a problem when attaching entities if they don't have an id property on the .NET type to store the value.

この制限を回避するには、id 値を手動で作成して設定するか、エンティティをまず追加済みとしてマークして、その後目的の状態に変更します。To work around this limitation one could create and set the id value manually or mark the entity as added first, then changing it to the desired state:

using (var context = new OrderContext())
{
    var distributorEntry = context.Add(distributor);
    distributorEntry.State = EntityState.Unchanged;

    distributor.ShippingCenters.Remove(distributor.ShippingCenters.Last());

    await context.SaveChangesAsync();
}

using (var context = new OrderContext())
{
    var firstDistributor = await context.Distributors.FirstAsync();
    Console.WriteLine($"Number of shipping centers is now: {firstDistributor.ShippingCenters.Count}");

    var distributorEntry = context.Entry(firstDistributor);
    var idProperty = distributorEntry.Property<string>("id");
    Console.WriteLine($"The distributor 'id' is: {idProperty.CurrentValue}");
}

結果の JSON は次のようになります。This is the resulting JSON:

{
    "Id": 1,
    "Discriminator": "Distributor",
    "id": "Distributor|1",
    "ShippingCenters": [
        {
            "City": "Phoenix",
            "Street": "500 S 48th Street"
        }
    ],
    "_rid": "JBwtAN8oNYEBAAAAAAAAAA==",
    "_self": "dbs/JBwtAA==/colls/JBwtAN8oNYE=/docs/JBwtAN8oNYEBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9377-d7a1ae7c01d5\"",
    "_attachments": "attachments/",
    "_ts": 1572917100
}