Modelos con Entity Framework Core
Introducción
Los desarrolladores siempre han necesitado leer datos de una base de datos, escribir registros, eliminar filas, gestionar transacciones, actualizar datos de usuario, etc. Desde sus inicios, .NET Framework ha contado con una API para comunicarse con una base de datos y enviarle consultas. Sin embargo, esto puede convertirse rápidamente en algo tedioso e imposible de mantener. Los errores tipográficos son frecuentes y gestionar todas las consultas SQL puede convertirse rápidamente en algo inmanejable.
Desde hace varios años, Microsoft desarrolla Entity Framework, un paquete de software que facilita el acceso a una base de datos. Este framework es un ORM (Object-Relational Mapping), es decir, una herramienta que permite trabajar con objetos C# en lugar de con consultas en duro. El desarrollador gestiona, consulta, añade o elimina objetos a través del ORM, que traduce automáticamente las operaciones en consultas SQL.
La última versión de Entity Framework, la versión Core, es una reescritura completa de las versiones anteriores. Esta elección se hizo, en primer lugar, para mejorar el rendimiento de procesamiento del ORM, que no siempre está optimizado. En segundo lugar, para los equipos de Microsoft era importante que el framework fuera más modular, más abierto y más fácil de mantener y, en tercer lugar, para acercar el modelo de objetos...
Los distintos proveedores de bases de datos
En primer lugar, antes de utilizar un ORM, es importante saber qué tipo de base de datos admite. Como Entity Framework Core aún está en pañales, de momento (abril de 2016) solo admite bases de datos SQL Server y SQLite. También admite las denominadas bases de datos in-memory, es decir, bases de datos almacenadas en memoria, pero este tipo de base de datos se utiliza más con fines de prueba. El objetivo del proyecto es cubrir el mayor número posible de bases de datos. Los espacios de nombres más importantes para los providers de bases de datos son:
-
Microsoft.EntityFrameworkCore.SqlServer: conjunto de clases para gestionar el acceso a una base de datos MySQL.
-
Microsoft.EntityFrameworkCore.SqlServer.Design: clases de utilidad para SQL Server.
-
Microsoft.EntityFrameworkCore.Sqlite: conjunto de clases para gestionar una base de datos SQLite.
-
Microsoft.EntityFrameworkCore.Sqlite.Designer: clases de utilidad para SQLite.
-
Microsoft.EntityFrameworkCore.InMemory: conjunto de clases para gestionar una base de datos en memoria.
El acceso a las bases de datos se ha vuelto mucho más modular que antes, lo que permite integrar en las aplicaciones web sólo lo que el desarrollador necesita. Además, con una arquitectura desacoplada, será mucho más fácil añadir nuevos proveedores para otros tipos de bases de datos (MySQL, Oracle, etc.), lo que resultaba mucho más complicado en versiones...
Migraciones
Antes de hablar de la migración, es importante entender por qué Entity Framework Core ya no soporta los Database Project y los EDMX. Este tipo de proyectos permitía construir un modelo relacional, en Visual Studio, según las necesidades del proyecto. A continuación, este modelo se aplicaba a la base de datos y, al mismo tiempo, las clases de C# que componían los modelos se generaban y el desarrollador podía utilizarlas inmediatamente.
Sobre el papel, este proceso parece beneficioso para la limpieza del proyecto, pero no lo es. Incluso llega a ser contraproducente en el contexto de un proyecto muy grande (cuando se llega al centenar de modelos). De hecho, este tipo de proceso utiliza un archivo XML como referencia para gestionar el modelo de base de datos y su uso se vuelve problemático cuando lo comparte un equipo de desarrollo, ya que las modificaciones se hacen demasiado engorrosas de gestionar para el gestor del código fuente. Por tanto, es probable que el proyecto pierda información sobre el modelo de datos, lo que no es aceptable en un contexto profesional.
La solución elegida por el equipo de Microsoft es simplemente dejar de soportar este tipo de proyectos. En su lugar, los expertos del equipo recomiendan el uso del enfoque Code First. Ya integrado en el framework desde la versión 4.1, este enfoque se centra en las clases C# que los desarrolladores habrán creado en el proyecto. El objetivo de estas clases es representar la base de datos como un modelo de objetos que el ORM pueda entender, que podrá interpretar automáticamente las correspondencias, las claves extranjeras, etc.
También es posible utilizar atributos para ayudar al ORM a manejarse mejor o especificar mappings muy concretos, pero no es el tema de esta sección.
La ventaja de este enfoque es que es fácil de implementar y mantener: su proyecto sólo contiene clases de C#, y nada más. El ORM se encarga del resto. Este enfoque es muy apreciado por los desarrolladores, que suelen estar más acostumbrados a manejar un modelo de objetos, en lugar de un modelo de base de datos. Sin embargo, queda una duda: ¿cómo aplicar los cambios en el modelo a la base de datos? La respuesta está en las migraciones.
Las migraciones de Entity Framework Code First han sufrido cambios colosales. Aunque los conceptos básicos...
El API Fluent
El enfoque "Code First" es una forma excelente de diseñar un modelo de datos que sea tanto mantenible como flexible. Sin embargo, en un proyecto puede haber casos en los que la asignación deba ser explícita en lugar de implícita.
Entity Framework Core aplica un mapeo básico por convención, es decir, una entidad Blog creará una tabla "Blog" en la base de datos. Lo mismo se aplica a las propiedades: un campo "Nombre" creará una columna Nombre, y así sucesivamente. La ventaja es que el desarrollador puede construir el mapping entre un modelo y una base de datos muy rápidamente.
Hay dos formas de declarar una asignación específica:
-
las anotaciones,
-
el API Fluent.
En este capítulo sólo nos centraremos en el API Fluent, ya que es la parte de Entity Framework que ha sufrido más cambios.
El API Fluent permite codificar las relaciones entre entidades. Esto significa que el desarrollador utilizará métodos de extensión, proporcionados por el framework, para mapear el modelo a la base de datos y definir las claves primarias, claves foráneas, etc. El API se encuentra en el espacio de nombres EntityFrameworkCore.Relational.
Tomemos un ejemplo sencillo que consiste en mapear una entidad a una tabla que no tiene el mismo nombre. En primer lugar, hay que saber que el mapping se realiza en un DbContext y, concretamente, en el método OnModelCreating. Este método recibe un objeto de tipo ModelBuilder, y es este objeto el que le permitirá actuar sobre el mapping.
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable("blogs");
}
}
public class Blog ...
Integración de Entity Framework Core
Entity Framework Core ofrece a los desarrolladores un sistema de configuración avanzado que les permite personalizar su DbContext de forma extensible. De hecho, son los distintos providers los que añaden sus propias opciones al sistema de configuración básico. En el ejemplo de SQLite, es el namespace Microsoft.EntityFrameworkCore.Sqlite el que añadirá el método UseSqlite para configurar el acceso a la base de datos SQLite.
services.AddEntityFramework()
.AddSqlite()
.AddDbContext<BloggingContext>(options =>
options.UseSqlite("Filename=./blog.db"));
Este método se llama en la clase Startup, que contendrá toda la configuración del contexto de acceso a la base de datos para cada llamada. A continuación, el método AddDbContext simplemente registra el DbContext como un servicio que se puede utilizar en toda la aplicación, a través de la inyección de dependencia.
Los controladores son un buen ejemplo de cómo se puede utilizar DbContext. La inyección de dependencia permite inyectar este servicio en el constructor, haciendo que el acceso a la base de datos esté disponible en todas las acciones del controlador.
public MyController(BloggingContext context)
Esta técnica es útil en la mayoría de los casos, pero no es realmente una buena práctica. Inyectar el DbContext directamente en el controlador no lo hace realmente verificable y mantener el código de negocio dentro del controlador, no es una buena solución. La mejor solución es exportar el DbContext en clases de ’servicio’ inyectables y el código de negocio que las acompaña, incrementando así la verificabilidad de su código.
La verificabilidad de un DbContext también es objeto de debate...
Validación de los modelos
Cuando un usuario manipula la aplicación web para crear o modificar un registro, el formulario utilizado le indicará cierta información de formato, para garantizar el correcto tratamiento de los datos creados o modificados. Hay muchos ejemplos de esto:
-
formatear correctamente un correo electrónico o una fecha,
-
el carácter obligatorio de los datos,
-
la aplicación de determinadas restricciones de tamaño (para una cadena de caracteres), complejidad (para una contraseña) y muchas otras.
Estas restricciones se deben respetar para evitar posibles fallos de seguridad y comprobar que los datos cumplen ciertas reglas, antes de ser insertados en la base de datos. Con un modelo MVC, la validación se puede hacer tanto del lado del cliente (con jQuery, por ejemplo) como del lado del servidor (con C#).
El framework .NET ha abstraído todos estos mecanismos de validación mediante atributos de validación. Éstos se aplican a los modelos de objetos C# de la aplicación, incrustando la lógica de negocio para reducir la duplicación de código y se pueden interpretar tanto en el lado del cliente como en el del servidor, para validar los datos. Las restricciones pueden variar desde simples reglas de campos obligatorios hasta patrones de validación más complejos, como para tarjetas de crédito, números de teléfono o correos electrónicos.
El siguiente ejemplo muestra la aplicación de algunos de estos atributos del espacio de nombres System.ComponentModel.DataAnnotations.
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Title { get; set; }
[Required]
[Range(0, 999.99)]
public float Price { get; set; }
}
Los atributos son muy significativos y proporcionan un modelo...
Patterns et antipatterns
Para utilizar Entity Framework correctamente, es importante entender cómo funciona el ORM, pero también utilizarlo correctamente. Hay muchas formas de configurar Entity Framework dentro de un proyecto ASP.NET Core y en esta sección, explicaremos algunas buenas y malas prácticas para ayudar a los desarrolladores en su trabajo diario.
En primer lugar, DbContext no es thread safe. En el contexto de una aplicación web ASP.NET Core, esto significa que el DbContext no debe ser compartido entre varias peticiones HTTP, ya que una petición equivale a tener un nuevo thread. Básicamente, vamos a favorecer un servicio DbContext en modo Scoped tanto como sea posible, no un singleton. El modo transient es aceptable. De hecho, cuando el desarrollador escribe AddDbContext, esto es exactamente lo que hace el framework: añade el DbContext como Scoped (ver capítulo Nuevos mecanismos en ASP NET Core, sección Inyección de dependencia, para más detalles sobre los ciclos de vida Scoped y Transient). Con esta buena práctica, el desarrollador se asegura de que sólo hay un DbContext por thread y no necesita preocuparse de gestionar el ciclo de vida de este objeto.
Dentro del ecosistema de Entity Framework, a menudo oímos hablar del pattern Repository y del pattern Unit of Work. Con respecto al primero, ya hemos demostrado en este capítulo cómo implementar un repositorio abstracto, flexible, reutilizable y altamente verificable.
Le recomiendo que utilice este tipo de pattern, que facilita la abstracción de las relaciones entre su código de negocio y el acceso a los datos. Para las pruebas, siempre es más fácil implementar un repositorio en memoria a través de su propia abstracción (el repository) que configurar una base de datos en memoria, a través de Entity Framework. La abstracción aquí es la clave para cambiar de un modo a otro, dependiendo de la situación.
El concepto en el que se basa el pattern Unit of Work es sencillo: sincronizar y gestionar el ciclo de vida de los objetos de acceso a la base de datos, a la vez que se proporciona un acceso abstracto y seguro a las transacciones creadas por el DbContext,que, a menudo, es compartido por varios repositorios dentro de una misma Unit of Work.
public...