Configurar o modelo com o Provedor EF Core do Azure Cosmos DB
Contêineres e tipos de entidade
No Azure Cosmos DB, os documentos JSON são armazenados em contêineres. Ao contrário das tabelas em bancos de dados relacionais, os contêineres do Azure Cosmos DB podem conter documentos com formas diferentes; um contêiner não impõe um esquema uniforme em seus documentos. No entanto, várias opções de configuração são definidas no nível do contêiner e, portanto, afetam todos os documentos contidos nele. Consulte a documentação do Azure Cosmos DB sobre contêineres para obter mais informações.
Por padrão, o EF mapeia todos os tipos de entidade para o mesmo contêiner; esse geralmente é um bom padrão em termos de desempenho e preço. O contêiner padrão recebe o nome do tipo de contexto .NET (OrderContext
neste caso). Para alterar o nome do contêiner padrão, use HasDefaultContainer:
modelBuilder.HasDefaultContainer("Store");
Para mapear um tipo de entidade para um contêiner diferente, use ToContainer:
modelBuilder.Entity<Order>().ToContainer("Orders");
Antes de mapear tipos de entidades para contêineres diferentes, certifique-se de compreender as implicações potenciais de desempenho e preços (por exemplo, em relação à taxa de transferência dedicada e compartilhada); consulte a documentação do Azure Cosmos DB para saber mais.
IDs e chaves
O Azure Cosmos DB exige que todos os documentos tenham uma propriedade JSON id
que os identifique exclusivamente. Assim como outros provedores EF, o provedor EF Azure Cosmos DB tentará localizar uma propriedade chamada Id
ou <type name>Id
, e configurar essa propriedade como a chave do tipo de entidade, mapeando-a para a propriedade JSON id
. Você pode configurar qualquer propriedade para ser a propriedade chave usando HasKey; consulte a documentação geral do EF sobre chaves para obter mais informações.
Os desenvolvedores que acessam o Azure Cosmos DB de outros bancos de dados às vezes esperam que a propriedade (Id
) chave seja gerada automaticamente. Por exemplo, no SQL Server, o EF configura propriedades de chave numérica como colunas IDENTITY, onde valores de incremento automático são gerados no banco de dados. Por outro lado, o Azure Cosmos DB não dá suporte à geração automática de propriedades e, portanto, as propriedades de chave devem ser definidas explicitamente. A inserção de um tipo de entidade com uma propriedade de chave não definida simplesmente inserirá o valor padrão CLR para essa propriedade (por exemplo, 0 para int
) e uma segunda inserção falhará; o EF emitirá um aviso se você tentar fazer isso.
Se quiser usar um GUID como sua propriedade chave, você poderá configurar o EF para gerar valores exclusivos e aleatórios no cliente:
modelBuilder.Entity<Session>().Property(b => b.Id).HasValueGenerator<GuidValueGenerator>();
Chaves de partição
O Azure Cosmos DB usa o particionamento para obter o dimensionamento horizontal; a modelagem adequada e a seleção cuidadosa da chave de partição são fundamentais para obter um bom desempenho e manter os custos baixos. É altamente recomendável ler a documentação do Azure Cosmos DB sobre particionamento e planejar sua estratégia de particionamento com antecedência.
Para configurar a chave de partição com o EF, chame HasPartitionKey, passando a ela uma propriedade regular em seu tipo de entidade:
modelBuilder.Entity<Order>().HasPartitionKey(o => o.PartitionKey);
Qualquer propriedade pode ser transformada em uma chave de partição, contanto que seja convertida em cadeia de caracteres. Depois de configurada, a propriedade da chave de partição sempre deve ter um valor não nulo; se você tentar inserir um novo tipo de entidade com uma propriedade de chave de partição não definida, isso causará um erro.
Observe que o Azure Cosmos DB permite que dois documentos com a mesma propriedade id
existam em um contêiner, desde que eles estejam em partições diferentes; isso significa que, para identificar exclusivamente um documento dentro de um contêiner, as propriedades da chave de partição e da partição id
devem ser fornecidas. Por esse motivo, a noção interna do EF da chave primária da entidade contém esses dois elementos por convenção, diferentemente de bancos de dados relacionais em que não há nenhum conceito de chave de partição. Isso significa, por exemplo, que FindAsync
requer tanto propriedades de chave quanto de chave de partição (consulte mais documentos), e uma consulta deve especificá-las em sua cláusula Where
para se beneficiar de propriedades eficientes e econômicas point reads
.
Observe que a chave de partição é definida no nível do contêiner. Isso significa que não é possível que vários tipos de entidade no mesmo contêiner tenham propriedades de chave de partição diferentes. Se você precisar definir chaves de partição diferentes, mapeie os tipos de entidade relevantes para contêineres diferentes.
Chaves de partição hierárquicas
O Azure Cosmos DB também dá suporte a chaves de partição hierárquicas para otimizar ainda mais a distribuição de dados; consulte a documentação do para obter mais detalhes. O EF 9.0 adicionou suporte para chaves de partição hierárquicas; para configurá-las, basta passar até 3 propriedades para HasPartitionKey:
modelBuilder.Entity<Order>().HasPartitionKey(o => new { e.TenantId, e.UserId, e.SessionId });
Com essa chave de partição hierárquica, as consultas podem ser facilmente enviadas apenas para um subconjunto relevante de subpartições. Por exemplo, se você consultar os Pedidos de um locatário específico, essas consultas serão executadas apenas nas subpartições desse locatário.
Se você não configurar uma chave de partição com o EF, um aviso será registrado na inicialização; o EF Core criará contêineres com a chave de partição definida como __partitionKey
e não fornecerá nenhum valor para ele ao inserir itens. Quando nenhuma chave de partição for definida, seu contêiner será limitado a 20 GB de dados, que é o armazenamento máximo para uma única partição lógica. Embora isso possa funcionar para pequenos aplicativos de desenvolvimento/teste, não é recomendável implantar um aplicativo de produção sem uma estratégia de chave de partição bem configurada.
Depois que configurar corretamente as propriedades de chave de partição, você poderá fornecer valores para elas em consultas; confira Consulta com chaves de partição para obter mais informações.
Discriminadores
Como vários tipos de entidade podem ser mapeados para o mesmo contêiner, o EF Core sempre adiciona uma propriedade discriminatória $type
a todos os documentos JSON que você salva (essa propriedade era chamada de Discriminator
antes do EF 9.0); isso permite que o EF reconheça documentos que estão sendo carregados do banco de dados e materialize o tipo .NET correto. Os desenvolvedores provenientes de bancos de dados relacionais podem estar familiarizados com discriminadores no contexto de TPH (herança de tabela por hierarquia). No Azure Cosmos DB, além de serem usados em cenários de mapeamento de herança, os discriminadores são usados também porque o mesmo contêiner pode conter tipos de documento completamente diferentes.
O nome e os valores da propriedade discriminatória podem ser configurados com as APIs EF padrão, consulte estes documentos para obter mais informações. Se você estiver mapeando um único tipo de entidade para um contêiner, tiver certeza de que nunca mapeará outro e quiser se livrar da propriedade discriminator, chame HasNoDiscriminator:
modelBuilder.Entity<Order>().HasNoDiscriminator();
Como o mesmo contêiner pode conter tipos de entidade diferentes e a propriedade JSON id
deve ser exclusiva em uma partição de contêiner, não é possível usar o mesmo valor id
para entidades de tipos diferentes na mesma partição de contêiner. Compare isso com bancos de dados relacionais, em que cada tipo de entidade é mapeado para uma tabela diferente e, portanto, tem seu próprio espaço de chave separado. Portanto, é sua responsabilidade garantir a id
exclusividade dos documentos que são inseridos em um contêiner. Se precisar usar diferentes tipos de entidade com os mesmos valores de chave primária, você poderá instruir o EF a inserir automaticamente o discriminador na propriedade id
da seguinte maneira:
modelBuilder.Entity<Session>().HasDiscriminatorInJsonId();
Embora possa facilitar o trabalho com valores id
, isso pode dificultar a interoperação com aplicativos externos que trabalham com seus documentos, pois agora eles devem estar cientes do formato concatenado do EFid
, bem como dos valores discriminatórios, que são derivados de seus tipos .NET por padrão. Observe que esse era o comportamento padrão antes do EF 9.0.
Uma opção adicional é instruir o EF a inserir apenas o discriminador raiz, que é o discriminador do tipo de entidade raiz da hierarquia, na propriedade id
:
modelBuilder.Entity<Session>().HasRootDiscriminatorInJsonId();
Isso é semelhante, mas permite que o EF use leituras de ponto eficientes em mais cenários. Se você precisar inserir um discriminador na propriedade id
, considere inserir o discriminador raiz para obter melhor desempenho.
Taxa de transferência provisionada
Se você utilizar o EF Core para criar o banco de dados ou contêineres do Azure Cosmos DB, poderá configurar a taxa de transferência provisionada para o banco de dados chamando CosmosModelBuilderExtensions.HasAutoscaleThroughput ou CosmosModelBuilderExtensions.HasManualThroughput. Por exemplo:
modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);
Para configurar a taxa de transferência provisionada para uma chamada de contêiner CosmosEntityTypeBuilderExtensions.HasAutoscaleThroughput ou CosmosEntityTypeBuilderExtensions.HasManualThroughput. Por exemplo:
modelBuilder.Entity<Family>(
entityTypeBuilder =>
{
entityTypeBuilder.HasManualThroughput(5000);
entityTypeBuilder.HasAutoscaleThroughput(3000);
});
Vida útil
Os tipos de entidade no modelo do Azure Cosmos DB podem ser configurados com um tempo de vida padrão. Por exemplo:
modelBuilder.Entity<Hamlet>().HasDefaultTimeToLive(3600);
Ou, para o repositório analítico:
modelBuilder.Entity<Hamlet>().HasAnalyticalStoreTimeToLive(3600);
O tempo de vida útil para entidades individuais pode ser definido usando uma propriedade mapeada para "ttl" no documento JSON. Por exemplo:
modelBuilder.Entity<Village>()
.HasDefaultTimeToLive(3600)
.Property(e => e.TimeToLive)
.ToJsonProperty("ttl");
Observação
Uma vida útil padrão deve ser configurada no tipo de entidade para que o "ttl" tenha qualquer efeito. Consulte TTL (vida útil) no Azure Cosmos DB para obter mais informações.
A propriedade de vida útil é definida antes que a entidade seja salva. Por exemplo:
var village = new Village { Id = "DN41", Name = "Healing", TimeToLive = 60 };
context.Add(village);
await context.SaveChangesAsync();
A propriedade de vida útil pode ser uma propriedade de sombra para evitar poluir a entidade de domínio com preocupações de banco de dados. Por exemplo:
modelBuilder.Entity<Hamlet>()
.HasDefaultTimeToLive(3600)
.Property<int>("TimeToLive")
.ToJsonProperty("ttl");
A propriedade tempo de vida útil da sombra é definida acessando a entidade controlada. Por exemplo:
var hamlet = new Hamlet { Id = "DN37", Name = "Irby" };
context.Add(hamlet);
context.Entry(hamlet).Property("TimeToLive").CurrentValue = 60;
await context.SaveChangesAsync();
Entidades inseridas
Observação
Os tipos de entidades relacionadas são configurados como propriedade por padrão. Para evitar isso para um tipo de entidade específico, chame ModelBuilder.Entity.
No Azure Cosmos DB, as entidades proprietárias são inseridas no mesmo item que o proprietário. Para alterar o nome de uma propriedade, 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");
});
Com essa configuração, a ordem do exemplo acima é armazenada da seguinte forma:
{
"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
}
Coleções de entidades próprias também são inseridas. Para o próximo exemplo, usaremos a classe Distributor
com uma coleção de StreetAddress
:
public class Distributor
{
public int Id { get; set; }
public string ETag { get; set; }
public ICollection<StreetAddress> ShippingCenters { get; set; }
}
As entidades próprias não precisam fornecer valores de chave explícitos para serem armazenadas:
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();
}
Elas serão persistidas da seguinte forma:
{
"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
}
Internamente, o EF Core sempre precisa ter valores de chave exclusivos para todas as entidades rastreadas. A chave primária criada por padrão para coleções de tipos próprios é composta pelas propriedades de chave estrangeira que apontam para o proprietário e por uma propriedade int
correspondente ao índice na matriz JSON. Para recuperar esses valores, a API de entrada pode ser usada:
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();
}
Dica
Quando necessário, a chave primária padrão para os tipos de entidade própria pode ser alterada, mas os valores da chave devem ser fornecidos explicitamente.
Coleções de tipos primitivos
Coleções de tipos primitivos com suporte, como string
e int
, são descobertas e mapeadas automaticamente. Coleções com suporte são todos os tipos que implementam IReadOnlyList<T> ou IReadOnlyDictionary<TKey,TValue>. Por exemplo, considere este tipo de entidade:
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; }
}
O IList
e o IDictionary
podem ser preenchidos e persistidos no banco de dados:
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();
Isso resulta no seguinte documento 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
}
Depois, essas coleções podem ser atualizadas novamente da maneira normal:
book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";
await context.SaveChangesAsync();
Limitações:
- Há suporte somente para dicionários com chaves de cadeia de caracteres.
- O suporte para consultas em coleções primitivas foi adicionado no EF Core 9.0.
Simultaneidade otimista com eTags
Para configurar um tipo de entidade para usar simultaneidade otimista, chame UseETagConcurrency. Essa chamada criará uma propriedade _etag
no estado de sombra e a definirá como o token de simultaneidade.
modelBuilder.Entity<Order>()
.UseETagConcurrency();
Para facilitar a resolução de erros de simultaneidade, você pode mapear a eTag para uma propriedade CLR usando IsETagConcurrency.
modelBuilder.Entity<Distributor>()
.Property(d => d.ETag)
.IsETagConcurrency();