透過 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.HasAutoscaleThroughput 或 CosmosModelBuilderExtensions.HasManualThroughput 藉以設定資料庫的佈建輸送量。 例如:
modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);
呼叫 CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput 或 CosmosEntityTypeBuilderExtensions.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();
}
提示
必要時,您可以變更自有實體類型的預設主索引鍵,但應明確提供索引鍵值。
基本類型集合
系統會自動探索及對應支援的基本類型集合,例如 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();