Compartir a través de



Marzo de 2019

Volumen 34, número 3

[Puntos de datos]

Un vistazo a la versión preliminar del proveedor de Cosmos DB en EF Core (segunda parte)

Por Julie Lerman

Julie LermanEn la columna de puntos de datos de enero de 2019, presenté un primer vistazo al proveedor de Cosmos DB para EF Core. Este proveedor aún está en versión preliminar y se espera que se publique con la versión 3.0 de EF Core, por lo que es un buen momento para prepararse de antemano.

En la primera parte, pudo leer por qué hay un proveedor de NoSQL para un ORM y aprendió a realizar algunas acciones básicas de lectura y escritura para objetos individuales y sus datos relacionados según los define un sencillo modelo. Escribir código que use este proveedor no es distinto de trabajar con los proveedores de bases de datos relacionales más conocidos.

También ha averiguado cómo EF Core puede crear una base de datos y contenedores sobre la marcha para cuentas de Azure Database existentes y ver los datos en la nube con la extensión de Cosmos DB para Visual Studio Code.

En mi columna de febrero de 2019 (msdn.com/magazine/mt833267), eché un vistazo a la API de MongoDB de Azure Cosmos DB, aunque esto no está relacionado con EF Core. Ahora volveré a mi tema anterior para compartir otros descubrimientos interesantes que hice al explorar el proveedor de Cosmos DB de EF Core.

En este artículo, obtendrá información sobre algunas de las características más avanzadas del proveedor, como la configuración de la clase DbContext para cambiar cómo EF Core se orienta a contenedores de bases de datos de Cosmos DB, la creación de documentos insertados con entidades en propiedad y el uso del registro de EF Core para ver el SQL junto con otra información de procesamiento interesante que genera el proveedor.

Más información sobre los contenedores y las asignaciones de EF Core

Los contenedores, también conocidos como colecciones en las API de Mongo DB y SQL de Cosmos DB, son agrupaciones de elementos independientes del esquema que constituyen las "unidades fundamentales de escalabilidad" para Cosmos DB. Es decir, puede definir un rendimiento por contenedor, y un contenedor se puede escalar y replicar como unidad. A medida que crecen los datos, el diseño de modelos y su forma de alinearse con contenedores afectará al rendimiento y al costo. Si ha usado EF o EF Core, estará familiarizado con el valor predeterminado que un elemento DbSet <TEntity> asigna a una tabla en una base de datos relacional. Sin embargo, la creación de un contenedor de Cosmos DB independiente para cada elemento DbSet podría generar un valor predeterminado costoso. Pero la opción predeterminada, como aprendió en la primera parte, es que todos los datos de una clase DbContext se asignan a un único contenedor. Y la convención es que el contenedor tiene el mismo nombre que la clase DbContext.

Echemos un vistazo a los valores predeterminados y veamos cuáles se pueden controlar con EF Core.

En el artículo anterior, dejé que EF Core desencadenara la creación de una nueva base de datos y un nuevo contenedor sobre la marcha. Ya tenía una cuenta de Azure destinada a la API de SQL (que es la que usa EF Core). La redundancia geográfica está habilitada de forma predeterminada, pero solo he configurado mi cuenta para que use un único centro de datos en el este de Estados Unidos. Por lo tanto, de forma predeterminada, la escritura en varias regiones está deshabilitada. Así que cualquier base de datos que agregue a esta cuenta y cualquier contenedor que agregue a las bases de datos seguirá las especificaciones dominantes que controla la cuenta.

En la primera columna, tenía un elemento DbContext denominado ExpanseDbContext. Al configurar ExpanseDbContext para que usara el proveedor Cosmos, especifiqué que el nombre de la base de datos debía ser ExpanseCosmosDemo:

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

La primera vez que mi código llamó a Database.EnsureCreated en una instancia de ExpanseDbContext, se creó la base de datos ExpanseCosmosDemo junto con el contenedor predeterminado, llamado ExpanseDbContext, siguiendo la convención de usar el nombre de la clase DbContext.

Se creó el contenedor con los valores predeterminados de Azure Cosmos DB que se muestran en la figura 1. Lo que no se muestra en la figura es la configuración de la directiva de indexación que usa el valor predeterminado, que es "consistent".

Valores predeterminados de Azure Cosmos DB para crear un contenedor
Figura 1 Valores predeterminados de Azure Cosmos DB para crear un contenedor

Esta configuración no puede verse afectada por EF Core. Puede modificarla en el portal mediante la CLI de Azure o un SDK. Esto tiene sentido porque el rol de EF Core es leer y escribir datos. Pero algo que puede afectar con EF Core es el almacenamiento de los nombres de contenedor y las entidades de asignación en contenedores diferentes.

Puede reemplazar el nombre del contenedor predeterminado con el método HasDefaultContainerName en OnConfiguring. Por ejemplo, en el ejemplo siguiente se usa ExpanseDocuments como nombre predeterminado, en lugar de ExpanseDbContext:

modelBuilder.HasDefaultContainerName("ExpanseDocuments");

Si ha determinado que desea dividir los datos en contenedores diferentes, puede asignar un nuevo nombre de contenedor para entidades concretas. Este es un ejemplo que especifica la entidad Ship del artículo anterior en un contenedor denominado ExpanseShips:

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

Puede dirigir tantas entidades a un único contenedor como quiera. El contenedor predeterminado ya muestra esto. Pero, si quiere, también puede usar ToContainer("ExpanseShips") con otras entidades.

¿Qué ocurre cuando se agrega un nuevo contenedor a una base de datos de esta manera? Como he indicado en la parte 1, la única manera de que EF Core cree una base de datos o contenedor es llamando a context.Database.EnsureCreated. EF Core reconocerá lo que ya existe y lo que aún no y creará los contenedores nuevos que sean necesarios.

Si cambia el nombre del contenedor predeterminado, EF creará el nuevo contenedor y funcionará con dicho contenedor en adelante. Pero los datos del contenedor original seguirán allí.

Dado que Azure Cosmos DB no tiene la capacidad de cambiar el nombre de un contenedor existente, la recomendación oficial es mover los datos a la nueva colección, quizás con una biblioteca de ejecutor masiva, como la de bit.ly/2RbpTvp. Lo mismo sucede si cambia la asignación de una entidad a un contenedor diferente. No se moverán los datos originales y será responsable de garantizar que se transfieren los elementos antiguos. De nuevo, probablemente, es más razonable hacer ese movimiento único fuera de EF Core.

También he probado a agregar grafos de Consortium con Ships en que los documentos acabarían en contenedores distintos de la base de datos. Al leer los datos, pude escribir una consulta para Consortia que cargaba los datos de las naves; por ejemplo:

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

EF Core pudo recuperar los datos de los distintos contenedores y reconstruir el grafo de objetos.

Entidades en propiedad insertadas en documentos principales

En la primera parte, vio que las entidades relacionadas se almacenan en sus propios documentos. He enumerado las clases Expanse de la figura 2 como recordatorio del modelo de ejemplo. Al compilar un grafo de Consortium con Ships, cada objeto se almacenó como documento independiente con claves externas que permiten que EF Core u otro código los vuelva a conectar. Es un concepto muy relacional, pero, dado que Consortia y Ships son entidades únicas con sus propias claves de identidad, así es como EF Core las persiste. Pero EF Core reconoce los documentos insertados y las bases de datos de documentos, lo que se aprecia al trabajar con entidades en propiedad. Tenga en cuenta que el tipo de origen no tiene ninguna propiedad clave y se utiliza como propiedad tanto de Ship como de Consortium. En mi modelo, será una entidad en propiedad. Para obtener más información sobre la característica de entidad en propiedad de EF Core, consulte mi artículo de abril de 2018 sobre puntos de datos: msdn.com/magazine/mt846463.

Figura 2 Clases de 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 EF Core comprenda un tipo en propiedad y pueda asignarlo a una base de datos, debe configurarlo como anotación de datos o (siempre mi preferencia) una configuración de API fluida. Esto último ocurre en el método OnConfiguring de DbContext, como se muestra aquí:

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"}});

Cuando se guarda Consortium a través de ExpanseContext, también se guarda el nuevo valor de Ship en su propio documento.

En la figura 3 se muestra el documento para el elemento Ship con su elemento Origin representado como documento insertado. Una base de datos de documentos no necesita ningún subdocumento para tener una clave externa con su elemento principal. Sin embargo, la lógica de EF Core para persistir entidades en propiedad requiere la clave externa (controlada por propiedades reemplazadas de EF Core) para persistir las entidades en propiedad en bases de datos relacionales. Por lo tanto, aprovecha su lógica existente para deducir la propiedad ShipId dentro del subdocumento Origin.

Figura 3 Documento Ship con subdocumento Origin insertado

{
  "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
}

EF Core también permite asignar colecciones en propiedad con la asignación OwnsMany. En este caso, vería varios subdocumentos dentro del documento principal en la base de datos.

Hay un problema que se corregirá en la versión preliminar 2 de EF Core 3.0.0. Actualmente, EF Core no entiende las propiedades de entidad con propiedad null. El resto de proveedores de bases de datos lanzarán una excepción en tiempo de ejecución si intenta agregar un objeto con una propiedad de entidad de propiedad null, un comportamiento sobre el que puede obtener más información en la mencionada columna de abril de 2018. Lamentablemente, el proveedor de Cosmos DB no impide agregar objetos en este estado, pero no es capaz de materializar objetos que no tienen la propiedad de entidad en propiedad completada. Esta es la excepción que se generó cuando me encontré con este problema:

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

Así que, si ve este error al intentar consultar las entidades que disponen de propiedades de tipo en propiedad, espero que recuerde que, probablemente, el motivo de la excepción es una propiedad de tipo de propiedad null.

Registrar la actividad del proveedor

EF Core se conecta al marco de registro de .NET Core, como describí en mi columna de octubre de 2018 (msdn.com/magazine/mt830355). Poco después de la publicación del artículo, se simplificó la sintaxis para crear instancias de LoggerFactory, aunque los medios para usar categorías y niveles de registro para determinar qué debería generar resultados en los registros no cambió. Hablé de la sintaxis actualizada en una entrada de blog: "Logging in EF Core 2.2 Has a Simpler Syntax—More Like ASP.NET Core" (El registro de EF Core 2.2 tiene una sintaxis más sencilla, más parecida a la de ASP.NET Core, bit.ly/2UdSkuI).

Cuando EF Core interactúa con el proveedor de Cosmos DB, también comparte detalles con el registrador. Esto significa que puede ver los mismos tipos de información en los registros que con otros proveedores.

Tenga en cuenta que CosmosDB no usa SQL para insertar, actualizar ni eliminar, como solía hacer con las bases de datos relacionales. SQL solo se usa para consultas, de modo que SaveChanges no mostrará SQL en los registros. Sin embargo, puede ver cómo EF Core corrige todos los objetos, y crea los identificadores necesarios, las claves externas y los discriminadores. Pude ver toda esta información al registrar todas las categorías vinculadas al nivel de registro de depuración, en lugar de filtrar únicamente por comandos de base de datos.

A continuación se muestra cómo configuré mi método GetLoggerFactory para hacerlo. Observe el método AddFilter. En lugar de pasar una categoría en el primer parámetro, estoy usando una cadena vacía, lo que me devuelve todas las categorías:

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

Si hubiera querido filtrar solo por comandos SQL, habría pasado DbLoggerCategory.Database.Command.Name para proporcionar la cadena correcta solo para esos eventos, en lugar de una cadena vacía. Esto retransmitió muchos mensajes de registro al insertar algunos grafos y, a continuación, ejecutar una única consulta para recuperar algunos datos insertados. Incluiré el resultado completo y mi programa en la descarga que se incluye con esta columna.

Algunos alicientes interesantes de dichos registros incluyen esta información acerca de cómo agregar propiedades reemplazadas donde pueda. En el caso de este proveedor, consulte la propiedad Discriminator que se completa:

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.

Si va a guardar los datos, después de corregir todo esto, verá un mensaje de registro que indica que se está iniciando SaveChanges:

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

Esto va seguido de mensajes sobre la llamada a DetectChanges. El proveedor usará la lógica interna de API para agregar, modificar o quitar el documento de la colección correspondiente, pero no verá ningún registro concreto sobre esa acción. Sin embargo, después de completar estas acciones, los registros retransmitirán pasos comunes posteriores al almacenamiento, como la actualización del contexto del estado del objeto recién 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'.

Si ejecuta una consulta, verá varios mensajes a medida que EF Core elabora la consulta. EF Core empieza por compilar la consulta y, después, la retoca hasta que llega al SQL que se envía a la base de datos. Esto es un mensaje de registro con el SQL final:

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

A la espera del lanzamiento

La versión preliminar del proveedor de Cosmos DB en EF Core está disponible para EF Core 2.2 y versiones posteriores. Trabajé con EF Core 2.2.1 y, a continuación, para ver si observaba algún cambio, cambié a los paquetes de EF Core no publicados de la versión preliminar más reciente de EF Core 3, versión 3.0.0-preview.18572.1.

EF Core 3 está en la misma programación de versión que .NET Core 3.0, pero la información más reciente acerca de la publicación solo indica "en algún momento de 2019". Se anunció el lanzamiento oficial de la versión preliminar 2 a finales de enero de 2019 en la entrada de blog disponible en bit.ly/2UsNBp6. Si está interesado en admitir Azure Cosmos DB, le recomiendo que lo pruebe ahora y ayude al equipo de EF a descubrir cualquier problema para que sea un proveedor más viable para usted cuando se publique.


Julie Lerman es directora regional de Microsoft, MVP de Microsoft, instructora y consultora del equipo de software. Vive en las colinas de Vermont. Puede encontrarla haciendo presentaciones sobre el acceso a datos y otros temas en grupos de usuarios y en conferencias en todo el mundo. Su blog es thedatafarm.com/blog y es la autora de "Programming Entity Framework", así como de una edición de Code First y una edición de DbContext, de O’Reilly Media. Sígala en Twitter en @julielerman y vea sus cursos de Pluralsight en bit.ly/PS-Julie.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: Andriy Svyryd
Andriy Svyryd es un desarrollador de Microsoft especializado en modelos de datos y diseño de API.  Ha trabajado como desarrollador en el equipo de Entity Framework desde 2010. Su trabajo y proyectos personales se pueden consultar en https://github.com/AndriySvyryd. Su biografía completa está disponible en https://www.linkedin.com/in/andriy-svyryd-51364719/.