共用方式為


透過 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),而第二次插入則會失敗;如果您嘗試這樣做,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 同時需要索引鍵和分割區索引鍵屬性 (請進一步參閱其他文件),而且查詢必須在其 Where 子句指定這些屬性,以便從高效且符合成本效益的 point reads 受益。

請注意,分割區索引鍵需在容器層級定義。 這值得注意的是,同一個容器中的多個實體類型不可能有不同的分割區索引鍵屬性。 如果您需要定義不同的分割區索引鍵,請將相關的實體類型對應到不同容器。

階層式分割區索引鍵

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 的數據,這是單 一邏輯分割區的最大記憶體。 雖然這適用於小型開發/測試應用程式,但強烈建議您部署生產應用程式,而不需要設定良好的分割區索引鍵策略。

正確設定分割區索引鍵屬性之後,您可以在查詢中提供這些屬性的值;如需詳細資訊,請參閱 使用分割區索引鍵 進行查詢。

鑑別子

由於您可將多個實體類型對應至相同容器,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 以前的預設行為。

另一個選項是指示 EF 僅將根目錄鑑別子插入 id 屬性,它在階層中是根目錄實體類型的鑑別子:

modelBuilder.Entity<Session>().HasRootDiscriminatorInJsonId();

這種作法也很相似,但可讓 EF 在更多情境中使用有效率的 點讀取。 如果您需要將鑑別子插入 id 屬性,建議插入根目錄鑑別子,以便提升效能。

佈建的輸送量

如果使用 EF Core 建立 Azure Cosmos DB 資料庫或容器,您可以呼叫 CosmosModelBuilderExtensions.HasAutoscaleThroughputCosmosModelBuilderExtensions.HasManualThroughput 藉以設定資料庫的佈建輸送量。 例如:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

呼叫 CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughputCosmosEntityTypeBuilderExtensions.HasManualThroughput 可設定容器的佈建輸送量。 例如:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

存留時間

您可以使用預設存留時間來設定 Azure Cosmos DB 模型的實體類型。 例如:

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

或者,如果是分析存放區:

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

您可以使用在 JSON 檔中對應至「ttl」的屬性來設定個別實體的存留時間。 例如:

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

注意

您必須對實體類型設定預設的存留時間,「ttl」才能產生作用。 如需詳細資訊,請參閱 Azure Cosmos DB 的存留時間 (TTL)

您需先設定存留時間屬性再儲存實體。 例如:

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

存留時間屬性可以是陰影屬性,以免影響需考量資料庫的網域實體。 例如:

modelBuilder.Entity<Hamlet>()
    .HasDefaultTimeToLive(3600)
    .Property<int>("TimeToLive")
    .ToJsonProperty("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();