Compartir a través de


Entity Framework

Code First en ADO.NET Entity Framework 4.1

Rowan Miller

ADO.NET Entity Framework 4.1 se lanzó en abril e incluye una gama de nuevas características creadas a partir de la funcionalidad de Entity Framework 4 existente que se lanzó en Microsoft .NET Framework 4 y Visual Studio 2010.

Entity Framework 4.1 está disponible como un instalador independiente (msdn.microsoft.com/data/ee712906), como el paquete NuGet “EntityFramework” y también está incluido en la instalación de ASP.NET MVC 3.01.

Entity Framework 4.1 incluye dos características principales nuevas: la API de DbContext y Code First. En este artículo, trataré de abarcar cómo estas dos características se pueden usar para desarrollar aplicaciones. Daremos un rápido vistazo a la introducción a Code First y después profundizaremos en algunas de sus capacidades más avanzadas.

La API de DbContext es una abstracción simplificada del tipo ObjectContext, además de otros tipos que se incluyeron en versiones anteriores de Entity Framework. La superficie de la API de DbContext está optimizada para las tareas comunes y los patrones de código. La funcionalidad común se muestra en el nivel raíz y se presentan funcionalidades más avanzadas a medida que se profundiza en la API.

Code First es un nuevo patrón de desarrollo para Entity Framework que proporciona una alternativa a los patrones Database First y Model First existentes. Code First le permite definir su modelo mediante clases CLR. Después esas clases se pueden asignar a una base de datos existente o se pueden usar para generar un esquema de base de datos. Se puede realizar una configuración más detallada usando anotaciones de datos o mediante una API fluida.

Introducción

Code First lleva algo de tiempo en circulación, de manera que no entraré en detalles en la introducción al programa. Puede completar el tutorial de Code First (bit.ly/evXlOc) si no está familiarizado con sus aspectos básicos. La figura 1 es una lista de código completa que le ayudará a poner en marcha una aplicación de Code First.

Figura 1 Introducción a Code First

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System;

namespace Blogging
{
  class Program
  {
    static void Main(string[] args)
    {
      Database.SetInitializer<BlogContext>(new BlogInitializer());

      // TODO: Make this program do something!
    }
  }

  public class BlogContext : DbContext
  {
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
      // TODO: Perform any fluent API configuration here!
    }
  }

  public class Blog
  {
    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Abstract { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
  }

  public class RssEnabledBlog : Blog
  {
    public string RssFeed { get; set; }
  }

  public class Post
  {
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public byte[] Photo { get; set; }

    public virtual Blog Blog { get; set; }
  }

  public class BlogInitializer : DropCreateDatabaseIfModelChanges<BlogContext>
  {
    protected override void Seed(BlogContext context)
    {
      context.Blogs.Add(new RssEnabledBlog
      {
        Name = "blogs.msdn.com/data",
        RssFeed = "https://blogs.msdn.com/b/data/rss.aspx",
        Posts = new List<Post>
        {
          new Post { Title = "Introducing EF4.1" },
          new Post { Title = "Code First with EF4.1" },
        }
      });

      context.Blogs.Add(new Blog { Name = "romiller.com" });
      context.SaveChanges();
    }
  }
}

En pos de la simpleza, escogeré que Code First genere una base de datos. La primera vez que use BlogContext para hacer persistir y consultar datos se creará la base de datos. El resto del artículo también se aplicará a los casos en los que Code First se asigne a un esquema de base de datos existente. Podrá observar que estoy usando un inicializador de base de datos para anular y recrear la base de datos a medida que vayamos cambiando el modelo a lo largo del desarrollo del artículo.

Asignación con la API fluida

Code First empezará por examinar sus clases CLR para inferir la forma del modelo. Se usa una serie de convenciones para detectar elementos como las claves primarias. Puede invalidar o agregar a lo que se detectó con convenciones mediante anotaciones de datos o una API fluida. Existen varios artículos que describen cómo realizar tareas comunes mediante la API fluida, de manera que examinaré parte de la configuración más avanzada que se puede realizar. En específico, me enfocaré en las secciones de “asignación” de la API. Una configuración de asignación se puede usar para asignar un esquema de base de datos existente o para afectar la forma de un esquema generado. La API fluida se expone mediante el tipo DbModelBuilder y se puede obtener acceso a él más fácilmente al invalidar el método OnModelCreating en DbContext.

Separación de entidad: la separación de entidad permite que las propiedades de un tipo de entidad se distribuyan entre varias tablas. Por ejemplo, digamos que deseo separar los datos de fotografías para entradas en una tabla separada de manera que se almacenen en un grupo de archivos distinto. La separación de entidades usa varias llamadas a Map para asignar un subconjunto de propiedades a una tabla específica. En la figura 2, asignaré la propiedad Photo a la tabla “PostPhotos” y las propiedades restantes a la tabla “Posts”. Podrá observar que no incluí la clave primaria en la lista de propiedades. La clave primaria siempre es necesaria en todas las tablas, podría haberla incluido, pero Code First la agregará por mí de manera automática.

Figura 2 Separación de entidad

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Post>()
    .Map(m =>
      {
        m.Properties(p => new { p.Title, p.Content });
        m.ToTable("Posts");
      })
    .Map(m =>
      {
        m.Properties(p => new { p.Photo });
        m.ToTable("PostPhotos");
      });
}

Herencia de tabla por jerarquía (TPH): TPH constituye el almacenamiento de datos para una jerarquía de herencia en una sola tabla donde se usa una columna discriminadora para identificar el tipo de cada fila. Code First usará TPH de manera predeterminada si no se proporciona una configuración. La columna de discriminador se denominará “Discriminator” y se usará el nombre de tipo de CLR de cada tipo en los valores del discriminador.

Sin embargo, puede que desee personalizar cómo se realiza la asignación de TPH. Para hacer esto, puede usar el método Map para configurar los valores de columna de discriminador para el tipo base y después Map<TEntityType> para configurar cada tipo derivado. En este caso estoy usando una columna denominada “HasRssFeed” para almacenar un valor true/false que distinga entre las instancias “Blog” y “RssEnabledBlog”:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m => m.Requires("HasRssFeed").HasValue(false))
    .Map<RssEnabledBlog>(m => m.Requires("HasRssFeed").HasValue(true));
}

En el ejemplo anterior sigo usando una columna independiente para distinguir entre tipos, pero sé que RssEnabledBlogs se puede identificar por el hecho de que tiene una fuente RSS. Puedo volver a escribir la asignación para hacer que Entity Framework identifique que debe usar la columna que almacena “Blog.RssFeed” para distinguir entre tipos. Si la columna tiene un valor que no es nulo, debe ser RssEnabledBlog:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map<RssEnabledBlog>(m => m.Requires(b => b.RssFeed).HasValue());
}

Herencia de tabla por tipo (TPT): TPT constituye el almacenamiento de todas las propiedades del tipo base en una sola tabla. Cualquier propiedad adicional para tipos derivados se almacena en tablas separadas con una clave externa en la tabla base. La asignación de TPT usa una llamada Map para especificar el nombre de tabla base y después usa Map<TEntityType> para configurar la tabla para cada tipo derivado. En el siguiente ejemplo, almacenaré datos que son comunes a todos los blogs en la tabla “Blogs” y datos específicos para los blogs que tienen RSS habilitado en la tabla “RssBlogs”:

modelBuilder.Entity<Blog>()
  .Map(m => m.ToTable("Blogs"))
  .Map<RssEnabledBlog>(m => m.ToTable("RssBlogs"));

Herencia de tabla por tipo concreto (TPC): TPC constituye el almacenamiento de datos para cada tipo en una tabla completamente separada sin limitaciones por claves externas entre ellas. La configuración es similar a la que se realiza en la asignación TPT, con la excepción de que se incluye una llamada “MapInheritedProperties” al configurar cada tipo derivado. MapInheritedProperties permite que Code First reasigne todas las propiedades heredadas de la clase base a las nuevas columnas en la tabla de la clase derivada:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m => m.ToTable("Blogs"))
    .Map<RssEnabledBlog>(m =>
      {
        m.MapInheritedProperties();
        m.ToTable("RssBlogs");
      });
}

Por convención, Code First usará las columnas de identidad para las claves principales de enteros. Sin embargo, con TPC no hay una tabla que contenga todos los blogs que se pueden usar para generar claves primarias. Debido a esto, Code First desactivará la identidad al usar asignaciones de TPC. Si hará una asignación a una base de datos existente configurada para generar valores únicos entre varias tablas, se pude volver a establecer una identidad mediante la sección de configuración de propiedades de la API fluida.

Asignaciones híbridas Naturalmente, la forma de su esquema no siempre se ajustará a uno de los patrones que revisamos, especialmente si está haciendo asignaciones a una base de datos existente. Lo bueno es que la API de asignación admite composición y que puede combinar varias estrategias de asignación. La figura 3 incluye un ejemplo que muestra la combinación de Entity Splitting con asignación de herencia de TPT. Los datos de Blogs se dividen entre las tablas “Blogs” y “BlogAbstracts” y los datos específicos para los blogs con RSS habilitado se almacenan en una tabla separada denominada “RssBlogs”.

Figura 3 Combinación de separación de entidad con asignación de herencia de TPT

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  modelBuilder.Entity<Blog>()
    .Map(m =>
      {
        m.Properties(b => new { b.Name });
        m.ToTable("Blogs");
      })
    .Map(m =>
      {
        m.Properties(b => new { b.Abstract });
        m.ToTable("BlogAbstracts");
      })
    .Map<RssEnabledBlog>(m =>
      {
         m.ToTable("RssBlogs");
      });
}

API de la herramienta de seguimiento de cambios

Después de haber revisado la configuración de asignaciones de base de datos, quiero dedicarle un poco de tiempo al trabajo con datos. Profundizaré inmediatamente en ciertos escenarios más avanzados; si no está familiarizado con el acceso a datos básicos, tómese un minuto para leer la tutorial de Code First que se mencionó anteriormente.

Estado de información para una entidad única En muchos casos, como en el inicio de sesión, es práctico obtener acceso a la información de estado de una entidad. Esto puede incluir cosas como el estado de la entidad y cuáles propiedades se modificarán. DbContext proporciona acceso a esta información para una entidad individual mediante el método “Entry”. El fragmento de código en la
figura 4 carga un “Blog” de la base de datos, modifica una propiedad y después imprime los valores actuales y originales de cada propiedad a la consola.

Figura 4 Obtención de información de estado de una entidad

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Change the name of one blog
    var blog = db.Blogs.First();
    blog.Name = "ADO.NET Team Blog";

    // Print out original and current value for each property
    var propertyNames = db.Entry(blog).CurrentValues.PropertyNames;
    foreach (var property in propertyNames)
    {
      System.Console.WriteLine(
        "{0}\n Original Value: {1}\n Current Value: {2}", 
        property, 
        db.Entry(blog).OriginalValues[property],
        db.Entry(blog).CurrentValues[property]);
    }
  }

  Console.ReadKey();
}

Al ejecutar el código en la figura 4, la salida de la consola es la siguiente:

BlogId
 Valor original: 1
 Valor actual: 1
 
Nombre
 Valor original: blogs.msdn.com/data
 Valor actual: Blog del equipo de ADO.NET
 
Extracto
 Valor original:
 Valor actual:
 
RssFeed
 Valor original: https://blogs.msdn.com/b/data/rss.aspx
 Valor actual: https://blogs.msdn.com/b/data/rss.aspx

Información de estado para varias entidades DbContext le permite obtener acceso a información sobre entradas múltiples mediante el método “ChangeTracker.Entries”. Existe una sobrecarga genérica que otorga entidades de un tipo específico y una sobrecarga que no es genérica que otorga todas las entidades. El parámetro genérico no necesita ser de un tipo de entidad. Por ejemplo, podría obtener entradas para todos los objetos cargados que implementan una interfaz específica. El código en la figura 5 demuestra la carga de todos los blogs a la memoria, la modificación de uno de ellos y la impresión del estado de cada blog seguido.

Figura 5 Obtención de acceso a información para varias entradas con DbContext

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load all blogs into memory
    db.Blogs.Load();

    // Change the name of one blog
    var blog = db.Blogs.First();
    blog.Name = "ADO.NET Team Blog";

    // Print out state for each blog that is in memory
    foreach (var entry in db.ChangeTracker.Entries<Blog>())
    {
      Console.WriteLine("BlogId: {0}\n State: {1}\n",
        entry.Entity.BlogId,
        entry.State);
    }
  }

Al ejecutar el código en la figura 5, la salida de la consola es la siguiente:

BlogId: 1
  Estado: Modificado
 
BlogId: 2
  Estado: Sin cambios

Consulta de instancias locales Siempre que ejecute una consulta LINQ en DbSet, se enviará la consulta a la base de datos para que sea procesada. Esto garantiza que siempre obtenga resultados completos y actualizados, pero si sabe que todos los datos que necesita ya están en la memoria, puede evitar una vuelta a la base de datos al consultar los datos locales. El código en la figura 6 carga todos los blogs en la memoria y después ejecuta dos consultas de LINQ por blogs que no obtengan aciertos en la base de datos.

Figura 6 Ejecución de consultas de LINQ por datos en memoria

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load all blogs into memory
    db.Blogs.Load();

    // Query for blogs ordered by name
    var orderedBlogs = from b in db.Blogs.Local 
                       orderby b.Name
                       select b;

    Console.WriteLine("All Blogs:");
    foreach (var blog in orderedBlogs)
    {
      Console.WriteLine(" - {0}", blog.Name);
    }

    // Query for all RSS enabled blogs
    var rssBlogs = from b in db.Blogs.Local
                   where b is RssEnabledBlog
                   select b;

    Console.WriteLine("\n Rss Blog Count: {0}", rssBlogs.Count());
  }

  Console.ReadKey();
}

Al ejecutar el código en la figura 6, la salida de la consola es la siguiente:

Todos los blogs:
 - blogs.msdn.com/data
 - romiller.com
 
Recuento de Rss de blogs: 1

Propiedad de navegación como consulta DbContext permite obtener una consulta que representa los contenidos de una propiedad de navegación para una instancia de entidad dada. Esto le permite modelar o filtrar los elementos que desea traer a la memoria y puede evitar traer datos innecesarios.

Por ejemplo, tengo una instancia de blog y quiero saber cuantas entradas tiene. Podría escribir el código que se muestra en la figura 7, pero esto requiere una carga diferida para traer a la memoria todas esas entradas sólo para poder determinar el recuento.

Figura 7 Obtención de un recuento de elementos de base de datos con carga diferida

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load a single blog
    var blog = db.Blogs.First();

    // Print out the number of posts
    Console.WriteLine("Blog {0} has {1} posts.",
      blog.BlogId,
      blog.Posts.Count());
  }

  Console.ReadKey();
}

Eso resulta en la transferencia de muchos datos de la base de datos y el uso de mucha memoria en comparación al resultado de un solo entero, que es lo que en realidad necesito.

Afortunadamente, puedo optimizar el código mediante el método Entry en DbContext para obtener una consulta que representa la colección de entradas vinculadas al blog. Dado que LINQ admite composición, puedo vincularlo al operador “Count” y toda la consulta se inserta en la base de datos, de manera que sólo se devuelve el resultado entero (ver la figura 8).

Figura 8 Uso de DbContext para optimizar el código de consulta y ahorrar recursos

static void Main(string[] args)
{
  Database.SetInitializer<BlogContext>(new BlogInitializer());

  using (var db = new BlogContext())
  {
    // Load a single blog
    var blog = db.Blogs.First();

    // Query for count
    var postCount = db.Entry(blog)
      .Collection(b => b.Posts)
      .Query()
      .Count();

    // Print out the number of posts
    Console.WriteLine("Blog {0} has {1} posts.",
      blog.BlogId,
      postCount);
  }

  Console.ReadKey();
}

Reflexiones sobre la implementación

Hasta ahora he examinado cómo poner en marcha el acceso a datos. Ahora examinemos un poco más allá a algunas cosas que se deben considerar a medida que la aplicación empieza a crecer y se acerca la fecha de lanzamiento.

Cadenas de conexión: Hasta ahora sólo he dejado que Code First genere una base de datos en localhost\SQLEXPRESS. En el momento de implementar la aplicación, probablemente quiera cambiar la base de datos a la cual apunta Code First. El enfoque recomendado para esto es agregar una entrada de cadena de conexión al archivo App.config (o Web.config para aplicaciones web). Este también es el enfoque recomendado para usar Code First con el fin de asignar una base de datos existente. Si el nombre de la cadena de conexión coincide con el nombre de tipo calificado de forma íntegra, Code First lo captará automáticamente en el momento de ejecución. Sin embargo, el enfoque recomendado es usar el constructor DbContext, el cual acepta un nombre de conexión que use la sintaxis nombre=<nombre de cadena de conexión>. Esto asegura que Code First siempre usará el archivo de configuración. Aparecerá una excepción si no se encuentra la entrada de cadena de conexión. El siguiente ejemplo muestra la sección de cadena de conexión que se puede usar para afectar la base de datos que se orienta a nuestra aplicación de muestra:

<connectionStrings>
  <add 
    name="Blogging" 
    providerName="System.Data.SqlClient"
    connectionString="Server=MyServer;Database=Blogging;
    Integrated Security=True;MultipleActiveResultSets=True;" />
</connectionStrings>

Este es el código de contexto actualizado:

public class BlogContext : DbContext
{
  public BlogContext() 
    : base("name=Blogging")
  {}

  public DbSet<Blog> Blogs { get; set; }
  public DbSet<Post> Posts { get; set; }
}

Observe que es recomendable activar “Conjuntos de resultados activos múltiples”. Esto permite que se activen dos consultas al mismo tiempo. Por ejemplo, esto sería necesario para consultar entradas vinculadas a un blog mientras se enumeran todos los blogs.

Inicializadores de base de datos Code First, de forma predeterminada, creará una base de datos automáticamente si la base de datos a la que está orientada no existe. Para algunos, esta será la funcionalidad deseada incluso en la fase de implementación y sólo se creará la base de datos de producción la primera vez que se inicie la aplicación. Si cuenta con una DBA que se preocupa de su entorno de producción, es mucho más probable que la DBA cree la base de datos de producción para usted y, una vez que se implemente su base de datos, debería generar un error si la base de datos a la que está destinada no existe. En este artículo, también invalidé la lógica del inicializador predeterminado y configuré la base de datos para que se anule y se vuelva a crear cada vez que cambie el esquema. Esto es algo que definitivamente no deseará dejar así al implementar la producción.

El enfoque recomendado para cambiar o deshabilitar el comportamiento del inicializador durante la implementación es usar el archivo App.config (o Web.config para aplicaciones web). En la sección appSettings, agregue una entrada cuya clave sea DatabaseInitializerForType, seguido del nombre de tipo de contexto y el ensamblado donde se define. El valor puede ser “Disabled” o el nombre del tipo de inicializador seguido del ensamblado en el que se define.

El siguiente ejemplo deshabilita cualquier lógica de inicializador para el contexto que se usa en este artículo:

<appSettings>
  <add 
    key="DatabaseInitializerForType Blogging.BlogContext, Blogging" 
    value="Disabled" />
</appSettings>

El siguiente ejemplo cambiará el inicializador a sus funciones predeterminadas que crearán la base de datos sólo si ella no existe:

<appSettings>
  <add 
    key="DatabaseInitializerForType Blogging.BlogContext, Blogging" 
    value="System.Data.Entity.CreateDatabaseIfNotExists EntityFramework" />
</appSettings>

Cuentas de usuario Si decide permitir que su aplicación de producción cree la base de datos, se deberá inicializar la aplicación mediante una cuenta que tenga permisos para crear la base de datos y modificar esquemas. De aplicarse dichos permisos, aumentará significativamente el impacto potencial de infracciones a la seguridad de su aplicación. Recomiendo encarecidamente que una aplicación se ejecute con el conjunto mínimo de permisos necesarios para consultar y hacer persistir datos.

Más por aprender

En síntesis, en este artículo, di una mirada rápida a la introducción en el desarrollo de Code First y la nueva API de DbContext, ambas de ellas están incluidas en ADO.NET Entity Framework 4.1. Pudo observar cómo la API fluida se usa para asignar una base de datos existente o para afectar la forma de un esquema de base de datos generado por Code First. Después, di un vistazo a la API de la herramienta de seguimiento de cambios y cómo se puede usar para consultar instancias de entidad local e información adicional respecto a esas instancias. Para terminar, revisé algunas reflexiones sobre la implementación de una aplicación que usa Code First para obtener acceso a datos.

Si le gustaría aprender más sobre cualquiera de las características incluidas en Entity Framework 4.1, visite msdn.com/data/ef. También puede usar el foro del Centro de desarrollo de datos para obtener ayuda acerca del uso de Entity Framework 4.1: bit.ly/166o1Z.

Rowan Miller es administrador de programa del equipo de Entity Framework en Microsoft. Puede obtener más información acerca de Entity Framework en su blog en romiller.com.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Arthur Vickers