다음을 통해 공유


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해당 속성을 엔터티 형식의 키로 구성하여 JSON 속성에 id 매핑합니다. HasKey를 사용하여 모든 속성을 키 속성으로 구성할 수 있습니다. 자세한 내용은 키에 대한 일반 EF 설명서를 참조하세요.

다른 데이터베이스에서 Azure Cosmos DB로 오는 개발자는 키(Id) 속성이 자동으로 생성될 것으로 예상하는 경우가 있습니다. 예를 들어 SQL Server에서 EF는 데이터베이스에서 자동 증분 값이 생성되는 IDENTITY 열로 숫자 키 속성을 구성합니다. 반면, Azure Cosmos DB는 속성의 자동 생성을 지원하지 않으므로 키 속성을 명시적으로 설정해야 합니다. 키 속성이 설정되지 않은 엔터티 형식을 삽입하면 해당 속성의 CLR 기본값(예: int의 경우 0)이 삽입되고 두 번째 삽입이 실패합니다. 이 작업을 수행하려고 하면 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 속성을 가진 두 개의 문서가 컨테이너에 존재할 수 있도록 허용합니다. 즉, 컨테이너 내에서 문서를 고유하게 식별하려면 파티션 키 속성과 파티션 키 속성을 모두 id 제공해야 합니다. 이로 인해 엔터티 기본 키에 대한 EF의 내부 개념은 파티션 키 개념이 없는 관계형 데이터베이스와 달리 규칙에 따라 이러한 두 요소를 모두 포함합니다. 즉, 예를 들어 FindAsync에는 키 및 파티션 키 속성이 모두 필요하며(추가 문서 참조), 쿼리는 효율적이고 비용 효과적인 point reads의 이점을 활용하기 위해 Where 절에 이를 지정해야 합니다.

파티션 키는 컨테이너 수준에서 정의됩니다. 이는 특히 동일한 컨테이너의 여러 엔터티 형식이 서로 다른 파티션 키 속성을 가질 수 없음을 의미합니다. 다른 파티션 키를 정의해야 하는 경우 관련 엔터티 형식을 다른 컨테이너에 매핑합니다.

계층적 파티션 키

또한 Azure Cosmos DB는 계층적 파티션 키를 지원하여 데이터 배포를 더욱 최적화합니다.자세한 내용은 설명서를 참조하세요. EF 9.0은 계층적 파티션 키에 대한 지원을 추가했습니다. 이를 구성하려면 HasPartitionKey에 최대 3개의 속성을 전달하기만 하면 됩니다.

modelBuilder.Entity<Order>().HasPartitionKey(o => new { e.TenantId, e.UserId, e.SessionId });

이러한 계층적 파티션 키를 사용하면 쿼리를 하위 파티션의 관련 하위 집합으로만 쉽게 보낼 수 있습니다. 예를 들어 특정 테넌트의 순서를 쿼리하는 경우 해당 쿼리는 해당 테넌트의 하위 파티션에 대해서만 실행됩니다.

EF를 사용하여 파티션 키를 구성하지 않으면 시작 시 경고가 기록됩니다. EF Core는 파티션 키가 __partitionKey로 설정된 컨테이너를 만들고 항목을 삽입할 때 이에 대한 값을 제공하지 않습니다. 파티션 키가 설정되지 않은 경우 컨테이너는 단일 논리 파티션에 대한 최대 스토리지인 20GB의 데이터로 제한됩니다. 이는 소규모 개발/테스트 애플리케이션에서 작동할 수 있지만 잘 구성된 파티션 키 전략 없이 프로덕션 애플리케이션을 배포하는 것이 좋습니다.

파티션 키 속성이 제대로 구성되면 쿼리에서 값을 제공할 수 있습니다. 자세한 내용은 파티션 키를 사용하여 쿼리를 참조하세요.

판별자

여러 엔터티 형식을 동일한 컨테이너에 매핑할 수 있으므로 EF Core는 저장한 모든 JSON 문서에 $type 판별자 속성을 항상 추가합니다(이 속성은 EF 9.0 이전에 Discriminator라고 함). 이렇게 하면 EF는 데이터베이스에서 로드되는 문서를 인식하고 올바른 .NET 형식을 구체화할 수 있습니다. 관계형 데이터베이스에서 온 개발자는 TPH(테이블별 계층 상속)컨텍스트에서 판별자에 익숙할 수 있습니다. Azure Cosmos DB에서는 상속 매핑 시나리오뿐만 아니라 동일한 컨테이너에 완전히 다른 문서 형식이 포함될 수 있기 때문에 판별자가 사용됩니다.

판별자 속성 이름 및 값은 표준 EF API를 사용하여 구성할 수 있습니다. 자세한 내용은 다음 문서를 참조하세요. 단일 엔터티 형식을 컨테이너에 매핑하는 경우 다른 엔터티 형식을 매핑하지 않을 것이며 판별자 속성을 제거하려는 경우 HasNoDiscriminator를 호출합니다.

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

동일한 컨테이너는 다른 엔터티 형식을 포함할 수 있고 JSON id 속성은 컨테이너 파티션 내에서 고유해야 하므로 동일한 컨테이너 파티션에서 서로 다른 형식의 엔터티에 대해 동일한 id 값을 가질 수 없습니다. 각 엔터티 형식이 다른 테이블에 매핑되므로 별도의 고유한 키 공간이 있는 관계형 데이터베이스와 비교합니다. 따라서 컨테이너에 삽입하는 문서의 id 고유성을 확인하는 것은 사용자의 책임입니다. 기본 키 값이 동일한 다른 엔터티 형식이 필요한 경우 다음과 같이 EF에 판별자를 id 속성에 자동으로 삽입하도록 지시할 수 있습니다.

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 모델의 엔터티 형식은 기본 TTL(Time to Live)로 구성할 수 있습니다. 예시:

modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);

또는 분석 저장소의 경우:

modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);

JSON 문서에서 "ttl"에 매핑된 속성을 사용하여 개별 엔터티의 TTL(Time-to-Live)을 설정할 수 있습니다. 예시:

modelBuilder.Entity<Village>()
    .HasDefaultTimeToLive(3600)
    .Property(e => e.TimeToLive)
    .ToJsonProperty("ttl");

참고 항목

"ttl"이 적용되려면 엔터티 형식에 기본 TTL(Time-To-Live)을 구성해야 합니다. 자세한 내용은 Azure Cosmos DB의 TTL(Time to Live)을 참조하세요.

그러면 엔터티가 저장되기 전에 TTL 속성이 설정됩니다. 예시:

var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();

데이터베이스 문제로 인해 도메인 엔터티가 오염되는 것을 방지하기 위해 TTL 속성은 섀도 속성이 될 수 있습니다. 예시:

modelBuilder.Entity<Hamlet>()
    .HasDefaultTimeToLive(3600)
    .Property<int>("TimeToLive")
    .ToJsonProperty("ttl");

그런 다음 추적된 엔터티에 액세스하여 섀도 TTL 속성을 설정합니다. 예시:

var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();

포함된 엔터티

참고

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

IListIDictionary는 데이터베이스에 채워지고 유지될 수 있습니다.

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