Enero de 2019

Volumen 34, número 1

[Puntos de datos]

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

Por Julie Lerman | Enero de 2019

El proveedor de Cosmos DB en EF Core se encuentra en versión preliminar. Toda la información está sujeta a cambios.

Julie LermanLos lectores habituales de esta columna sabrán que trabajo mucho con Entity Framework (EF) y la versión más reciente de EF Core, y que también soy un gran defensor de Azure Cosmos DB. A primera vista, podría pensar que un asignador relacional de objetos (ORM) y un documento de base de datos, como Cosmos DB, no tienen nada que ver el uno con el otro. ORM se diseño para solucionar el problema de asignación de objetos a bases de datos relacionales. Con una base de datos de documentos o de otros tipos de NoSQL, simplemente puede almacenar los objetos y los gráficos de objetos como JSON y no tener que preocuparse por las restricciones de una base de datos relacional. Así que, ¿por qué yo, el equipo de EF u otros desarrolladores los querrían combinar?

¿Por qué se debería usar un ORM con una base de datos NoSQL?

Cuando llegaron las primeras versiones beta de EF Core en forma de EF7, incluían un proveedor de prueba de concepto para interactuar con Azure Table Storage, la única base de datos NoSQL en Azure en el momento. Después de probarlo, me di cuenta de una ventaja importante que proporciona EF7 y de que actualmente existe la misma ventaja con EF Core y Azure Cosmos DB. Los clientes que interactúan con la base de datos, por ejemplo, el cliente de .NET o el SDK de Node.js, requieren mucho código de configuración para identificar la base de datos, los contenedores y los objetos de consulta. (Tenga en cuenta que las versiones futuras de los clientes reducirán esta complejidad). Pero con EF Core, lo primero de lo que me di cuenta es que no tenía que escribir todo ese código adicional. Era suficiente con solo proporcionar una cadena de conexión. De hecho, al confirmar que este almacén de datos no es relacional, no tengo que centrarme en él mientras realizo tareas superficiales, como consultar o guardar datos. Puedo aprovechar toda mi experiencia de interactuar con los almacenes de datos con EF Core y únicamente apuntarlos a un almacén de datos diferente. El proveedor se encarga de la interpretación de las consultas y la creación y lectura de los documentos JSON que se almacenan en la base de datos.

Aunque el proveedor también puede crear bases de datos y contenedores mediante convenciones, todavía depende de usted el hecho de crear las cuentas de Cosmos DB para las bases de datos y determinar su configuración, así como optimizar cada base de datos para mejorar el rendimiento y reducir los costos. Estas son tareas que realiza con cualquier base de datos relacional, por lo que ocurre lo mismo cuando se apunta EF Core a una base de datos de Azure Cosmos DB.

El nuevo proveedor de Cosmos DB para EF Core se incluye como una versión preliminar con EF Core 2.2. Se espera que se lance por completo con EF Core 3.0. No obstante, he tenido mucha curiosidad para usarlo desde que probé la prueba de concepto para Azure Table Storage hace más de dos años, así que he decidido no esperar hasta que se lance completamente para echarle un vistazo. Estoy seguro que muchos de vosotros también habéis tenido curiosidad al respecto.

Herramientas de cliente de Cosmos DB

Azure Portal cuenta con un explorador de datos fantástico para visualizar los contenedores, los documentos y las bases de datos de Cosmos DB, pero a veces a uno no le apetece ir pasando del IDE a un sitio web y viceversa. Tanto Visual Studio Code como Visual Studio 2017 cuentan con excelentes extensiones para ver y editar documentos en sus bases de datos de Cosmos DB. VS Code tiene la extensión de Azure Cosmos DB (bit.ly/2SuTXmS) y Visual Studio, la extensión de Cloud Explorer para Visual Studio 2017 (bit.ly/2G3SNxj). La extensión de Azure Cosmos DB también le permite trabajar con cuentas de Cosmos DB preexistentes para crear y quitar contenedores y bases de datos sobre la marcha.

Incorporar el proveedor en la solución

El proveedor funciona como cualquier otro proveedor de EF Core. Debe hacer referencia a su paquete del proyecto y, luego, especificarlo en OnConfiguring o bien, si usa ASP.NET Core, cuando defina la clase de DbContext en el inicio.

El proveedor se denomina Microsoft.EntityFrameworkCore.Cosmos. (El nombre se ha acortado desde las versiones anteriores, por si se lo estaba preguntando). Y, como con cualquier paquete, hay varias formas de agregarlo a su proyecto. En Visual Studio 2017, puede usar el administrador de paquetes. En la línea de comandos, puede agregarlo con:

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

O bien, si simplemente quiere abrir el archivo de proyecto, puede agregarlo a una sección ItemGroup con el siguiente valor de PackageReference:

<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.Cosmos"/>
</ItemGroup>

Una vez que haya agregado el paquete, debe indicarle a su clase de DbContext que use el proveedor. Tengo una clase de DbContext denominada ExpanseContext que asigna mi modelo simplista de una serie de libros o un programa de televisión que me gusta ("The Expanse" ["La expansión"]) a mi almacén de datos, una base de datos de Azure Cosmos DB.

Al igual que con cualquiera de los otros proveedores, este paquete le proporcionará el método de extensión UseCosmos en DbContextOptionsBuilder. Desde allí, deberá proporcionar los tres elementos clave de una cadena de conexión de Azure Cosmos DB: el valor AccountEndpoint, el valor de clave y el nombre de la base de datos.

Puede copiar la cadena de conexión desde Azure Portal, la extensión de Azure CosmosDB para VS Code o la extensión de Cloud Explorer para Visual Studio 2017. El formato que requiere UseCosmos no es el mismo que el de la cadena de conexión, pero esta última le proporciona una ventaja. Debe proporcionar los tres valores como tres parámetros separados por comas. En la Figura 1, se muestra un ejemplo en el que estoy configurando la conexión directamente en el método OnConfiguring de la clase ExpanseContext.

Figura 1 Especificación de la conexión de Cosmos DB en la clase DbContext

using Expanse.Classes;
using Microsoft.EntityFrameworkCore;
public class ExpanseContext : DbContext
{
  public DbSet<Consortium> Consortia{get;set;}
  public DbSet<Planet> Planets { get; set; }
  public DbSet<Character> Characters { get; set; }
  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder.UseCosmos(
      "https://lermandatapoints.documents.azure.com:443",
      "theverylongaccesskeygoeshere",
      "ExpanseCosmosDemo"
  );
}

Los parámetros de la conexión son los siguientes:

  • El punto de conexión de mi cuenta
  • Un sustituto de mi valor de clave
  • El nombre de la base de datos, ExpanseCosmosDemo

Si va a compilar una aplicación de ASP.NET Core, puede configurar el contexto en el método ConfigureServices de la clase de inicio como se muestra a continuación:

services.AddDbContext<ExpanseContext>(options=>
  options.UseCosmos(
    "https://lermandatapoints.documents.azure.com:443",
    "theverylongaccesskeygoeshere",
    "ExpanseCosmosDemo"
);

Hay algunos otros valores que puede configurar en relación con la base de datos de Cosmos DB, pero primero me gustaría mostrarle algunos de los comportamientos básicos y predeterminados. En la segunda parte de este artículo se mostrarán las opciones de configuración adicionales.

Clases de modelos

Deberá estar familiarizado con mi modelo, que se muestra en la Figura 2. El relato de la expansión trata sobre dos consorcios que compiten entre sí, las Naciones Unidas y la República del Congreso de Marte. Lo he simplificado a fin de reducir la complejidad de cambiar las lealtades o de los personajes que simplemente se vuelven totalmente deshonestos y rechazan los dos consorcios. Respetar eso en el modelo sería una lección de diseño guiado por el dominio (DDD).

Figura 2 Modelo de objetos de extensión de mi demostración

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 Guid PlanetId {get;set;}
    public Origin Origin{get;set;}
  }
  public class Origin
  {
    public DateTime Date{get;set;}
    public String Location{get;set;}
  }

Esto me deja con un puñado de clases simplistas y, de nuevo, con tal de centrarme en el comportamiento del proveedor, las he dejado como clases de CRUD sin lógica empresarial real. Dejaré de lado la estación espacial más interesante y las clases de personajes, ya que no voy a trabajar con ellos en la demostración.

Además de configurar el proveedor, la clase de contexto especifica DbSets para las entidades Consortium y Ship. A continuación, puede ver la lista completa de mi clase de contexto, ExpanseContext:

public class ExpanseContext : DbContext
{
  public DbSet<Consortium> Consortia{get;set;}
  public DbSet<Ship> Ships { get; set; }
  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    optionsBuilder.UseCosmos(
      "https://lermandatapoints.documents.azure.com:443",
      "theverylongaccesskeygoeshere",
      "ExpanseCosmosDemo");
  }
}

Permitir que EF Core cree la base de datos de Cosmos DB

Una base de datos de Azure Cosmos DB está vinculada a una cuenta de Cosmos DB y cada base de datos almacena sus documentos en varios subconjuntos, denominados contenedores. Puede que los conozca como colecciones en Cosmos DB, pero la terminología ha cambiado recientemente. Ahora, los documentos se conocen como elementos. Sin embargo, no piense que los contenedores son como tablas de bases de datos relacionales. Son muy diferentes. En la documentación se hace referencia a ellos como "la unidad de escalabilidad para el rendimiento aprovisionado y el almacenamiento de elementos". Las bases de datos de documentos no predefinen las estructuras de datos, por lo que es posible almacenar documentos estructurados de forma diferente en un solo contenedor. Si es nuevo en las bases de datos de documentos, consulte mi artículo anterior "¿Qué son las bases de datos documentales?" en msdn.com/magazine/hh547103. Dado que Cosmos DB se ha diseñado para almacenar grandes cantidades de datos, determinar el equilibrio entre diferentes contenedores y particiones es un proceso importante. Debe estar familiarizado un poco con él antes de embarcarse en la administración del rendimiento y los costos. Por supuesto, puede ajustar estos aspectos a medida que obtiene más información sobre cómo se utilizan sus datos. Recomiendo ver el vídeo "Modeling Data and Best Practices for the Azure Cosmos DB SQL API" ("Modelado de datos y procedimientos recomendados para Azure Cosmos DB SQL API") en bit.ly/2FZtIDs.

El método Database.EnsureCreated de EF Core puede crear una base de datos de Azure Cosmos DB, así como los contenedores necesarios en Azure. Si usa el emulador de Cosmos DB basado en Windows (bit.ly/2sHNsAn), puede dirigirse a una versión local de la base de datos durante el desarrollo. Deberá tener una cuenta de Cosmos DB previamente. La mayoría de las decisiones importantes que afectan al rendimiento y los costos se toman al crear cuenta de Azure Cosmos DB y aprovisionar el rendimiento en contenedores. El hecho de que EF Core pueda estar creando nuevas bases de datos o contenedores no le obliga a aceptar algunos valores predeterminados desconocidos, pero hay dos valores predeterminados que debe tener en cuenta: Primero, que EF Core almacenará todos los documentos en una sola partición y, segundo, que creará un contenedor con el nombre de su clase DbContext y almacenará todos los documentos en dicho contenedor. El motivo es que el número de contenedores que tiene en la base de datos afecta al costo. Por lo tanto, si aún no tiene ningún plan para distribuir sus elementos, puede comenzar con todo en un contenedor y una partición y, luego, configurar contenedores con particiones adicionales según sea necesario a fin de mejorar el rendimiento. De nuevo, deberá buscar ese equilibrio entre el rendimiento y el costo.

Mi aplicación de demostración solo es una aplicación de consola. Su método principal desencadena una llamada a mi método CreateDB, que llama a EffectCreated de EF Core:

private static void CreateDB()
  {
    using(var context=new ExpanseContext())
    {
      context.Database.EnsureCreated();
    }
  }

Dado que uso los valores predeterminados, la primera vez que se ejecute, EnsureCreated creará la nueva base de datos y un único contenedor, denominado ExpanseContext. Si agrega explícitamente otros contenedores más adelante, EnsureCreated reconocerá que la base de datos y el contenedor inicial ya existen y solo creará el nuevo contenedor automáticamente. Examinaré este tema en más detalle en la segunda parte del artículo.

Almacenar datos en un contenedor

La interacción de su propio código con EF Core es la misma que con cualquier otra base de datos. Sin embargo, lo que resulta interesante es cómo EF Core interactúa con Cosmos DB en segundo plano.

Por ejemplo, a continuación tiene un método para crear un único objeto Consortium, adjuntarlo al contexto y llamar a SaveChanges:

private static void AddObject () {
  var consortium = new Consortium { ConsortiumId = Guid.NewGuid (),
    Name = "Martian Congressional Republic" };
  using (var context = new ExpanseContext ()) {
    context.Consortia.Add (consortium);
    context.SaveChanges ();
  }
}

El código es el mismo que escribiría para un proveedor de bases de datos relacionales. No obstante, en el método SaveChanges subyacente, EF Core transformará ese objeto en un objeto JSON y los datos JSON serán los que se almacenarán en la base de datos.

Pero antes de enviarlo a Cosmos DB, EF Core agrega dos propiedades especiales al objeto JSON. Una es una propiedad llamada "id" cuyo valor es un GUID. Dicho valor "id" garantizará que cada elemento del contenedor tenga un identificador realmente único, aunque represente entidades diferentes. La otra propiedad agregada se llama Discriminator y contiene el nombre del tipo de entidad que representan estos datos. Este valor "discriminator" permite a EF Core distinguir entre los tipos de entidad que representan los elementos. EF Core puede crear estas propiedades adicionales, conocidas solo por DbContext, gracias a su característica de propiedades reemplazadas (bit.ly/2PjUq9k). Esto también significa que, cuando consulta datos, las propiedades reemplazadas se devolverán a la clase DbContext, pero se ignorarán al materializar la entidad.

La propiedad reemplazada "id" no satisface el requisito de EF Core en el que cada entidad debe tener una propiedad clave. Las convenciones son las mismas que siempre. EF Core aún busca una propiedad clave denominada "Id" o "[entidad]Id" y, en mi caso, esa es la propiedad ConsortiumId. Y siempre puede invalidar esa convención con asignaciones.

Si trabaja con bases de datos relacionales, es posible que esté acostumbrado a confiar en el hecho de que le pueden generar los valores clave automáticamente. Cosmos DB no es una base de datos relacional y no rellenará el valor ConsortiumId ni las otras claves. Sin embargo, EF Core proporcionará valores para las claves que falten que sean GUID. El problema es que no cuenta con ningún generador de claves para enteros y no es nada divertido tener que realizar un seguimiento de los enteros incrementales. Por lo tanto, si planea proporcionar las claves o permitir que lo haga EF Core, le recomiendo que utilice GUID. Mi método AddObject que crea el valor ConsortiumId hace esto, aunque no tengo ningún caso de uso real para llevarlo a cabo, ya que no uso el valor en ningún otro sitio. Es solo para la demostración. Si no hubiera proporcionado ese valor al crear una instancia de Consortium, EF Core lo habría hecho por mí. Tenga en cuenta que, si usa el método HasData para propagar datos, al igual que con los otros proveedores, deberá especificar los valores de las propiedades de clave principal y externa.

En la Figura 3 se muestra el elemento que se almacenó en el contenedor como resultado del método AddObject. Las primeras cuatro propiedades provenían de EF Core. Pero, ¿qué hay de las demás? Cosmos DB siempre agrega una serie de propiedades de metadatos que usa en segundo plano. Sin embargo, cuando consulta esos datos con EF Core, no se devuelve ninguna de esas propiedades de metadatos a la clase DbContext de EF Core.

Elemento creado por la nueva entidad Consortium representada en la extensión de Cosmos DB
Figura 3 Elemento creado por la nueva entidad Consortium representada en la extensión de Cosmos DB

¿Qué sucede con los gráficos de datos?

Una entidad Consortium puede tener una o varias entidades Ship. Si creo una nueva entidad Consortium con una entidad Ship, el aspecto de mi código podría ser el siguiente:

var consortium=new Consortium{ConsortiumId= Guid.NewGuid(),
  ConsortiumName="United Nations"};
consortium.Ships.Add(
  new Ship{ShipId=Guid.NewGuid(),ShipName="Canterbury"});

Después de agregar el gráfico de Consortium al contexto y llamar a SaveChanges, se agregarán dos nuevos elementos al contenedor, tal y como se muestra en la Figura 4.

Figura 4 Elementos creados según un nuevo gráfico de Consortium que contiene una entidad Ship

{
  "ConsortiumId": "09bf2c04-e951-41d7-b890-ea5bc27b5766",
  "ConsortiumName": "United Nations Thursay",
  "Discriminator": "Consortium",
  "id": "fa479b49-144f-47ee-9761-e4f6dfe94cb2",
  "_rid": "Q0wDAKsiftgBAAAAAAAAAA==",
  "_self": "dbs/Q0wDAA==/colls/Q0wDAKsiftg=/docs/Q0wDAKsiftgBAAAAAAAAAA==/",
  "_etag": "\"000058c7-0000-0000-0000-5bf80dcf0000\"",
  "_attachments": "attachments/",
  "_ts": 1542983119
}
{
  "ShipId": "581a5c65-8df7-4479-8626-9d8fd2b1c4c7",
  "ConsortiumId": "09bf2c04-e951-41d7-b890-ea5bc27b5766",
  "Discriminator": "Ship",
  "PlanetId": 0,
  "ShipName": "Canterbury 3rd",
  "id": "ebc2dcda-efb5-451b-a65d-f6fa0bb011a4",
  "Origin": null,
  "_rid": "Q0wDAKsiftgCAAAAAAAAAA==",
  "_self": "dbs/Q0wDAA==/colls/Q0wDAKsiftg=/docs/Q0wDAKsiftgCAAAAAAAAAA==/",
  "_etag": "\"000059c7-0000-0000-0000-5bf80dd00000\"",
  "_attachments": "attachments/",
  "_ts": 1542983120
}

Tenga en cuenta que, aunque no definí ninguna propiedad de clave externa de ConsortiumId en la clase Ship, EF Core sabe que será necesaria para realizar un seguimiento de la relación, por lo que hizo lo que siempre se hace cuando se aplica el valor de la clave externa: usar sus conocimientos respecto al hecho de que el objeto Ship pertenece al objeto Consortium. En función de mi lógica de negocios, a menudo uso y controlo las propiedades de la clave externa en mis tipos relacionados. Pero en caso de que no lo haga, es interesante ver que el proveedor se encargará de eso por usted.

No obstante, EF Core no se encarga de crear un solo documento JSON con una entidad Ship como subdocumento de la entidad Consortium. Esto se debe a que ambos tipos tienen propiedades clave y son entidades verdaderas en el modelo de datos de mi entidad, tal como se define en ExpanseContext, por lo que siempre se representarán como documentos individuales.

Sin embargo, EF Core comprende el concepto de objetos JSON jerárquicos y puede ver esto en acción cuando usa entidades en propiedad en su modelo. Examinaré con más detalle las entidades en propiedad con este proveedor en el segundo artículo.

Recuperar datos de Cosmos DB

En esta columna, trataré un tema más y haré un vistazo rápido a la recuperación de datos.

Para recuperar datos, simplemente puede escribir consultas LINQ como con cualquier otro proveedor. EF Core usará Cosmos DB SQL API para transformar las consultas LINQ y obtener los elementos.

Y, tal y como hace EF Core con otros proveedores, cualquier propiedad reemplazada que defina usted explícitamente o el proveedor de forma implícita (por ejemplo, las propiedades "Discriminator" e "id"), siempre se devolverá al contexto como parte de la entrada. Eso permite a EF Core materializar los tipos de objeto correctos y mantener las identidades individuales de cada objeto.

Para verlo, consulte las entradas de ChangeTracker después de consultar datos de la base de datos como lo he hecho aquí:

private static void GetSomeDataBack () {
  using (var context = new ExpanseContext ()) {
    var consortia = context.Consortia.ToList ();
    var entries = context.ChangeTracker.Entries ().ToList ();
  }
}

Al profundizar en las propiedades de una de las entradas, puede ver que hay cinco propiedades, aunque la entidad Consortium solo tiene dos, ConsortiumId y Name. Observe que incluso hay metadatos que indican que son propiedades reemplazadas. Una propiedad reemplazada final es __jObject, que contiene la representación de cadena del objeto JSON completo de la entrada.

Consultar datos relacionados

Puede cargar datos relacionados mediante la carga diligente con Include o proyecciones, así como mediante la carga explícita. Probé la carga diferida basada en proxy, que no devolvió los datos relacionados, y me han comentado que la carga diferida basada en inyección de dependencia tampoco es funcional en este momento. 

En los tres métodos de la Figura 5 se muestran ejemplos prácticos de la carga de entidades Ship relacionadas mediante Include, una proyección y una carga explícita, junto con un poco de filtrado. El código no es distinto a si estuviera haciendo una consulta en un proveedor de base de datos relacional.

Figura 5 La carga de datos relacionados funciona igual que con los proveedores de RDBMS

private static void EagerLoadInclude () {
  using (var context = new ExpanseContext ()) {
    var consortia = context.Consortia.Include (c => c.Ships).ToList ();
  }
}
private static void EagerLoadProjection () {
  using (var context = new ExpanseContext ()) {
    var consortia =
      context.Consortia.Select (c => new { c, c.Ships }).ToList ();
  }
}
private static void ExplicitLoad () {
  using (var context = new ExpanseContext ()) {
    var consortium =
      context.Consortia.FirstOrDefault (c => c.Name.Contains ("United"));
    context.Entry (consortium).Container (c => c.Ships).Load ();
  }
}

Trabajar con Cosmos DB mediante los conocimientos de EF Core que ya tiene

Aunque Cosmos DB es un tipo de almacén de datos completamente diferente (una base de datos de documentos que almacena documentos JSON), y no se parece en nada a una base de datos relacional, puede aprovechar el conocimiento que tiene de EF Core para trabajar con él a fin de almacenar y recuperar datos. No obstante, al igual que con cualquier base de datos, ya sea relacional o no, aún deberá trabajar fuera de EF Core para asegurarse de que usa la base de datos de manera eficiente y rentable.

En la segunda parte de este artículo, examinaré algunas características más avanzadas, como la configuración de contenedores y particiones, la integración de entidades en propiedad en la combinación y el uso del registro para modificar algunos de los SQL generados desde la API. Y tal vez para entonces, habrá una nueva versión de la versión preliminar disponible para unirla con la funcionalidad adicional. Si quiere permanecer atento al progreso del proveedor, hay una extensa "lista de resultados" de características con las que trabajar y para tener en cuenta en el repositorio de GitHub de EF Core en bit.ly/2rmUpYN.


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 juliel.me/PS-Videos.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: Andriy Svyryd y Diego Vega