Entity Framework

Code First no ADO.NET Entity Framework 4.1

Rowan Miller

O ADO.NET Entity Framework 4.1 foi lançado em abril e inclui uma série de novos recursos que se baseiam na funcionalidade existente do Entity Framework 4 lançada no Microsoft .NET Framework 4 e no Visual Studio 2010.

O Entity Framework 4.1 está disponível como um instalador independente (msdn.microsoft.com/data/ee712906), como o pacote “EntityFramework” NuGet e é também incluído quando você instala o ASP.NET MVC 3.01.

O Entity Framework 4.1 inclui dois novos recursos principais: API DbContext e Code First. Neste artigo, discutirei como esses dois recursos podem ser usados para desenvolver aplicativos. Daremos uma olhada rápida na introdução do Code First e, então, nos aprofundaremos em alguns recursos mais avançados.

A API DbContext é uma abstração simplificada do tipo ObjectContext existente e de vários outros tipos que foram incluídos em versões anteriores do Entity Framework. A superfície da API DbContext é otimizada para tarefas comuns e padrões de codificação. A funcionalidade comum está exposta no nível raiz e a funcionalidade mais avançada está disponível à medida que você faz uma busca detalhada na API.

O Code First é um novo padrão de desenvolvimento do Entity Framework que oferece uma alternativa para os padrões Database First e Model First existentes. O Code First permite definir o seu modelo usando classes CLR; é possível, então, mapear essas classes para um banco de dados existente ou usá-las para gerar um esquema de banco de dados. Configuração adicional pode ser fornecida usando Data Annotations ou por meio de uma API fluente.

Introdução

O Code First já existe há algum tempo; portanto, não entrarei em detalhes na introdução. Você pode completar o Passo a passo do Code First (bit.ly/evXlOc) se ainda não estiver familiarizado com as noções básicas. A Figura 1 é uma listagem completa de código para ajudá-lo a colocar em funcionamento o aplicativo Code First.

Figura 1 Introdução ao 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 = "http://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();
    }
  }
}

Para simplificar, optei por permitir que o Code First gere um banco de dados. O banco de dados será criado na primeira vez em que eu usar BlogContext para manter e consultar dados. O restante deste artigo será aplicado igualmente aos casos em que o Code First é mapeado para um esquema de banco de dados existente. Você notará que estou usando um inicializador de banco de dados para eliminar e recriar o banco de dados à medida que mudarmos o modelo ao longo deste artigo.

Mapeando com a API fluente

O Code First começa a examinar suas classes CLR para inferir a forma do seu modelo. Um conjunto de convenções é usado para detectar algo, como chaves primárias. É possível substituir ou adicionar ao que foi detectado pela convenção usando Data Annotations ou uma API fluente. Há vários artigos sobre como executar tarefas comuns usando a API fluente; portanto, vamos dar uma olhada em algumas das configurações mais avançadas que podem ser executadas. Em especial, vamos nos concentrar nas seções de “mapeamento” da API. Uma configuração de mapeamento pode ser usada para mapear para um esquema de banco de dados existente ou para afetar o formato de um esquema gerado. A API fluente é exposta por meio do tipo DbModelBuilder e é acessada com mais facilidade substituindo o método OnModelCreating no DbContext.

Separação da entidade A separação da entidade permite que as propriedades de um tipo de entidade sejam distribuídas em várias tabelas. Por exemplo, digamos que desejo dividir os dados da foto para postá-los em uma tabela separada para que eles possam ser armazenados em um grupo de arquivos diferente. A separação da entidade usa várias chamadas a Map para mapear um subconjunto de propriedades para uma tabela específica. Na Figura 2, estou mapeando a propriedade Photo para a tabela “PostPhotos” e as propriedades restantes para a tabela “Posts”. Você notará que não incluí a chave primária na lista de propriedades. A chave primária é sempre necessária em toda tabela; eu poderia tê-la incluído, mas o Code First a adicionará para mim automaticamente.

Figura 2 Separação da entidade

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

Herança de tabela por hierarquia (TPH) A TPH envolve o armazenamento de dados em uma hierarquia de herança em uma única tabela e o uso de uma coluna discriminadora para identificar o tipo de cada linha. O Code First usará TPH por padrão se nenhuma configuração for fornecida. A coluna discriminadora será apropriadamente denominada “Discriminadora” e o nome do tipo CLR de cada tipo será usado para os valores discriminadores.

Talvez você queira, no entanto, personalizar como o mapeamento TPH será executado. Para fazer isso, use o método Map para configurar os valores da coluna discriminadora para o tipo base e, em seguida, Map<TEntityType> para configurar cada tipo derivado. Aqui, estou usando uma coluna “HasRssFeed” para armazenar um valor true/false para distinguir entre as instâncias “Blog” e “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));
}

No exemplo anterior, eu ainda usava uma coluna independente para distinguir entre os tipos, mas sei que RssEnabledBlogs pode ser identificado pelo fato de que eles possuem um RSS. Posso reescrever o mapeamento para permitir que o Entity Framework saiba que ele deve usar a coluna que armazena “Blog.RssFeed” para distinguir entre os tipos. Se a coluna tiver um valor não nulo, ela deverá ser um RssEnabledBlog:

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

Herança de tabela por tipo (TPT) O TPT envolve o armazenamento de todas as propriedades do tipo base em uma única tabela. Todas as propriedades adicionais de tipos derivados são, então, armazenadas em tabelas separadas com uma chave estrangeira novamente na tabela base. O mapeamento TPT usa uma chamada a Map para especificar o nome da tabela base e, em seguida, Map<TEntityType> para configurar a tabela para cada tipo derivado. No exemplo a seguir, estou armazenando dados que são comuns a todos os blogs da tabela “Blogs” e dados específicos dos blogs habilitados para RSS na tabela “RssBlogs”:

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

Herança de tabela por tipo concreto (TPC) O TPC envolve o armazenamento de dados para cada tipo em uma tabela completamente separada sem nenhuma restrição de chave estrangeira entre eles. A configuração é semelhante ao mapeamento TPT, exceto que inclui uma chamada a “MapInheritedProperties” ao configurar cada tipo derivado. MapInheritedProperties permite que o Code First consiga remapear todas as propriedades que foram herdadas da classe base para as novas colunas da tabela da classe derivada:

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

Por convenção, o Code First usará colunas de identidade para chaves primárias de inteiros. Entretanto, com o TPC não há mais uma tabela única que contenha todos os blogs que podem ser usados para gerar chaves primárias. Por esse motivo, o Code First desativará a identidade quando você usar o mapeamento TPC. Se estiver mapeando para um banco de dados existente que foi configurado para gerar valores exclusivos em várias tabelas, poderá reativar a identidade por meio da seção de configuração de propriedade da API fluente.

Mapeamentos híbridos É claro que a forma do seu esquema nem sempre estará em conformidade com um dos padrões abordados, especialmente se estiver mapeando para um banco de dados existente. A boa notícia é que a API do mapeamento é composta, e será possível combinar várias estratégias de mapeamento. A Figura 3 inclui um exemplo que mostra a combinação de Separação da entidade com Mapeamento de herança TPT. Os dados dos Blogs são divididos entre tabelas “Blogs” e “BlogAbstracts”, e os dados específicos para blogs habilitados para RSS são armazenados em uma tabela “RssBlogs” separada.

Figura 3 Combinando Separação da entidade com Mapeamento de herança 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 do rastreador de alterações

Agora que dei uma olhada na configuração de mapeamentos de bancos de dados, quero dedicar algum tempo para trabalhar com dados. Vou me aprofundar em alguns cenários mais avançados; se você não estiver familiarizado com acesso básico a dados, reserve alguns minutos para ler todo o Passo a passo do Code First, mencionado anteriormente.

Informações de estado para uma única entidade Em muitos casos, como no registro em log, é útil obter acesso às informações de estado de uma entidade. Isso pode incluir algo como o estado da entidade e quais propriedades são modificadas. DbContext fornece acesso a essas informações para uma entidade individual por meio do método “Entry”. O trecho de código na Figura 4 carrega um “Blog” a partir do banco de dados, modifica uma propriedade e, em seguida, imprime os valores atual e original de cada propriedade no console.

Figura 4 Obtendo informações de estado para uma entidade

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

Quando o código na Figura 4 for executado, a saída do console será a seguinte:

BlogId
 Valor original: 1
 Valor atual: 1
 
Name
 Valor original: blogs.msdn.com/data
 Valor atual: Blog da equipe do ADO.NET
 
Resumo
 Valor original:
 Valor atual:
 
RssFeed
 Valor original: http://blogs.msdn.com/b/data/rss.aspx
 Valor atual: http://blogs.msdn.com/b/data/rss.aspx

Informações de estado para várias entidades DbContext permite acessar informações sobre várias entidades por meio do método “ChangeTracker.Entries”. Há uma sobrecarga genérica que fornece entidades de um tipo específico e uma sobrecarga não genérica que fornece todas as entidades. O parâmetro genérico não precisa ser um tipo de entidade. Por exemplo, você poderia obter entradas para todos os objetos carregados que implementam uma interface específica. O código na Figura 5 demonstra o carregamento de todos os blogs na memória, a modificação de uma propriedade em um deles e a impressão do estado de cada blog rastreado.

Figura 5 Acessando informações para várias entidades com 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);
    }
  }

Quando o código na Figura 5 for executado, a saída do console será a seguinte:

BlogId: 1
  Estado: Modificado
 
BlogId: 2
  Estado: Inalterado

Consultando instâncias locais Sempre que você executar uma consulta LINQ a DbSet, ela será enviada ao banco de dados para ser processada. Isso garante que você sempre obterá resultados completos e atualizados, mas se souber que todos os dados necessários já estão na memória, poderá evitar uma viagem de ida e volta ao banco de dados consultando os dados locais. O código na Figura 6 carrega todos os blogs na memória e, em seguida, executa duas consultas LINQ nos blogs que não atingem o banco de dados.

Figura 6 Executando consultas LINQ a dados na memória

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

Quando o código na Figura 6 for executado, a saída do console será a seguinte:

Todos os blogs:
 - blogs.msdn.com/data
 - romiller.com
 
Contagem de blogs Rss: 1

Propriedade de navegação como uma consulta O DbContext permite obter uma consulta que representa o conteúdo de uma propriedade de navegação para uma determinada instância da entidade. Isso permite moldar ou filtrar os itens que deseja inserir na memória e pode evitar o retorno de dados desnecessários.

Por exemplo, tenho uma instância do blog e desejo saber quantas postagens ela possui. Eu poderia gravar o código mostrado na Figura 7, mas ela está dependendo do carregamento lento para retornar todas as postagens relacionadas na memória para que eu possa saber a contagem.

Figura 7 Obtendo uma contagem de itens do banco de dados com carregamento lento

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

Isso significa uma grande quantidade de dados sendo transferida do banco de dados e sendo inserida na memória em comparação com o único resultado inteiro de que realmente necessito.

Felizmente, consigo otimizar o meu código usando o método Entry no DbContext para obter uma consulta que representa a coleção de postagens associada ao blog. Como o LINQ é composto, posso encadear no operador “Count” e a consulta inteira é enviada para o banco de dados para que esse único resultado inteiro seja retornado (consulte a Figura 8).

Figura 8 Usando DbContext para otimizar o código de consulta e os recursos de salvamento

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

Considerações quanto à implantação

Até agora, vimos como começar a trabalhar com o acesso a dados. Agora, vamos nos aprofundar um pouco mais em alguns aspectos a serem considerados à medida que seu aplicativo amadurece e você chega mais perto de uma versão para produção.

Cadeias de caracteres de conexão: Até agora, deixei o Code First gerar um banco de dados em localhost\SQLEXPRESS. Quando chegar a hora de implantar meu aplicativo, provavelmente desejarei alterar o banco de dados para o qual o Code First está apontado. A abordagem recomendada para isso é adicionar uma entrada de cadeia de caracteres de conexão ao arquivo App.config (ou Web.config para aplicativos Web). Essa é também a abordagem recomendada para uso do Code First para mapear para um banco de dados existente. Se o nome da cadeia de caracteres de conexão corresponder ao nome do tipo totalmente qualificado do contexto, o Code First será selecionado automaticamente no tempo de execução. Entretanto, a abordagem recomendada é usar o construtor DbContext que aceita uma nome de conexão usando a sintaxe name=<connection string name>. Isso garante que o Code First sempre usará o arquivo de configuração. Uma exceção será gerada se a entrada da cadeia de caracteres de conexão não puder ser localizada. O exemplo a seguir mostra a seção da cadeia de caracteres de conexão que poderia ser usada para afetar o banco de dados pretendido pelo nosso aplicativo de exemplo:

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

Eis o código de contexto atualizado:

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

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

Observe que a ativação de “Vários conjuntos de resultados ativos” é recomendada. Isso permite que duas consultas fiquem ativas ao mesmo tempo. Por exemplo, isso seria necessário para consultar postagens associadas a um blog enquanto enumera todos os blogs.

Inicializadores do banco de dados Por padrão, o Code First criará um banco de dados automaticamente se o banco de dados pretendido não existir. Para algumas pessoas, essa será a funcionalidade desejada mesmo ao implantar, e apenas o banco de dados de produção será criado na primeira vez em que o aplicativo for iniciado. Se você tiver um DBA responsável pelo seu ambiente de produção, será muito mais provável que o DBA criará o banco de dados de produção para você e, depois que seu aplicativo estiver implantado, ele falhará se o banco de dados pretendido não existir. Neste artigo, substituí também a lógica do inicializador padrão e configurei o banco de dados para ser eliminado e recriado sempre que o meu esquema for alterado. Isso é algo que você definitivamente não deseja deixar no lugar depois de implantar na produção.

A abordagem recomendada para alterar ou desativar o comportamento do inicializador ao implantar é usar o arquivo App.config (ou Web.config para aplicativos Web). Na seção appSettings, adicione uma entrada cuja chave seja DatabaseInitializerForType, seguida pelo nome do tipo de contexto e a montagem na qual ela está definida. O valor pode ser “Desativado” ou o nome do tipo de inicializador seguido pela montagem na qual ele está definido.

O exemplo a seguir desativa qualquer lógica do inicializador para o contexto usado neste artigo:

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

O exemplo a seguir retornará o inicializador para a funcionalidade padrão que criará o banco de dados somente se ele não existir:

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

Contas do usuário Se você decidir deixar que o seu aplicativo de produção crie o banco de dados, o aplicativo precisará executar inicialmente usando uma conta que tenha permissões para criar o banco de dados e modificar o esquema. Se essas permissões forem deixadas no lugar, o impacto potencial de um comprometimento da segurança de seu aplicativo será significativamente maior. É altamente recomendado que um aplicativo seja executado com o conjunto mínimo de permissões necessárias para consultar e manter dados.

Mais para aprender

Resumindo, neste artigo, forneci uma rápida introdução sobre o desenvolvimento do Code First e a nova API DbContext, que estão incluídos no ADO.NET Entity Framework 4.1. Você viu como a API fluente pode ser usada para mapear um banco de dados existente ou para afetar a forma de um esquema de banco de dados que é gerado pelo Code First. Em seguida, examinei a API do rastreador e como ela pode ser usada para consultar instâncias da entidade local e informações adicionais sobre essas instâncias. Finalmente, abordei algumas considerações para a implantação de um aplicativo que usa o Code First para acesso a dados.

Se você desejar saber mais sobre quaisquer dos recursos incluídos no Entity Framework 4.1, visite msdn.com/data/ef. Também é possível usar o fórum Data Developer Center para obter ajuda com o uso do Entity Framework 4.1: bit.ly/166o1Z.

Rowan Miller é gerente de programas da equipe do Entity Framework da Microsoft. Você pode aprender mais sobre o Entity Framework em seu blog em romiller.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Arthur Vickers