entity framework
DESCRIPTION
Entity FrameworkTRANSCRIPT
O que é uma ferramenta de mapeamento objeto-relacional?
Todos nós, desenvolvedores de software, sabemos da importância de banco de dados
em nossos sistemas. Necessitamos, a todo instante, realizar consultas aos dados para
atender as solicitações do usuário, bem como, registrar qualquer informação passada por
este.
Atualmente possuímos muitos bancos de dados relacionais, dentre os mais conhecidas
podemos citar: Oracle, MS SQL Server, MySql, etc. Nessas ferramentas, as
informações são armazenadas como registros de tabelas.
Por exemplo, imagine o sistema para gerenciamento de produtos em uma loja virtual. O
banco de dados para armazenar os produtos disponíveis para venda teria uma tabela
chamada Produtos, composta por campos que caracterizam um produto, tais como:
Código, Nome, Preço, Categoria, etc.
Sem a utilização de alguma ferramenta, o trabalho do programador seria árduo e muito
vulnerável a falhas. Imagine o mapeamento de uma tabela Usuários criada através do
seguinte comando SQL:
create table Users (
id int UNSIGNED NOT NULL AUTO_INCREMENT,
email varchar (255) NOT NULL,
password varchar (20) NOT NULL,
name varchar (255) NOT NULL,
active bit (1),
date datetime NOT NULL,
PRIMARY KEY(id)
)
Em nosso sistema temos a necessidade de listar todos os usuários cadastrados no banco.
Quando queremos acessar o banco de dados, precisamos de uma conexão:
SqlConnection conexao = new SqlConnection("String de conexão com o MS SQL");
conexao.Open();
Depois de abrir a conexão, precisamos preparar um comando para o banco de dados que
enviará umSELECT que devolverá a lista de usuários: SqlCommand comando = new SqlCommand(
"SELECT id, email, password, name, active, date FROM Users",
conexao);
comando.CommandType = System.Data.CommandType.Text;
O comando preparado é executado através do método ExecuteReader, esse método
devolve um objeto especializado em ler o resultado da busca: IDataReader reader = comando.ExecuteReader();
Agora que temos o leitor do resultado, precisamos transformá-lo em uma lista de
modelos da aplicação. O IDataReader possui o método Read que é responsável por ler os
resultados devolvidos pela busca.
Cada vez que chamamos o Read, estamos avançando para o próximo registro da busca.
Enquanto for possível avançar, o Read devolve o valor true, o false é devolvido
quando não há mais registros na busca. O código para ler todos os registros fica da
seguinte forma: while (reader.Read())
{
// lê o registro
}
Podemos acessar as informações do registro atual utilizando a notação de array, da
mesma forma que fazemos com um dicionário do C#, porém os tipos dos dados
devolvidos pelo reader são tipos do banco de dados e, portanto, precisamos convertê-
los para os tipos do c#. Esse trabalho será feito pela classe Convert: while(reader.Read())
{
int id = Convert.ToInt32(reader["id"]);
}
Os dados que estamos lendo pertencem ao modelo Usuario, então vamos guardá-los em
uma instância dessa classe while(reader.Read())
{
Usuario usuario = new Usuario();
usuario.Id = Convert.ToInt32(reader["id"]);
usuario.Email = Convert.ToString(reader["email"]);
usuario.Senha = Convert.ToString(reader["pasword"]);
usuario.Nome = Convert.ToString(reader["name"]);
usuario.Ativo = Convert.ToBoolean(reader["active"]);
usuario.DataCadastro = Convert.ToDateTime(reader["date"]);
}
O loop lê o registro, cria o usuário com os dados preenchidos e imediatamente perde a
referência para o usuário criado. Para não perdemos a referência, vamos guardá-la em
uma lista:
IList<Usuario> usuarios = new List<Usuario>();
while(reader.Read())
{
Usuario usuario = new Usuario();
usuario.Id = Convert.ToInt32(reader["id"]);
usuario.Email = Convert.ToString(reader["email"]);
usuario.Senha = Convert.ToString(reader["pasword"]);
usuario.Nome = Convert.ToString(reader["name"]);
usuario.Ativo = Convert.ToBoolean(reader["active"]);
usuario.DataCadastro = Convert.ToDateTime(reader["date"]);
usuarios.Add(Usuario);
}
Agora que terminamos de fazer a query, precisamos fechar o reader e a conexão.
reader.Close();
conexao.Close();
O código para acessar o banco de dados é trabalhoso e repetitivo, além disso, quando
fazemos queries que envolvem valores passados pelo usuário, podemos facilmente
inserir vulnerabilidades que permitem ataques como o SQL Injection (Técnica utilizada
por hackers para enviar comandos nocivos à base de dados, através de campos do
formulário ou URLs, por exemplo).
Além do código repetitivo e dos problemas de segurança, o que acontece quando
precisamos trocar o banco MySQL, utilizado atualmente, para o Oracle? Nesse caso,
teremos que modificar o código do sistema inteiro, além das SQLs, para poder suportar
o novo banco.
Esses são apenas alguns dos problemas que temos quando lidamos com o banco de
dados diretamente, mas reparem que para construirmos uma query na tabela de usuários,
precisamos apenas olhar a estrutura da classe Usuario, ou seja, podemos ter uma
ferramenta que dada uma classe, consegue construir as queries necessárias. Ferramentas
que fazem o mapeamento do mundo orientado a objetos (classes e objetos) para o
mundo relacional são chamadas de mapeadores objeto relacional, ou ORM (Object
Relational Mapper). Nesse mapeamento, classes se transformam em tabelas e objetos
em registros das tabelas.
Nesse curso, aprenderemos como funciona o ORM da Microsoft. O Entity Framework!
Instalação do Entity Framework
O Entity Framework é uma ferramenta de mapeamento objeto relacional desenvolvido
pela Microsoft, nele as entidades do banco de dados são mapeadas para coleções de
dados que podem ser utilizadas no LINQ.
Para exemplificarmos o uso do Entity Framework, criaremos uma loja virtual
simplificada, em um projeto chamado LojaEF. Nesse projeto, criaremos classes que
possuem representação no banco de dados, chamaremos essas classes de Entidades.
Vamos criar um projeto do tipo Console Application, que chamaremos de LojaEF
Para utilizarmos o utilizarmos o entity framework no projeto, precisamos instalá-lo e
para isso, utilizaremos o gerenciador de pacotes padrão do Visual Studio, o NuGet.
Dentro do Visual Studio, clique com o botão direito no projeto Loja e escolha a
opção Manage Nuget Packages. Uma janela como a da imagem abaixo será aberta.
Na aba Online Packages, busque por Entity Framework e após encontrar clique no
botão Install.
O próprio Nuget se encarrega de efetuar o download do framework e já referenciar as
dlls necessárias ao seu projeto. Pronto! Você já pode utilizar o Entity Framework em
seu projeto.
Mapeando a primeira entidade com o Entity Framework
Agora que temos o Entity Framework instalado no projeto, podemos escrever a primeira
entidade, a classe Usuario. Ela será uma entidade do domínio da aplicação, que ficará
dentro do namespaceLojaEF.Entidades
O usuário terá um ID, que será o identificador único do usuário e um Nome:
public class Usuario
{
public int ID { get; set; }
public string Nome { get; set; }
}
Quando utilizamos o Entity Framework, não precisamos mais nos preocupar com o
banco de dados. Essa responsabilidade agora é do ORM, nossa preocupação será apenas
com o domínio da aplicação.
Agora que a classe foi criada, precisamos dizer ao Entity Framework que ela representa
uma entidade do banco de dados. Para isso, ela deve ser colocada dentro de uma classe
que herda de DbContext, o contexto do Entity Framework: public class EntidadesContext : DbContext
{
}
Essa classe funcionará como uma conexão com o banco de dados. Sempre que
quisermos gravar, recuperar, atualizar ou remover uma entidade faremos isso através do
contexto. Para mapearmos oUsuario, precisamos apenas definir uma propriedade do
tipo DbSet dentro do EntidadesContext: public class EntidadesContext : DbContext
{
public DbSet<Usuario> Usuarios { get; set; }
}
O Entity Framework segue o padrão Convention Over Configuration no mapeamento
das classes, o nome da tabela que conterá as informações de uma entidade é o plural do
nome da classe. Cada atributo da entidade se transformará em uma coluna com o
mesmo nome do atributo. O ID da entidade será o atributo que contém a palavra ID no
nome.
Agora que terminamos o mapeamento da classe, vamos utilizar o Entity Framework
para gerar as tabelas do banco de dados.
Nessa aplicação utilizaremos o SQLServer de testes que é integrado ao visual studio.
Para criar o banco de dados que será utilizado, dentro do Solution Explorer, clique com
o botão direito no projeto e selecione a opção Add > New Item. No canto esquerdo da
janela do New Item, escolha a opção Datae depois Service Based Database.
Escolha LojaEF.mdf como nome do novo banco de dados e depois clique no botão Add.
Quando clicarmos no Add, o Visual Studio abrirá um assistente para nos ajudar a
configurar o banco de dados, o Data Source Configuration Wizard. Na janela aberta,
escolha a opção Dataset e depois clique em next.
Depois que o assistente terminar de configurar o banco de dados, clique no
botão Finish
Quando criamos o banco pelo Visual Studio, as configuraçãoes de acesso para o banco
criado são colocadas dentro do arquivo de configuração da aplicação, o App.config.
No App.config, as configurações do banco de dados ficam dentro de uma tag chamada
connectionStrings:
<connectionStrings>
<add name="LojaEF.Properties.Settings.LojaEFConnectionString"
connectionString="Data
Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\LojaEF.mdf;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
Para utilizarmos o Entity Framework com o banco criado, precisamos mudar o nome da
string de conexão para o nome do contexto que criamos para a aplicação, o
EntidadesContext:
<connectionStrings>
<add name="EntidadesContext"
connectionString="Data
Source=(LocalDB)\v11.0;AttachDbFilename=|DataDirectory|\LojaEF.mdf;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
Se deixarmos essa configuração padrão na string de conexão, toda vez que executarmos
a aplicação, o banco de dados começará vazio! Para fazermos com que o banco não seja
apagado, colocaremos o caminho absoluto do banco na
configuração AttachDbFilename da string de conexão.
Quando queremos trabalhar com outros bancos de dados, precisamos apenas modificar
o arquivo de configuração da aplicação!
<connectionStrings>
<add name="EntidadesContext"
connectionString="Data Source=(LocalDB)\v11.0;AttachDbFilename=Caminho Absolu to
Banco;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
Se quisermos mudar o banco de dados da aplicação, precisamos apenas modificar as
configurações que estão no App.config, não precisamos modificar o código fonte da
aplicação!
Agora que já configuramos o banco de dados, vamos utilizar o contexto para gerar as
tabelas do banco. Para isso, utilizaremos a propriedade Database do contexto. Dentro do
método Main, vamos criar um novo contexto: class Program
{
static void Main(string [] args)
{
var contexto = new ExtidadesContext();
}
}
E agora que temos o contexto, precisamos apenas chamar a
método CreateIfNotExists da propriedade Database do contexto. var contexto = new EntidadesContext();
contexto.Database.CreateIfNotExists();
Com isso, o Entity Framework enviará os comandos para criar as tabelas no banco de
dados!
Além disso, o contexto do Entity Framework pode ser utilizado para gravar usuários no
banco de dados.
Então vamos criar um novo usuário chamado Victor:
Usuario victor = new Usuario { Nome = "Victor" };
E agora vamos gravar esse usuário no banco de dados. Para isso, utilizaremos a
propriedadeUsuarios que foi declarada no contexto. Essa propriedade representa um
conjunto de entidades que estão gravadas no banco e para inserir um usuário no banco,
precisamos apenas chamar o métodoAdd nesse conjunto. var contexto = new EntidadesContext();
Usuario victor = new Usuario { Nome = "Victor" };
contexto.Usuarios.Add(victor);
E agora que terminamos de adicionar as informações no contexto, precisamos apenas
salvar as modificações:
contexto.SaveChanges();
E por fim, precisamos fechar o contexto utilizando o método Dispose. contexto.Dispose();
O programa completo que gera as tabelas e grava a entidade fica da seguinte forma:
class Program
{
static void Main(string[] args)
{
var contexto = new EntidadesContext();
contexto.Database.CreateIfNotExists();
Usuario victor = new Usuario { Nome = "victor" };
contexto.Usuarios.Add(victor);
contexto.SaveChanges();
contexto.Dispose();
}
}
E com esse código simples, conseguimos gravar o usuário no banco de dados utilizando
o Entity Framework.
Explicação
Agora que já entendemos o funcionamento básico do Entity Framework, vamos
aprender como manipular os dados que temos no banco de dados.
Operações básicas utilizando o Entity Framework
Já vimos que o Entity Framework utiliza o LINQ para manipular os dados gravados no
banco de dados como se fossem coleções do C#, com isso, podemos adicionar um novo
registro no banco fazendo simplesmente um Add na coleção: var contexto = new EntidadesContext();
Usuario usuario = new Usuario()
{
Nome = "victor",
Senha = "123"
};
contexto.Usuarios.Add(usuario);
Agora para efetivarmos as modificações, utilizamos o método SaveChanges no contexto: // envia as modificações para o banco de dados.
contexto.SaveChanges();
Agora que já temos um objeto gravado, queremos recuperá-lo. Para recuperarmos um
usuário pelo ID, utilizamos o método Find do objeto DbSet. var contexto = new EntidadesContext();
// busca o usuário de Id 1 do banco de dados.
Usuario usuario = contexto.Usuarios.Find(1L);
Console.WriteLine(usuario.Nome);
Se quisermos remover um usuário que foi buscado, precisamos apenas removê-lo da
coleção com o método Remove var contexto = new EntidadesContext();
Usuario usuario = contexto.Usuarios.Find(1L);
// remove o usuário da coleção.
contexto.Usuarios.Remove(usuario);
contexto.SaveChanges();
Isolando o acesso ao banco de dados com DAOs
Nos exemplos que utilizamos até o momento, o código de acesso ao banco está no
método Main do programa. Com isso estamos dificultando a manutenção da aplicação.
Precisamos isolar o código que acessa o banco de dados em classes especializadas em
fazer o acesso aos dados, que são conhecidas como Data Access Object ou DAO
Então criaremos um DAO para isolar o código que acessa as informações do usuário no
banco de dados, esse código será isolado na classe UsuariosDAO public class UsuariosDAO
{
public void Adiciona(Usuario usuario)
{
// código para adicionar o usuário
}
public void Remove(Usuario usuario)
{
// código para remover o usuário
}
public Usuario BuscaPorId(long id)
{
// busca o usuário por id
}
}
Cada um dos métodos do DAO precisa do contexto para executar a query no banco de
dados, então vamos abrí-lo no construtor da classe:
public class UsuariosDAO
{
private EntidadesContext contexto;
public UsuariosDAO()
{
this.contexto = new EntidadesContext();
}
}
E agora podemos implementar os métodos dos DAO:
public class UsuariosDAO
{
private EntidadesContext contexto;
public UsuariosDAO()
{
this.contexto = new EntidadesContext();
}
public void Adiciona(Usuario usuario)
{
this.contexto.Usuarios.Add(usuario);
this.contexto.SaveChanges();
}
public void Remove(Usuario usuario)
{
this.contexto.Usuarios.Remove(usuario);
this.contexto.SaveChanges();
}
public Usuario BuscaPorId(long id)
{
return this.contexto.Usuarios.Find(id);
}
}
E agora no código do Main não precisamos mais nos preocupar com o código de acesso
ao banco de dados, podemos simplesmente utilizar o DAO:
static void Main(string[] args)
{
Usuario usuario = new Usuario() { Nome = "victor", Senha = "123" };
UsuariosDAO dao = new UsuariosDAO();
dao.Adiciona(usuario);
}
Estado dos objetos
Agora que já aprendemos como fazer as operações básicas utilizando o entity
framework, vamos aprender como o entity framework gerencia o estado dos objetos do
contexto. Para ilustrar a explicação, vamos utilizar um banco de dados que contém um
usuário de id 1, chamado Victor e com senha 123.
Vimos que para buscar uma entidade do contexto, podemos utilizar o
método Find do DbSet: var contexto = new EntidadesContext();
Usuario usuario = contexto.Usuarios.Find(1);
Agora se quisermos mudar o nome do usuário para Victor Harada, precisamos
simplesmente mudar a propriedade nome do Usuario buscado:
usuario.Nome = "Victor Harada";
Agora que o usuário foi modificado, podemos salvar as modificações no contexto
contexto.SaveChanges();
Quando executamos a linha acima, o Entity Framework sincroniza o estado dos objetos
com o banco de dados, ou seja, nesse ponto do código o Entity Framework executa um
update no banco de dados para atualizar as informações do usuário. Então quando
buscamos um objeto, a entidade devolvida é gerenciada.
Ao buscarmos um objeto, a entidade está em um estado chamado Unchanged, quando
executamos oSaveChanges todas as entidades Unchanged não são modificadas no banco.
Ao mudarmos o valor de uma propriedade o entity framework muda o estado da
entidade para Modified. Ao chamarmos oSaveChanges quando o contexto possui uma
entidade modificada, as modificações são enviadas para o banco de dados.
Vimos que para gravarmos uma entidade, precisamos apenas adicioná-la ao conjunto do
contexto:
var contexto = new EntidadesContext();
Usuario u = new Usuario { Nome = "rodrigo" };
contexto.Usuarios.Add(u);
Quando adicionamos uma nova entidade no contexto, ela fica em um estado chamado
Added.
Para removermos uma entidade, precisamos apenas chamar o método Remove na coleção
do contexto passando a instância que deve ser removida, quando fazemos isso, a
entidade entra em um estado chamado Deleted.
Toda entidade que não está associada ao contexto está em um estado
chamado Detached.
Cadastro de produtos para a loja
Para facilitar a busca de produtos, queremos dividí-los em categorias. Vamos então criar
a entidadesCategoria: public class Categoria
{
public int ID { get; set; }
public string Nome { get; set; }
}
No c#, para representarmos que um produto possui uma categoria, precisamos apenas
criar uma nova propriedade do tipo Categoria na classe Produto: public class Produto
{
// outras propriedades
public Categoria Categoria;
}
Quando criamos um atributo que representa um relacionamento dentro da classe,
precisamos marcar o atributo como virtual:
public class Produto
{
// outras propriedades
public virtual Categoria Categoria;
}
Para representarmos esse relacionamento no banco de dados, precisamos guardar o id da
categoria na tabela de produtos. Uma coluna que guarda o id para outra tabela é
chamada de chave estrangeira e a propriedade que representa o relacionamento dentro
da classe é chamada de Navigation Property. Além de definirmos a Navigation Property
do produto para a categoria, também precisamos definir um atributo que será a chave
estrangeira do produto para a categoria, por convenção, o nome do atributo que
representa a chave estrangeira é ID, ou seja, no caso do produto teríamos CategoriaID: public class Produto
{
// propriedades do produto
// representa a chave estrangeira do produto para
// a categoria
public int CategoriaID { get; set; }
public Categoria Categoria { get; set; }
}
O tipo da chave estrangeira define a obrigatoriedade do relacionamento. Se quisermos
que todo produto tenha obrigatoriamente uma categoria, utilizamos o tipo int. Se a
categoria do produto for opcional, utilizamos o tipo int? como tipo da chave
estrangeira: public class Produto
{
// propriedades do produto
// agora o produto tem opcionalmente uma categoria
public int? CategoriaID { get; set; }
public Categoria Categoria { get; set; }
}
Como a chave estrangeira está na tabela produtos, podemos ter vários produtos
associados a mesma categoria, o que caracteriza um relacionamento muitos para um
(many to one).
Para que o entity framework reconheça essas classes como entidades, precisamos
adicioná-las ao contexto:
public class EntidadesContexto
{
public DbSet<Usuario> Usuarios { get; set; }
public DbSet<Produto> Produtos { get; set; }
public DbSet<Categoria> Categorias { get; set; }
}
E com isso conseguimos mapear o produto com uma categoria, mas quando tentarmos
executar a aplicação, o entity framework jogará uma InvalidOperationException pois o
estado do banco de dados não está sincronizado com o estado dos modelos, então
precisamos criar mais uma migration para incluir esses modelos no banco de dados.
Então vamos abrir novamente o console do NuGet e executar o comando Add-Migration
CriaTabelasProdutoECategoria e depois vamos atualizar o banco com o
comando Update-Database.
Adicionando um produto com categoria
Quando queremos gravar um novo produto no banco de dados, precisamos,
inicialmente, criar e inicializar uma nova instância do objeto Produto
Produto produto = new Produto();
produto.Nome = "Camiseta";
produto.Preco = 10.0;
E depois, para gravá-lo no banco, devemos adicioná-lo ao contexto e gravar as
modificações:
var contexto = new EntidadesContext();
contexto.Produtos.Add(produto);
contexto.SaveChanges();
Temos duas formas de associar um produto com uma categoria. Na primeira,
precisamos apenas colocar uma instância de Categoria no
atributo Categoria do Produto antes de gravá-lo no banco de dados. Produto produto = new Produto();
// inicializa o produto
Categoria categoria = new Categoria();
// inicializa a categoria
produto.Categoria = Categoria;
var contexto = new EntidadesContext();
contexto.Produtos.Add(produto);
contexto.SaveChanges();
Quando executamos esse código, o Entity Framework tenta gravar o produto no banco
de dados, porém ele percebe que o produto está associado com uma categoria que ainda
não foi gravada e, portanto, também grava a categoria no banco de dados. Ao final desse
código, teremos um produto associado com uma nova categoria.
Se quisermos associar o produto com uma categoria existente no banco, precisamos de
uma categoria que esteja associada com o contexto, ou seja, precisamos buscá-la antes
de gravar o produto.
var contexto = new EntidadesContext();
var categoria = contexto.Categorias.Find(1L);
var produto = new Produto();
//inicializa o produto
produto.Categoria = categoria;
contexto.Produtos.Add(categoria);
contexto.SaveChanges();
Com isso, o Entity Framework percebe que o novo produto está associado a uma
categoria já existente e, portanto, adiciona apenas o produto ao banco de dados.
A segunda forma de associarmos o produto com uma categoria é utilizando a
propriedade que representa a chave estrangeira do modelo. Se quisermos, por exemplo,
associar o novo produto com a categoria de id 1, que deve existir no banco de dados,
precisamos apenas colocar o id 1 na propriedade CategoriaID do novo produto: produto.CategoriaID = 1;
Com isso, ao gravarmos o produto no banco de dados, o entity framework
automaticamente associará o novo produto com a categoria de id 1.
Categoria com lista de produtos
Agora que definimos o mapeamento do relacionamento many to one do produto com a
categoria, estamos interessados em pegar todos os produtos associados a uma
determinada categoria.
Quando olhamos o relacionamento do ponto de vista de um produto, temos muitos
produtos associados a uma instância de categoria, porém do ponto de vista da categoria,
temos uma categoria associada a vários produtos, ou seja, temos um relacionamento one
to many.
No mundo relacional, toda vez que temos um relacionamento many to one, existe,
automaticamente, um one to many, ou seja, no mundo relacional, os relacionamentos
são sempre bidirecionais. Na orientação a objetos, não temos relacionamentos
bidirecionais automáticos, toda vez que queremos o relacionamento one to many,
devemos fazer o mapeamento explícito desse relacionamento.
Para mapear a lista de produtos na categoria, colocaremos, inicialmente, a lista como
uma propriedade da categoria:
public class Categoria
{
public int ID { get; set; }
public string Nome { get; set; }
public virtual IList<Produto> Produtos { get; set; }
}
Agora precisamos avisar o entity framework que o essa lista de produtos é a outra ponta
do relacionamento many to one que colocamos dentro do produto. Fazemos isso atráves
do métodoOnModelCreating do EntidadesContext: public class EntidadesContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// mapeamento do relacionamento aqui!
}
}
Dentro desse método, utilizamos o modelBuilder para definir os relacionamentos da
entidade. Para definirmos que estamos mapeando a Categoria, utilizamos o
método Entity: modelBuilder.Entity<Categoria>()
E agora para definirmos que a categoria tem muitos produtos, utilizamos o
método HasMany passando um lambda que devolve qual é o atributo da categoria que
representa o relacionamento: modelBuilder.Entity<Categoria>()
.HasMany(categoria => categoria.Produtos)
Agora para indicarmos que o relacionamento da propriedade Produtos é a outra ponta
da propriedade Categoria do produto, utilizamos o método WithOptional. Nesse
método precisamos passar um lambda que devolverá a categoria do produto: modelBuilder.Entity<Categoria>()
.HasMany(categoria => categoria.Produtos)
.WithOptional(produto => produto.Categoria);
Com isso, o entity framework sabe as duas pontas do relacionamento definido.
Vamos agora testar tentar listar todas as produtos de uma determinada categoria, para
isso, buscaremos a categoria do banco de dados e depois utilizaremos o foreach para
imprimir todos os produtos:
static void Main(string [] args)
{
var contexto = new EntidadesContext();
Categoria categoria = contexto.Categorias.Find(1L);
foreach(var produto in categoria.Produtos)
{
Console.WriteLine(produto.Nome);
}
contexto.Dispose();
}
Com esse código simples, conseguimos mapear os relacionamentos das entidades.
Consistência de relacionamentos bidirecionais
Imagine que temos a seguinte categoria no banco de dados:
+----+--------------+
| Id | Nome |
+----+--------------+
| 1 | Informática |
+----+--------------+
Associados a essa categoria, temos os seguintes produtos:
+----+---------+--------+-------------+
| Id | Nome | Preco | CategoriaId |
+----+---------+--------+-------------+
| 1 | Teclado | 20.00 | 1 |
| 2 | Monitor | 300.00 | 1 |
+----+---------+--------+-------------+
Podemos recuperar a lista de produtos utilizando o código abaixo:
var context = new EntidadesContext();
Categoria categoria = context.Categorias.Find(1L);
IList<Produto> produtos = categoria.Produtos;
Quando executamos esse código, o Entity Framework executa uma query que recupera
apenas a categoria do banco de dados, os produtos relacionados a essa categoria, por
padrão, não são carregados.
O Entity Framework carrega os relacionamentos apenas quando necessário (modo lazy).
Quando pedimos qualquer informação sobre o relacionamento, ele é forçado a realizar a
busca no banco de dados:
var context = new EntidadesContext();
Categoria categoria = context.Categorias.Find(1L);
IList<Produto> produtos = categoria.Produtos;
Console.WriteLine(produtos.Count);
Esse código imprime 2 no terminal, pois no banco de exemplo temos 2 produtos
associados à categoria de id 1. Agora vamos adicionar mais um produto com a categoria
1 e imprimir novamente o número de produtos dessa categoria:
var context = new EntidadesContext();
Categoria categoria = context.Categorias.Find(1L);
IList<Produto> produtos = categoria.Produtos;
Console.WriteLine(produtos.Count);
Produto produto = new Produto()
{
Categoria = categoria,
Nome = "nome",
Preco = 200.0
};
context.Produtos.Add(produto);
Console.WriteLine(categoria.Produtos.Count);
Nesse código, o primeiro WriteLine continua imprimindo o número 2 no terminal e
depois de adicionarmos o novo produto no contexto, o Console.WriteLine imprime o
número 3, ou seja, o entity framework consegue perceber mudanças no contexto e
sincronizar as pontas do relacionamento!
Agora que já vimos como mapear as entidades e seus relacionamentos, vamos aprender
como escrever consultas no banco de dados com o Entity Framework.
Como vimos anteriormente, a comunicação com o banco de dados é feita através
do DbContext e toda vez que queremos fazer uma query, precisamos utilizar os
conjuntos de entidades que foram mapeados no contexto: EntidadesContext contexto = new EntidadesContext();
Assim como quando estamos trabalhando com listas e conjuntos do C#, fazemos buscas
nos conjuntos do contexto utilizando a Language Integrated Query ou LINQ. Então para
listarmos todos os produtos do banco de dados, utilizamos o seguinte código:
var busca = from p in contexto.Produtos select p;
Agora para transformarmos o resultado da query em uma lista de produtos, precisamos
apenas chamar o método ToList na busca: var busca = from p in contexto.Produtos select p;
IList<Produto> produtos = busca.ToList();
A query definida na variável busca só é enviada para o banco de dados quando
executamos o métodoToList.
E agora, podemos, por exemplo, utilizar o foreach para imprimir os produtos que foram
recuperados do banco de dados:
var busca = from p in contexto.Produtos select p;
IList<Produto> produtos = busca.ToList();
foreach(var produto in produtos)
{
Console.WriteLine(produto.Nome);
}
E se quisermos ordenar o resultado por algum critério, podemos utilizar o orderby do
LINQ: var busca = from p in contexto.Produtos orderby p.Nome select p;
Buscando produtos por preço
Agora que já aprendemos como fazer uma query que lista entidades, vamos aprender
como colocar restrições na busca. Assim como na busca em listas do c#, utilizamos a
instrução where do LINQ: var busca = from p in contexto.Produtos
where condições
select p;
Queremos buscar todos os produtos com preço maior do que 10.0, então a condição da
query deve aceitar apenas produtos com a propriedade Preco maior do que o valor 10.0:
var busca = from p in contexto.Produtos
where p.Preco > 10.0m
select p;
E se também quisermos os produtos com preço maior do que 100.0? Teríamos que
escrever uma query diferente. Precisamos definir um parâmetro nessa busca que será o
preço mínimo do produto e como o LINQ se integra com o código C#, podemos
simplesmente utilizar as variáveis que estão em escopo dentro da busca:
decimal preco = 100.0;
var busca = from p in contexto.Produtos
where p.Preco > preco
select p;
Com isso, o Entity Framework define uma query com parâmetros utilizando a SQL e já
substitui o valor do parâmetro!
Busca de produtos por categoria
Queremos recuperar todos os produtos que pertencem a uma categoria chamada
informática, porém a Categoria é um outro modelo do sistema e é representada por uma
tabela diferente no banco de dados.
Para realizarmos essa busca, utilizando a SQL, teríamos que utilizar o join:
select p.*
from Produto p
inner join Categoria c on p.CategoriaId = c.Id
where c.Nome = 'Informatica'
Com o LINQ, precisamos apenas acessar as propriedades da entidade. Por exemplo,
para filtrarmos todos os produtos de uma categoria cujo nome está em uma variável do
código, utilizaríamos a seguinte query:
string nomeCategoria = "Informatica";
var query = from p in contexto.Produtos
where p.Categoria.Nome == nomeCategoria
select p;
Repare que na query acima, não precisamos nos preocupar com o join entre as tabelas, é
responsabilidade do Entity Framework enviar a SQL correta para o banco de dados.
Podemos também utilizar diversas condições na query. Assim como no if do C#,
podmeos utilizar o &&e o || para juntar as condições da query. Por exemplo, se
quiséssemos todos os produtos cuja categoria tem um determinado nome e com preço
maior do que um valor mínimo, poderíamos utilizar a seguinte query: string nomeCategoria = "Informatica";
decimal precoMinimo = 100.0m;
var query = from p in contexto.Produtos
where p.Categoria.Nome = nomeCategoria and p.Preco > precoMinimo
select p;
Número de produtos por categoria
Agora que já vimos como a HQL funciona, vamos escrever uma query mais avançada.
Queremos recuperar o número de produtos agrupados por categoria. Para resolvermos
esse problema na SQL utilizaríamos o group by. select c.Id, count(p.Id)
from Categoria c inner join Produto p on (c.Id = p.Categoria_Id)
group by c.Id
Podemos fazer uma query equivalente utilizando o LINQ. Essa query pode começar
pela entidade Categoria:
var busca = from c in contexto.Categorias select c
Mas nessa busca precisamos devolver tanto a categoria quanto a quantidade de produtos
associados. Para recuperar o número de produtos de uma determinada categoria,
podemos utilizar o count da lista:
c.Produtos.Count
Colocando essa linha na query, teremos:
var busca = from c in contexto.Categorias select c, c.Produtos.Count
Mas a busca do LINQ só pode devolver um objeto, logo precisamos usar uma projeção:
var busca = from c in contexto.Categorias
select new { Categoria = c, NumeroDeProdutos = c.Produtos.Count };
Com isso estamos devolvendo um objeto anônimo que contém um campo que guarda a
categoria e outro que guarda o número de produtos daquela categoria. E agora podemos
listar os resultados da query e utilizar um foreach para mostrar os resultados:
var busca = from c in contexto.Categorias
select new { Categoria = c, NumeroDeProdutos = c.Produtos.Count };
var resultados = busca.List();
foreach(var resultado in resultados)
{
Console.WriteLine(resultado.Categoria.Nome + " " + resultado.NumeroDeProdutos);
}
Mas só podemos utilizar o tipo anônimo dentro do método que o declara, então
criaremos uma nova classe para guardar o resultado dessa query:
public class ProdutosPorCategoria
{
public Categoria Categoria { get; set; }
public int NumeroDeProdutos { get; set; }
}
Para fazermos a query devolver instâncias de ProdutosPorCategoria ao invés de
instâncias de objetos anônimos, utilizaremos os initializers do c# para construir o
objeto: var busca = from c in contexto.Categorias
select new ProdutosPorCategoria()
{
Categoria = c,
NumeroDeProdutos = c.Produtos.Count
};
E agora, quando listarmos o resultado da query, teremos uma lista
de ProdutosPorCategoria: var busca = from c in contexto.Categorias
select new ProdutosPorCategoria()
{
Categoria = c,
NumeroDeProdutos = c.Produtos.Count
};
IList<ProdutosPorCategoria> resultado = busca.ToList();
Quando desenvolvemos software utilizando o Entity Framework, não precisamos nos
preocupar tanto com o banco de dados relacional, o que facilita muito o
desenvolvimento, mas devemos cuidar para que essas facilidades não acabem
prejudicando a performance do sistema.
Como aprendemos anteriormente, o Entity Framework carrega todos os
relacionamentos de forma lazy, ou seja, os relacionamentos são carregados apenas
quando necessário. Imagine que em nossa loja, queremos imprimir a lista com o nome
de todos os produtos junto com o nome da categoria de cada produto.
var produtos = contexto.Produtos.ToList();
foreach(var produto in produtos)
{
Console.WriteLine(produto.Nome + " - " + produto.Categoria.Nome);
}
No código acima, fazemos uma query para recuperar a lista de todos os produtos e
depois para cada produto imprimimos seu nome e o nome de sua categoria, porém a
categoria é um relacionamento, então o NHibernate só a carrega quando necessário
(quando acessamos seu nome), ou seja, para cada produto estamos enviando uma query
para carregar sua categoria. Esse problema é conhecido como N + 1 queries.
Evitando o problema do N+1
Quando estamos utilizando a SQL, podemos resolver o problema das N + 1 queries
utilizando um join:
select p.*, c.*
from Produto p join Categoria c on p.CategoriaId = c.Id
Podemos fazer algo parecido utilizando o LINQ. Para especificarmos que a query do
LINQ deve recuperar as categorias junto com a lista de produtos, utilizamos o método
include do DbSetinformando qual relacionamento queremos carregar: var busca = ctx.Produtos.Include("Categoria");
var produtos = busca.List();
Também podemos utilizar o Include na busca do LINQ: var busca = from produto in contexto.Produtos.Include("Categoria") select produto;
var produtos = busca.List();
Agora que estamos utilizando o LINQ, a query para trazer a lista de produtos já
carregará os produtos de cada uma das categorias.
N + 1 em relacionamentos to many
Vamos agora pensar numa query que devolve a lista de categorias.
IList<Categoria> categorias = contexto.Categorias.ToList();
Para cada categoria queremos imprimir o tamanho de sua lista de produtos:
IList<Categoria> categorias = contexto.Categorias.ToList();
foreach(var categoria in categorias)
{
Console.WriteLine(categoria.Nome + " - " + categoria.Produtos.Count);
}
Como a lista de produtos é um relacionamento do tipo one to many, ela é carregada de
forma lazy pelo Entity Framework. Quando executamos categoria.Produtos.Count, o
Entity Framework executa uma query que busca os produtos relacionados a categoria.
Temos novamente o problema de N + 1 Queries.
Para resolver o problema de N + 1 queries no relacionamento to many, também
podemos utilizar oInclude: var busca = contexto.Categorias.Include("Produtos");
Com isso, o Entity Framework traz a informação da categoria junto com sua lista de
produtos e com isso, conseguimos evitar o problema de N + 1 queries.
Queremos colocar na aplicação um novo relatório de produtos. Esse relatório filtrará os
produtos por nome, nome da categoria e preço mínimo, porém essas informações são
opcionais.
Quando a o nome é fornecido para a busca, devemos colocar a condição que compara o
nome do produto na busca, senão, devemos ignorar essa condição. Faremos o mesmo
para a categoria e o preço do produto.
Como essa busca acessa o banco de dados para procurar informações sobre produtos,
colocaremos sua implementação dentro da classe ProdutosDAO. public class ProdutosDAO
{
private EntidadesContext contexto;
// implementação dos outros métodos do DAO.
public IList<Produto> BuscaPorNomePrecoMinimoECategoria(string nome,
double precoMinimo, string nomeCategoria)
{
}
}
Dentro desse método do DAO, precisamos criar a query do LINQ que fará essa busca,
mas como ficará essa query? Começaremos com uma query que lista todos os produtos
do banco de dados:
public IList<Produto> BuscaPorNomePrecoMinimoECategoria(string nome,
double precoMinimo, string nomeCategoria)
{
var busca = from produto in contexto.Produtos select produto;
}
Agora se o nome estiver definido, queremos comparar o nome do produto na condição
da query:
var busca = from produto in contexto.Produtos select produto;
if(!String.IsNullOrEmpty(nome))
{
// coloca a nova condição na query
}
Agora para colocar essa nova condição, precisamos continuar a query que foi iniciada
na variável busca. Para isso podemos utilizar a busca dentro do LINQ como se fosse
uma lista!
var busca = from produto in contexto.Produtos select produto;
if(!String.IsNullOrEmpty(nome))
{
busca = from produto in busca where produto.Nome == nome select produto;
}
Esse código funciona pois as queries do LINQ são enviadas para o banco de dados
apenas quando chamamos o método ToList ou iteramos na busca, além disso, toda vez
que utilizamos a variávelbusca em uma nova query, estamos adicionando novas
restrições à busca. Agora precisamos apenas completar os outros ifs da busca: if(preco > 0.0m) {
busca = from produto in busca where produto.Preco > preco select produto;
}
if(!String.IsNullOrEmpty(nomeCategoria))
{
busca = from produto in busca where produto.Categoria.Nome == nomeCategoria;
}
Com isso conseguimos resolver o problema da busca dinâmica. Agora só precisamos
listar os produtos da busca:
return busca.ToList();
O código completo da solução fica da seguinte forma:
public IList<Produto> BuscaPorNomePrecoMinimoECategoria(string nome,
double precoMinimo, string nomeCategoria)
{
var busca = from produto in contexto.Produtos select produto;
if(!String.IsNullOrEmpty(nome))
{
busca = from produto in busca where produto.Nome == nome select produto;
}
if(preco > 0.0m) {
busca = from produto in busca where produto.Preco > preco select produto;
}
if(!String.IsNullOrEmpty(nomeCategoria))
{
busca = from produto in busca where produto.Categoria.Nome == nomeCategoria;
}
return busca.ToList();
}
Busca dinâmica com os métodos do LINQ
Repare que na solução da busca dinâmica estamos a todo instante repetindo o código:
from produto in contexto.Produtos where alguma condição select produto;
Para diminuir a repetição, podemos utilizar as chamadas de método do LINQ. Toda vez
que queremos incluir uma nova restrição na query, podemos utilizar o método Where. busca = busca.Where(produto => condição)
Para utilizarmos a sintaxe de métodos, o tipo da variável busca deve ser IQueryable,
como DbSetimplementa IQueryable, podemos fazer: IQueryable<Produto> busca = contexto.Produtos;
E agora podemos colocar a condição comparando o nome do produto com o seguinte
código:
busca = busca.Where(produto => produto.Nome == nome);
Agora podemos reescrever as condições da busca com o seguinte código:
if(!String.IsNullOrEmpty(nome))
{
busca = busca.Where(produto => produto.Nome == nome);
}
if(preco > 0.0m) {
busca = busca.Where(produto => produto.Preco > preco);
}
if(!String.IsNullOrEmpty(nomeCategoria))
{
busca = busca.Where(produto => produto.Categoria.Nome == nomeCategoria);
}
E o método do DAO fica da seguinte forma:
public IList<Produto> BuscaPorNomePrecoMinimoECategoria(string nome,
double precoMinimo, string nomeCategoria)
{
IQueryable<Produto> busca = contexto.Produtos;
if(!String.IsNullOrEmpty(nome))
{
busca = busca.Where(produto => produto.Nome == nome);
}
if(preco > 0.0m) {
busca = busca.Where(produto => produto.Preco > preco);
}
if(!String.IsNullOrEmpty(nomeCategoria))
{
busca = busca.Where(produto => produto.Categoria.Nome == nomeCategoria);
}
return busca.ToList();
}
Agora que já aprendemos a fazer as operações básicas da loja, vamos implementar as
vendas.
Toda venda é feita para um usuário (relacionamento many to one com o usuário)
public class Venda
{
public virtual int Id { get; set; }
public virtual Usuario Cliente { get; set; }
}
Cada venda possui diversos produtos e um produto pode participar de várias vendas, o
que caracteriza um relacionamento many to many. Para representar o many to many,
colocaremos uma lista de produtos como propriedade da venda e faremos sua
inicialização no construtor da classe:
public class Venda
{
public virtual int Id { get; set; }
public virtual Usuario Cliente { get; set; }
public virtual IList<Produto> Produtos { get; set; }
public Venda()
{
this.Produtos = new List<Produto>();
}
}
Vamos agora mapear a venda dentro do contexto do Entity Framework:
public class EntidadesContext : DbContext
{
// outros mapeamentos
public DbSet<Venda> Vendas;
}
E agora precisamos configurar o contexto para que ele saiba que a lista de vendas é um
relacionamento many-to-many. No banco de dados, para representarmos um
relacionamento many to many, utilizamos uma tabela intermediária que guarda os ids
das entidades participantes.
Para mepearmos a lista, precisamos abrir novamente o método OnModelCreating: public class EntidadesContext : DbContext
{
// mapeamentos
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// outras configurações
modelBuilder.Entity<Venda>();
}
}
Para configurar que a venda possui uma lista de produtos, utilizamos o método HasMany: modelBuilder.Entity<Venda>()
.HasMany(x => x.Produtos);
Com isso mapeamos que esse relacionamento é to many, para falarmos que ele é many
to many, precisamos utilizar o WithMany: modelBuilder.Entity<Venda>()
.HasMany(x => x.Produtos)
.WithMany();
Para definirmos qual será a tabela de relacionamento, utilizamos o método Map: modelBuilder.Entity<Venda>()
.HasMany(x => x.Produtos)
.WithMany()
.Map(relacionamento => {
// configurações do relacionamento
});
Dentro do método Map, podemos configurar as caracteristicas do relacionamento
Many-to-Many. Para definirmos o nome da tabela de relacionamentos, utilizamos o
método ToTable no relacionamento: relacionamento.ToTable("Venda_Produtos");
Agora para definirmos os nomes das chaves estrangeiras, utilizamos os
métodos MapLeftKey para mapear a chave estrangeira que aponta para
a Venda e MapRightKey para mapearmos a que aponta para o Produto: relacionamento.MapLeftKey("VendaId");
relacionamento.MapRightKey("Produto_Id");
O mapeamento completo do relacionamento fica da seguinte forma:
modelBuilder.Entity<Venda>()
.HasMany(x => x.Produtos)
.WithMany()
.Map(relacionamento => {
relacionamento.ToTable("Venda_Produtos");
relacionamento.MapLeftKey("VendaId");
relacionamento.MapRightKey("Produto_Id");
});
Depois de configurarmos o novo modelo, precisamos criar uma migração que atualizará
as tabelas do banco de dados. No console do NuGet, criaremos uma nova migração com
o comando Add-Migration: Add-Migration CriaVenda
Essa migração criará a tabela de vendas e a tabela que guardará as informações do
relacionamento many-to-many.
Agora precisamos apenas executar a migração com o comando Update-Database no
console do NuGet.
Criando Vendas
Agora que conseguimos mapear os produtos e as vendas, vamos começar a vender
produtos para os clientes!
Quando vendemos um produto, precisamos inicialmente saber para qual cliente estamos
vendendo:
var contexto = new EntidadesContext();
Venda venda = new Venda();
Usuario cliente = contexto.Usuarios.Find(1L);
venda.Cliente = cliente;
Depois de definirmos para quem estamos vendendo, informaremos que o cliente
comprará os produtos com ids 1 e 2, por exemplo.
Produto p1 = contexto.Produtos.Find(1L);
Produto p2 = contexto.Produtos.Find(2L);
Para relacionar os produtos p1 e p2 com a venda, precisamos apenas adicioná-los na
lista de produtos, o Entity Framework cuidará da sincronização com o banco de dados! venda.Produtos.Add(p1);
venda.Produtos.Add(p2);
Agora que já criamos a venda, vamos adicioná-la ao contexto e salvar as modificações:
contexto.Vendas.Add(venda);
contexto.SaveChanges();
O código completo para a criação da venda fica da seguinte forma:
var contexto = new EntidadesContext();
Venda venda = new Venda();
Usuario cliente = contexto.Usuarios.Find(1L);
venda.Cliente = cliente;
Produto p1 = contexto.Produtos.Find(1L);
Produto p2 = contexto.Produtos.Find(2L);
venda.Produtos.Add(p1);
venda.Produtos.Add(p2);
contexto.Vendas.Add(venda);
contexto.SaveChanges();
Venda para empresas
Nossa loja cresceu muito e agora atende também a revendedores, ou seja, nós
agora vendemos produtos no atacado e no varejo atendendo empresas e
cidadãos comuns, mas nosso modelo atual não atende essa nova regra de
trabalho. Precisamos fazer uma modificação para conseguir identificar se o
cliente é pessoa física ou pessoa jurídica.
Pessoas físicas possuem todos os atributos que definimos para a
entidade Usuario e um atributo chamado CPF.
public class PessoaFisica
{
public int Id { get; set; }
public string Nome { get; set; }
public string Senha { get; set; }
public string CPF { get; set; }
}
Já as pessoas jurídicas possuem, além dos atributos do Usuario, um
atributo CNPJ.
public class PessoaJuridica
{
public int Id { get; set; }
public string Nome { get; set; }
public string Senha { get; set; }
public string CNPJ { get; set; }
}
Esse modelo atende nossa loja, porém teremos de replicar todas as
funcionalidades do Usuario para essas duas novas entidades, o que dificulta a
manutenção do código. Se precisarmos alterar a lógica do cliente,
precisaremos alterar para os dois tipos de cliente. Além disso, para listarmos
todos os clientes da loja, precisaríamos de dois selects, um na
entidade PessoaFisica e outro naPessoaJuridica.
Na orientação a objetos, quando queremos resolver esse tipo de problema,
utilizamos o Polimorfismo. Para aproveitarmos a implementação existente na
classe Usuario, faremos com que as
entidadesPessoaJuridica e PessoaFisica herdem da entidade Usuario.
Os modelos ficarão da seguinte forma:
public class PessoaFisica : Usuario
{
public string CPF { get; set; }
}
public class PessoaJuridica : Usuario
{
public string CNPJ { get; set; }
}
Como todos os clientes do sistema devem ser uma instância ou
de PessoaFisica ou dePessoaJuridica, não queremos permitir que a
classe Usuario seja instanciada, ela, portanto, será uma classe abstrata:
public abstract class Usuario
{
public virtual int Id { get; set; }
public virtual string Nome { get; set; }
}
Agora que já definimos as entidades, precisamos mapeá-las no Entity
Framework. Existem duas estratégias principais para o mapeamento de
Herança:
Tabela Única: O Entity Framework utilizará uma única tabela que conterá todas as propriedades
mapeadas pela classe pai ou por suas filhas.
Uma Tabela por subclasse: Nesse mapeamento, o Entity Framework cria uma tabela que
armazena os dados da classe pai e uma para para cada uma de suas filhas. As tabelas que
representam as classes filhas tem um relacionamente do tipo one to one com a tabela da classe
pai.
Tabela Única
O mapeamento da herança em uma única tabela é o padrão do Entity
Framework.
Como estamos armazenando dados de várias entidades em uma única tabela,
precisamos diferenciar os registros de alguma forma. Para fazer essa
diferenciação, o Entity Framework precisa de uma coluna que discriminará o
tipo de entidade que está gravada naquele registro. Por padrão o nome dessa
coluna especial é Discriminator e o valor que será armazenado será o nome do
tipo guardado.
Como utilizaremos as configurações padrão, não precisamos customizar mais
nada no mapeamento, o Entity Framework fará todo o trabalho sozinho.
Agora para conseguirmos utilizar a herança que acabamos de definir,
precisamos apenas criar uma nova migração dentro do código.
Então no console do nugget vamos adicionar uma nova migração utilizando o
Add-Migration:
Add-Migration HerancaEmTabelaUnica
Quando executarmos a migration, teremos o seguinte banco:
Por fim, vamos inserir dois usuários em nosso banco de dados, um do
tipo PessoaFisica e outro do tipo PessoaJuridica.
EntidadesContext contexto = new EntidadesContext();
PessoaFisica murilo = new PessoaFisica();
murilo.Nome = "Murilo";
murilo.Senha = "987";
murilo.CPF = "123.456.789.00";
contexto.Usuarios.Add(murilo);
PessoaJuridica caelum = new PessoaJuridica();
caelum.Nome = "Caelum";
caelum.Senha = "987";
caelum.CNPJ = "123.456/0001-09";
contexto.Usuarios.Add(caelum);
contexto.SaveChanges();
Com esse código, o Entity Framework gravará as informações tanto da pessoa
física quanto da pessoa jurídica dentro da tabela de usuários e preencherá o
campo discriminador com o nome da classe.
Mas o que acontece com os dados que já estavam gravados no banco de
dados? Como os registros antigos não possuem um valor no campo
discriminador, o Entity Framework não considera essas linhas como resultados
válidos da query e, portanto, elas não são listadas pela coleção de usuários do
contexto.
Quando trabalhamos com banco de dados, muitas vezes precisamos migrar os
dados antigos do banco para que eles sejam compatíveis com a aplicação. No
Entity Framework, podemos resolver esse problema através das migrations!
Vamos adicionar uma nova migração para atualizar a coluna Discriminator do
Usuário.
Add-Migration MigraDadosDoUsuario
Quando executarmos esse comando no console do NuGet, o Entity Framework
criará uma classe de migração que terá os métodos Up e Down com
implementações vazias.
public partial class MigraDadosDoUsuario : DbMigration
{
public override void Up()
{
}
public override void Down()
{
}
}
No método Up, queremos executar uma SQL que vai atualizar as informações
do banco de dados, fazemos isso com o método Sql da classe DbMigration:
public partial class MigraDadosDoUsuario : DbMigration
{
public override void Up()
{
Sql("sql que eu quero executar no banco");
}
public override void Down() { }
}
A Sql que queremos executar deve colocar o valor 'PessoaFisica'
em Discriminator se a coluna estiver vazia.
update tbl_Usuarios set Discriminator='PessoaFisica' where Discriminator=''
Onde tbl_Usuarios é o nome da tabela que guarda as informações dos
usuários. Agora só precisamos colocar essa Sql dentro do método Sql da
migration:
public partial class MigraDadosDoUsuario : DbMigration
{
public override void Up()
{
Sql("update tbl_Usuarios set Discriminator='PessoaFisica' where Discriminator=''");
}
public override void Down() { }
}
E depois de executarmos o comando Update-Database, nossos dados estarão
atualizados.
Uma tabela por subclasse
Quando queremos utilizar uma tabela por subclasse, precisamos mapear
explicitamente as entidades que pertencem à hierarquia de classes
explicitamente dentro do método OnModelCreating doEntidadesContext:
public class EntidadesContext : DbContext
{
public DbSet<Usuario> Usuarios { get; set; }
// outras propriedades
public override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PessoaFisica>().ToTable("PessoaFisica");
modelBuilder.Entity<PessoaJuridica>().ToTable("PessoaJuridica");
}
}
Agora para testarmos esse mapeamento, precisamos novamente fazer a
migração do banco de dados, porém nosso banco já está com o primeiro
mapeamento de herança aplicado. Então antes de gerarmos a migração para o
novo mapeamento, precisamos desfazer a migração anterior, voltando para a
última migração antes do mapeamento da herança, que é a migração do Many-
To-Many. Para isso, utilizaremos novamente o comando Update-
Database informando para qual migração queremos ir através da opção -
TargetMigration:
Update-Database -TargetMigration:CriaVenda
Com isso, o Entity Framework executará o método Down das
migrações MigraDadosDoUsuario eHerancaEmTabelaUnica.
Agora vamos apagar as migrações MigraDadosDoUsuario e
HerancaEmTabelaUnica e criar a migração para o novo mapeamento da
Herança:
Add-Migration HerancaTabelaPorClasse
Update-Database
Quando inserirmos novamente a pessoa física, o Entity Framework executará
um insert na tabelaUsuario e um na tabela PessoaJuridica. O mesmo acontecerá
quando inserirmos a pessoa jurídica.