Março de 2019

Volume 34 – Número 3

[Pontos de dados]

Uma olhadinha na versão prévia do provedor do Cosmos DB no EF Core, Parte 2

Por Julie Lerman

Julie LermanNa coluna Pontos de dados de janeiro de 2019, apresentei uma primeira avaliação do provedor do Cosmos DB no EF Core. Esse provedor ainda é uma versão prévia e será lançado com a versão 3.0 do EF Core, portanto, agora é o momento ideal para se preparar para ele.

Na primeira parte, você leu sobre o motivo de haver um provedor de NoSQL para um ORM e aprendeu a executar algumas ações básicas de leitura e gravação de objetos individuais e seus dados relacionados, conforme as definições de um modelo simples. Escrever código que possa ser utilizado por esse provedor não é muito diferente de trabalhar com os provedores de bancos de dados relacionais mais conhecidos.

Você também aprendeu como o EF Core pode criar um banco de dados e contêineres em tempo real para contas de banco de dados preexistentes do Azure e como exibir os dados na nuvem usando a extensão do Cosmos DB para o Visual Studio Code.

Fiz um à parte na minha coluna de fevereiro de 2019 (msdn.com/magazine/mt833267) para avaliar superficialmente a API do MongoDB do Azure Cosmos DB, apesar de ele não estar relacionado ao EF Core. Agora vou retomar o assunto anterior para compartilhar algumas das descobertas interessantes que fiz ao explorar o provedor do Cosmos DB no EF Core.

Nesta coluna, você aprenderá sobre alguns dos recursos mais avançados do provedor: como configurar o DbContext para alterar a forma como o EF Core se concentra nos contêineres de banco de dados do Cosmos DB, detectar documentos incorporados com entidades próprias e usar o registro em log do EF Core para ver o SQL, além de outras informações de processamento interessantes geradas pelo provedor.

Mais informações sobre contêineres e mapeamentos do EF Core

Contêineres, também conhecidos como coleções no SQL do Cosmos DB e APIs do Mongo DB, são agrupamentos de itens, independentes de esquema, que são "unidades fundamentais de escalabilidade" para o Cosmos DB. Ou seja, você pode definir a taxa de transferência por contêiner, e um contêiner pode ser dimensionado e replicado como uma unidade. Conforme os dados aumentarem, a criação de modelos e a forma como eles se alinham com os contêineres terão impacto sobre o desempenho e o custo. Se você já usou o EF ou o EF Core, está familiarizado com o mapeamento padrão de um DbSet <TEntity> para uma tabela em um banco de dados relacional. No entanto, criar um contêiner do Cosmos DB separado para cada DbSet pode ser um padrão caro. Mas, como você aprendeu na Parte 1, o padrão é o mapeamento de todos os dados de um DbContext para um único contêiner. Por convenção, o contêiner tem o mesmo nome que o DbContext.

Vamos dar uma olhada em padrões e ver quais você pode controlar com o EF Core.

No artigo anterior, deixei o EF Core acionar a crianção de um novo banco de dados e um contêiner em tempo real. Eu já tinha uma conta do Azure, direcionada para a API do SQL (usada pelo EF Core). A redundância geográfica vem habilitada por padrão, mas só configurei minha conta para usar um único datacenter no Leste dos Estados Unidos. Portanto, por padrão, as gravações de várias regiões estão desabilitadas. Então, os bancos de dados que eu adicionar a essa conta e os contêineres que incluir nesses bancos de dados seguirão essas especificações abrangentes controladas pela conta.

Na primeira coluna, eu tinha um DbContext chamado ExpanseDbContext. Ao configurar o ExpanseDbContext para usar o provedor do Cosmos, especifiquei que o nome do banco de dados deve ser ExpanseCosmosDemo:

optionsBuilder.UseCosmos(endpointstring,accountkeystring, "ExpanseCosmosDemo")

Na primeira vez que meu código chamou o Database.EnsureCreated em uma instância do ExpanseDbContext, o banco de dados ExpanseCosmosDemo foi criado com o contêiner padrão, chamado ExpanseDbContext, seguindo a convenção de uso do nome da classe DbContext.

O contêiner foi criado usando os padrões do Azure Cosmos DB mostrados na Figura 1. A configuração da política de indexação que usa o padrão, que é "consistente", não é mostrada na figura.

Padrões do Azure Cosmos DB para a criação de um contêiner
Figura 1 - Padrões do Azure Cosmos DB para criar um contêiner

Essas configurações não podem ser afetadas pelo EF Core. Você pode modificá-las no portal, usando a CLI do Azure ou um SDK. Isso é conveniente porque a função do EF Core é ler e gravar dados. Mas, com o EF Core, você pode afetar nomes de contêineres e entidades de mapeamento a serem armazenados em diferentes contêineres.

É possível substituir o nome do contêiner padrão com o método HasDefaultContainerName em OnConfiguring. O exemplo a seguir usará ExpanseDocuments como o nome padrão, em vez de ExpanseDbContext:

modelBuilder.HasDefaultContainerName("ExpanseDocuments");

Se você desejar dividir os dados em diferentes contêineres, pode mapear um novo nome de contêiner para entidades específicas. Veja um exemplo que especifica a entidade Ship do artigo anterior em um contêiner chamado ExpanseShips:

modelBuilder.Entity<Ship>().ToContainer("ExpanseShips");

Você pode direcionar quantas entidades desejar para um único contêiner. O contêiner padrão já demonstra isso. Mas você pode usar ToContainer("ExpanseShips") com outras entidades também, se desejar.

O que acontece quando você adiciona um novo contêiner a um banco de dados dessa maneira? Como observei na Parte 1, a única maneira de fazer o EF Core criar um banco de dados ou contêiner é chamando context.Database.EnsureCreated. O EF Core reconhecerá o que já existe e criará novos contêineres, conforme necessário.

Se você alterar o nome do contêiner padrão, o EF cria o novo contêiner e funciona com ele desse momento em diante. Observe, no entanto, que todos os dados no contêiner original permanecerão lá.

Como o Azure Cosmos DB não pode renomear um contêiner existente, a recomendação oficial é mover os dados para a nova coleção, talvez com uma biblioteca do executor em massa, como a encontrada em bit.ly/2RbpTvp. Isso também acontecerá se você alterar o mapeamento de uma entidade para um contêiner diferente. Os dados originais não serão movidos, e você será responsável por garantir que os itens antigos sejam transferidos. Novamente, provavelmente é melhor fazer essa movimentação uma vez fora do EF Core.

Eu também testei adicionar gráficos de Consortium com Ships, onde os documentos acabariam em contêineres separados no banco de dados. Ao ler esses dados, pude escrever uma consulta para Consortia, que carregou imediatamente seus dados de Ship, por exemplo:

context.Consortia.Include(c=>c.Ships).FirstOrDefault()

O EF Core não conseguiu recuperar os dados dos contêineres separados e reconstruir o gráfico do objeto.

As Entidades próprias são incorporadas em documentos pai

Na Parte 1, você viu que as entidades relacionadas foram armazenadas em seus próprios documentos. Listei as classes Expanse na Figura 2 como um lembrete do modelo de exemplo. Quando criei um gráfico de um Consortium com Ships, cada objeto foi armazenado como um documento separado com chaves estrangeiras que permitem que o EF Core ou outro código se conecte a elas novamente. Esse é um conceito muito relacional, mas como Consortia e Ships são entidades exclusivas que têm suas próprias chaves de identidade, é assim que o EF Core conseguirá fazê-las persistir. No entanto, o EF Core reconhece o banco de dados de documentos e os documentos incorporados, o que você pode confirmar ao trabalhar com entidades próprias. Observe que o tipo Origin não tem uma propriedade de chave e é usado como uma propriedade tanto de Ship quanto de Consortium. Trata-se de uma entidade própria no meu modelo. Você pode ler mais sobre o recurso Entidade própria do EF Core no meu artigo Pontos de dados, de abril de 2018, em msdn.com/magazine/mt846463.

Figura 2 - As classes Expanse

public class Consortium
{
  public Consortium()
  {
    Ships=new List<Ship>();
    Stations=new List<Station>();
  }
  public Guid ConsortiumId { get; set; }
  public string Name { get; set; }
  public List<Ship> Ships{get;set;}
  public List<Station> Stations{get;set;}
  public Origin Origin{get;set;}
}
public class Planet
{
  public Guid PlanetId { get; set; }
  public string PlanetName { get; set; }
}
public class Ship
{
  public Guid ShipId {get;set;}
  public string ShipName {get;set;}
  public int PlanetId {get;set;}
  public Origin Origin{get;set;}
}
public class Origin
{
  public DateTime Date{get;set;}
  public String Location{get;set;}
}

Para que o EF Core reconheça um tipo próprio e possa mapeá-lo para um banco de dados, você precisa configurá-lo como uma anotação de dados ou, preferencialmente, como uma configuração da API fluente. No método DbContext OnConfiguring, como estou fazendo aqui, é feita uma configuração da API fluente:

modelBuilder.Entity<Ship>().OwnsOne(s=>s.Origin);
modelBuilder.Entity<Consortium>().OwnsOne(s=>s.Origin);
Here’s some code for adding a new Ship, along with its origin, to a consortium object:
consortium.Ships.Add(new Ship{ShipId=Guid.NewGuid(),ShipName="Nathan Hale 3rd",
                              Origin= new Origin {Date=DateTime.Now,
                              Location="Earth"}});

Quando o Consortium é salvo por meio de ExpanseContext, o novo Ship também é salvo em seu próprio documento.

A Figura 3 exibe o documento para esse Ship com sua origem representada como um documento incorporado. Um banco de dados de documentos não precisa de um subdocumento para ter uma chave estrangeira retornada com seu pai. No entanto, a lógica do EF Core para manter entidades próprias persistentes exige a chave estrangeira (manipulada por propriedades de sombra do EF Core) para que as entidades próprias persistam em bancos de dados relacionais. Portanto, ele aproveita sua lógica existente para inferir a propriedade de ShipId dentro do subdocumento de Origin.

Figura 3 - Um documento de Ship com um subdocumento de Origin incorporado

{
  "ShipId": "e5d48ffd-e52e-4d55-97c0-cee486a91629",
  "ConsortiumId": "60ccb22d-4422-45b2-a54a-71fa240435b3",
  "Discriminator": "Ship",
  "PlanetId": 0,
  "ShipName": "Nathan Hale 3rd",
  "id": "c2bdd90f-cb6a-4a3f-bacf-b0b3ac191662",
  "Origin": {
    "ShipId": "e5d48ffd-e52e-4d55-97c0-cee486a91629",
    "Date": "2019-01-22T11:40:29.117453-05:00",
    "Discriminator": "Origin",
    "Location": "Earth"
  },
  "_rid": "cgEVAKklUPgCAAAAAAAAAA==",
  "_self": "dbs/cgEVAA==/colls/cgEVAKklUPg=/docs/
            cgEVAKklUPgCAAAAAAAAAA==/",
  "_etag": "\"0000a43b-0000-0000-0000-5c47477d0000\"",
  "_attachments": "attachments/",
  "_ts": 1548175229
}

O EF Core também tem a capacidade de mapear coleções próprias com o mapeamento OwnsMany. Nesse caso, você veria vários subdocumentos dentro do documento pai no banco de dados.

Há um erro que será corrigido na versão prévia 2 do EF Core 3.0.0. O EF Core atualmente não reconhece propriedades nulas de entidades próprias. Outros provedores de banco de dados lançarão uma exceção de tempo de execução se você tentar adicionar um objeto com uma propriedade nula de uma entidade própria, um comportamento sobre o qual você pode ler na coluna de abril de 2018, mencionada anteriormente. Infelizmente, o provedor do Cosmos DB não impede a adição de objetos nesse estado, mas ele não pode materializar objetos que não tenham a propriedade da entidade própria preenchida. Aqui está a exceção que foi gerada quando encontrei esse problema:

"System.InvalidCastException: Unable to cast object of type
 'Newtonsoft.Json.Linq.JValue' to type 'Newtonsoft.Json.Linq.JObject'."

Portanto, se você vir esse erro ao tentar consultar entidades que tenham propriedades de tipo próprio, espero que se lembre de que provavelmente é uma propriedade nula de tipo próprio que causou a exceção.

Registro em log da atividade do provedor

O EF Core se conecta à estrutura do registro em log do .NET Core, como abordei na minha coluna de outubro de 2018 (msdn.com/magazine/mt830355). Logo após esse artigo ser publicado, a sintaxe para instanciar o LoggerFactory foi simplificada, embora o modo de usar categorias e níveis de log para determinar o que deve constar nos logs não tenha sido alterado. Informei a sintaxe atualizada em uma postagem do blog, "Logging in EF Core 2.2 Has a Simpler Syntax—More Like ASP.NET Core" (O registro em log no EF Core 2.2 tem uma sintaxe mais simples — Mais parecida com o ASP.NET Core) (bit.ly/2UdSkuI).

Quando o EF Core interage com o provedor do Cosmos DB, ele também compartilha detalhes com o agente. Isso significa que você pode ver os mesmos tipos de informação encontrados nos logs de outros provedores.

Lembre-se de que o CosmosDB não usa SQL para inserir, atualizar e excluir, conforme você costumava fazer com bancos de dados relacionais. O SQL é usado para consultas somente. Então, SaveChanges não mostrará o SQL nos logs. No entanto, você pode ver como o EF Core está corrigindo os objetos, criando as IDs, chaves estrangeiras e discriminadores necessários. Pude ver todas essas informações quando fiz o registro em log de todas as categorias vinculadas ao Debug LogLevel, em vez de apenas filtrar os comandos de banco de dados.

Veja como configurei meu método GetLoggerFactory para fazer isso. Observe o método AddFilter. Em vez de passar uma categoria para o primeiro parâmetro, estou usando uma cadeia de caracteres vazia, o que gera todas as categorias:

private ILoggerFactory GetLoggerFactory()
{
  IServiceCollection serviceCollection = new ServiceCollection();
  serviceCollection.AddLogging(builder =>
         builder.AddConsole()
                .AddFilter("" , LogLevel.Debug));
  return serviceCollection.BuildServiceProvider()
          .GetService<ILoggerFactory>();
}

Se eu quisesse filtrar apenas os comandos SQL, teria passado o DbLoggerCategory.Database.Command.Name para gerar a cadeia de caracteres correta e obter apenas os eventos em vez de uma cadeia de caracteres vazia. Isso retransmitiu muitas mensagens de registro em log quando alguns gráficos foram inseridos e, em seguida, uma única consulta foi executada para recuperar alguns dos dados inseridos. Vou incluir a saída completa e meu programa no download fornecido nesta coluna.

Alguns dados interessantes desses logs são informações sobre como adicionar propriedades de sombra, onde você pode ver a propriedade Discriminator sendo preenchida (somente no caso desse provedor):

dbug: Microsoft.EntityFrameworkCore.Model[10600]
      The property 'Discriminator' on entity type 'Station' was created in shadow state
      because there are no eligible CLR members with a matching name.

Se você estiver salvando dados, depois de toda a correção ser feita, verá uma mensagem de log informando que SaveChanges está sendo iniciado:

debug: Microsoft.EntityFrameworkCore.Update[10004]
       SaveChanges starting for 'ExpanseContext'.

Depois disso, verá mensagens sobre a chamada do DetectChanges. O provedor usa a lógica de API interna para adicionar, modificar ou remover o documento na coleção relevante, mas você não verá nenhum log específico sobre essa ação. No entanto, após a conclusão dessas ações, os logs retransmitirão etapas pós-salvamento típicas, como a atualização de contexto do estado do objeto recém-publicado:

dbug: Microsoft.EntityFrameworkCore.ChangeTracking[10807]
      The 'Consortium' entity with key '{ConsortiumId: a4b0405e-a820-4806-8b60-159033184cf1}' 
      tracked by 'ExpanseContext' changed from 'Added' to 'Unchanged'.

Se você estiver executando uma consulta, verá uma série de mensagens enquanto o EF Core a resolve. O EF Core começa compilando a consulta e, em seguida, a executa até chegar no SQL, que a envia para o banco de dados. Veja uma mensagem do log mostrando o SQL final:

dbug: Microsoft.EntityFrameworkCore.Database.Command[30000]
      Executing Sql Query [Parameters=[]]
      SELECT c
      FROM root c
      WHERE (c["Discriminator"] = "Consortium")

Aguardando a versão

A versão prévia do provedor do Cosmos DB no EF Core está disponível para o EF Core 2.2+. Trabalhei com o EF Core 2.2.1 e, para ver se percebi todas as alterações, mudei para os pacotes do EF Core não lançados na versão prévia mais recente do EF Core 3, versão 3.0.0-preview.18572.1.

O EF Core 3 está para ser lançado com o .NET Core 3.0, mas as notícias mais recentes só informam que será em 2019. O lançamento oficial da versão prévia 2 foi anunciado no final de janeiro de 2019 na postagem do blog, em bit.ly/2UsNBp6. Caso tenha interesse nesse suporte para o Azure Cosmos DB, recomendo testá-lo agora e ajudar a equipe do EF a detectar problemas para torná-lo um provedor mais eficiente para você quando for lançado.


Julie Lerman é Diretora Regional da Microsoft, MVP da Microsoft, coach e consultora de equipes de software e reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos em grupos de usuários e conferências em todo o mundo. Ela escreve no blog thedatafarm.com/blog e é autora do “Programming Entity Framework”, bem como de uma edição do Code First e do DbContext, todos da O'Reilly Media. Siga-a no Twitter: @julielerman e confira seus cursos da Pluralsight em bit.ly/PS-Julie.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Andriy Svyryd
Andriy Svyryd é um desenvolvedor da Microsoft especializado em design de API e modelagem de dados.  Ele é desenvolvedor da equipe do Entity Framework desde 2010. Seu trabalho e projetos pessoais podem ser vistos em https://github.com/AndriySvyryd. A biografia completa está disponível em https://www.linkedin.com/in/andriy-svyryd-51364719/