EF Core Azure Cosmos DB 공급자

이 데이터베이스 공급자를 설치하면 Entity Framework Core를 Azure Cosmos DB에서 사용할 수 있습니다. 공급자는 Entity Framework Core 프로젝트의 일부로 유지 관리됩니다.

이 섹션을 읽기 전에 Azure Cosmos DB 설명서를 숙지하는 것이 좋습니다.

참고

이 공급자는 NoSQL용 Azure Cosmos DB에서만 작동합니다.

설치

Microsoft.EntityFrameworkCore.Cosmos NuGet 패키지를 설치합니다.

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

시작

GitHub에서 이 문서의 샘플을 볼 수 있습니다.

다른 공급자와 마찬가지로, 첫 번째 단계는 UseCosmos를 호출하는 것입니다.

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

경고

여기서는 간단하게 엔드포인트 및 키를 하드 코딩했지만 프로덕션 앱에서는 이들을 안전하게 저장해야 합니다.

이 예제에서 Order소유된 형식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 패턴을 따릅니다.

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를 호출해야 합니다. 그러나 성능 문제가 발생할 수 있으므로 EnsureCreatedAsync는 정상 작업이 아닌 배포 중에만 호출해야 합니다.

Azure Cosmos DB 옵션

또한 단일 연결 문자열을 사용하여 Azure Cosmos DB 공급자를 구성하고 연결을 사용자 지정하는 다른 옵션을 지정할 수 있습니다.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.UseCosmos(
        "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        databaseName: "OptionsDB",
        options =>
        {
            options.ConnectionMode(ConnectionMode.Gateway);
            options.WebProxy(new WebProxy());
            options.LimitToEndpoint();
            options.Region(Regions.AustraliaCentral);
            options.GatewayModeMaxConnectionLimit(32);
            options.MaxRequestsPerTcpConnection(8);
            options.MaxTcpConnectionsPerEndpoint(16);
            options.IdleTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.OpenTcpConnectionTimeout(TimeSpan.FromMinutes(1));
            options.RequestTimeout(TimeSpan.FromMinutes(1));
        });

위에서 언급한 각 옵션의 영향에 대한 자세한 내용은 Azure Cosmos DB 옵션 설명서를 참조하세요.

Cosmos 특정 모델 사용자 지정

기본적으로 모든 엔터티 형식은 파생 컨텍스트(이 경우 "OrderContext")의 이름을 따서 동일한 컨테이너에 매핑됩니다. 기본 컨테이너 이름을 변경하려면 HasDefaultContainer를 사용합니다.

modelBuilder.HasDefaultContainer("Store");

엔터티 형식을 다른 컨테이너에 매핑하려면 ToContainer를 사용합니다.

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

지정된 항목이 나타내는 엔터티 형식을 식별하기 위해 EF Core는 파생된 엔터티 형식이 없더라도 판별자 값을 추가합니다. 판별자의 이름과 값은 변경할 수 있습니다.

다른 엔터티 형식이 동일한 컨테이너에 저장되지 않을 경우 HasNoDiscriminator를 호출하여 판별자를 제거할 수 있습니다.

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

파티션 키

기본적으로 EF Core는 항목을 삽입할 때 값을 제공하지 않고 파티션 키가 "__partitionKey"로 설정된 컨테이너를 만듭니다. 그러나 Azure Cosmos DB의 성능을 완벽하게 활용하려면 신중하게 선택한 파티션 키를 사용해야 합니다. HasPartitionKey를 호출하여 구성할 수 있습니다.

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

참고

파티션 키 속성은 문자열로 변환되는 경우 모든 형식을 사용할 수 있습니다.

일단 구성된 경우, 파티션 키 속성에는 항상 null이 아닌 값이 있어야 합니다. WithPartitionKey 호출을 추가하여 쿼리를 단일 파티션으로 만들 수 있습니다.

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.WithPartitionKey("2").LastAsync();
    Console.WriteLine($"Last order will ship to: {order.ShippingAddress.Street}, {order.ShippingAddress.City}");
    Console.WriteLine();
}

일반적으로 서버 의미 체계를 가장 잘 반영하고 일부 최적화를 허용하므로 기본 키에 파티션 키를 추가하는 것이 좋습니다(예: FindAsync).

프로비전된 처리량

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

포함된 엔터티

참고

관련 엔터티 형식은 기본적으로 소유로 구성됩니다. 특정 엔터티 형식에 대해 이 문제를 방지하려면 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();
}

필요한 경우 소유된 엔터티 형식에 대한 기본 기본 키를 변경할 수 있지만 키 값을 명시적으로 제공해야 합니다.

기본 형식의 컬렉션

지원되는 기본 형식(예: stringint)의 컬렉션이 자동으로 검색되고 매핑됩니다. 지원되는 컬렉션은 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; }
}

목록과 사전은 모두 일반적인 방법으로 데이터베이스에 채워지고 삽입될 수 있습니다.

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);
context.SaveChanges();

그러면 다음 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";

context.SaveChanges();

제한 사항:

  • 문자열 키가 있는 사전만 지원됩니다.
  • 기본 컬렉션의 내용에 대한 쿼리는 현재 지원되지 않습니다. 이러한 기능이 중요한 경우 #16926, #25700#25701에 투표하세요.

연결이 끊긴 엔터티 사용

모든 항목은 지정된 파티션 키에 고유한 id 값이 있어야 합니다. 기본적으로 EF Core는 '|'를 구분 기호로 사용하여 판별자 및 기본 키 값을 연결하고 값을 생성합니다. 키 값은 엔터티가 Added 상태로 전환될 때만 생성됩니다. 이 경우 .NET 형식에 값을 저장할 id 속성이 없는 경우 엔터티를 연결할 때 문제가 발생할 수 있습니다.

이 제한 사항을 해결하려면 id 값을 수동으로 만들어 설정하거나 먼저 엔터티를 추가된 것으로 표시한 다음 원하는 상태로 변경할 수 있습니다.

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은 다음과 같습니다.

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

ETag를 사용한 낙관적 동시성

낙관적 동시성을 사용하도록 엔터티 형식을 구성하려면 UseETagConcurrency를 호출합니다. 이 호출은 섀도 상태_etag 속성을 만들어 동시성 토큰으로 설정합니다.

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

동시성 오류를 더 쉽게 해결하려면 IsETagConcurrency를 사용하여 ETag를 CLR 속성에 매핑하면 됩니다.

modelBuilder.Entity<Distributor>()
    .Property(d => d.ETag)
    .IsETagConcurrency();