3. Estudo de caso com o SQL Server Express 2012
3.1. Introdução
A maioria dos exemplos encontrados online para o Entity Framework são exemplos que utilizam o SQL Server. Isto é bastante normal. É provavelmente o SGBD mais utilizado no mundo .NET empresarial. Seguiremos esta tendência. Os exemplos serão então alargados a todas as bases de dados mencionadas na secção 1.2.
3.2. Instalação das ferramentas
Não iremos descrever a instalação das ferramentas. Fazer isso exigiria um grande número de capturas de ecrã que rapidamente se tornariam obsoletas. Esta é uma tarefa (reconhecidamente nem sempre fácil) que deixamos a cargo do leitor.
Precisamos de instalar as seguintes ferramentas:
- o SGBD SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Descarregue a versão «With Tools», que inclui uma ferramenta de administração com o SGBD:
Assim que o SGBD estiver instalado, iniciamo-lo:
![]() |
![]() |
- [1]: No Menu Iniciar, inicie o «SQL Server Configuration Manager»;
- [2]: Neste gestor, inicie o servidor;
- [3]: Está agora em execução.
Agora inicie a ferramenta de administração do SQL Server:
![]() |
- [1]: No menu Iniciar, inicie o «SQL Server Management Studio»;
- [2]: a ferramenta de administração.
Vamos ligar-nos ao servidor:
![]() |
- Em [1], abra o Explorador de Objetos;
- em [2], introduza os parâmetros de ligação:
- [3]: o servidor (local) (observe os parênteses obrigatórios) refere-se ao servidor instalado na máquina,
- [4]: Selecione a autenticação do Windows. Deve ser administrador no seu computador para que esta ligação seja bem-sucedida,
![]() |
- [6]: Está ligado;
- [7]: Pretende modificar determinadas propriedades do servidor;
![]() |
- [8]: Solicitamos que existam dois modos de autenticação:
- autenticação do Windows, tal como acabou de ser utilizada. Um utilizador do Windows com as permissões adequadas pode então iniciar sessão,
- autenticação do SQL Server. O utilizador deve ser um dos utilizadores registados no sistema de gestão de bases de dados;
Depois de feito isto, podemos validar as propriedades do servidor;
- [9]: Editar as propriedades do utilizador «sa» (administrador do sistema);
![]() |
- em [10], defina uma palavra-passe para o utilizador. No restante deste documento, a palavra-passe é sqlserver2012;
![]() |
- Em [10], conceda-lhes permissão para se conectarem;
- em [11], a ligação está ativada. O assistente pode agora ser confirmado;
- Em [12], saia do servidor.
Agora, voltamos a ligar-nos utilizando o login sa/sqlserver2012:
![]() |
- Em [1], voltamos a ligar-nos;
- em [2], durante a autenticação do SQL Server;
- Em [3], o utilizador é sa;
- em [4], a sua palavra-passe é sqlserver2012;
- em 5, fazemos o login;
![]() |
- em [6], estamos conectados.
Vamos agora criar uma base de dados de demonstração:
![]() |
- em [1], crie uma nova base de dados;
- em [2], nomeie-a como demo;
- em [3], clique em «Validar»;
![]() |
- em [4], a base de dados é criada;
- em 5, crie uma nova tabela na base de dados «demo»;
![]() |
![]() |
![]() |
![]() |
- em [6], definimos uma tabela com duas colunas, ID e NAME;
- em [7], definimos a coluna [ID] como a chave primária;
- em [8], a chave primária é representada por uma chave;
- em [9], a tabela é guardada;
- Em [10], atribuímos-lhe um nome;
- Em [11], para que a tabela apareça na base de dados [demo], é necessário atualizar a base de dados;
- em [12], a tabela [PERSONNES] foi criada com sucesso.
Agora já sabemos o suficiente sobre como utilizar o SQL Server Management Studio.
3.3. O servidor incorporado (localdb)\v11.0
O VS Express 2012 inclui um SQL Server incorporado. Partimos do princípio de que o VS Express 2012 já foi instalado [http://www.microsoft.com/visualstudio/fra/downloads]. Inicie o VS 2012 [1]:
![]() |
Inicie o SQL Server 2012 Management Studio [2] e inicie sessão [3].
![]() |
- Em [4], ligue-se ao servidor (localdb)\v11.0;
- Em 5, utilize a autenticação do Windows;
- Em [6], se a ligação for bem-sucedida, são apresentadas as bases de dados do servidor. Tal como anteriormente, pode criar uma nova base de dados.
Não utilizaremos este servidor incorporado no VS 2012.
3.4. Criação da base de dados a partir de entidades
O Entity Framework 5 Code First permite-lhe criar uma base de dados a partir de entidades. É isso que vamos explorar agora. Utilizando o VS Express 2012, criamos um projeto de consola inicial em C#:
![]() |
![]() |
- em [1], a definição do projeto;
- em [2], o projeto criado.
Todos os nossos projetos precisarão d , a DLL do Entity Framework 5. Adicionamo-la:
![]() |
- em [1], a ferramenta NuGet permite-lhe descarregar dependências;
![]() |
- em [2], descarregamos a dependência do Entity Framework;
- em [3], a referência foi adicionada ao projeto.
Pode saber mais consultando as propriedades da referência adicionada:
![]() |
- em [1], a versão da DLL. É necessária a versão 5;
- em [2], a sua localização no sistema de ficheiros: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll, onde <solution> é a pasta da solução do VS. Todos os pacotes adicionados pelo NuGet irão para a pasta <solution>/packages;
- em [3], foi criado um ficheiro [packages.config]. O seu conteúdo é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>
Apresenta a lista dos pacotes importados pelo NuGet.
Voltemos ao projeto do VS e criemos uma pasta [Models] no projeto:
![]() |
- em [1], adicionando uma pasta ao projeto;
- em [2], ela será denominada [Models].
Continuaremos com esta prática de colocar as nossas definições de entidades na pasta [Models].
Para criar as nossas entidades, utilizaremos a definição de base de dados MySQL 5 utilizada no projeto NHibernate. Vamos rever o papel das entidades EF:
![]() |
As entidades devem refletir as tabelas da base de dados. A camada de acesso aos dados utiliza estas entidades em vez de trabalhar diretamente com as tabelas. Comecemos pela tabela [DOCTORS]:
3.4.1. A entidade [Medecin]
Contém informações sobre os médicos geridos pela aplicação [RdvMedecins].
![]() | ![]() |
- ID: o número de identificação do médico — a chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- LAST_NAME: o apelido do médico
- FIRST NAME: o nome próprio do médico
- TITLE: o seu título (Sra., Sra., Sr.)
Podemos começar com a seguinte classe [Doctor]:
using System;
[Table("MEDECINS", Schema = "dbo")]
namespace RdvMedecins.Entites
{
public class Medecin
{
// data
public int Id { get; set; }
public string Titre { get; set; }
public string Nom { get; set; }
public string Prenom { get; set; }
}
- Linha 3: A classe [Medecin] está associada à tabela [MEDECINS] na base de dados. Esta tabela estará localizada num esquema denominado "dbo".
Colocamos esta classe num ficheiro chamado [Entities.cs] [1]. É aqui que colocaremos todas as nossas entidades.
![]() |
Ainda na pasta [Models], criamos o seguinte ficheiro [Context.cs]:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// the context
public class RdvMedecinsContext : DbContext
{
// the doctors
public DbSet<Medecin> Medecins { get; set; }
}
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
}
}
- linha 8: a classe [RdvMedecinsContext] representará o contexto de persistência, ou seja, o conjunto de entidades geridas pelo ORM. Deve derivar da classe [System.Data.Entity.DbContext];
- linha 11: o campo [Medecins] representa as entidades [Medecin] no contexto de persistência. É do tipo DbSet<Medecin>. Geralmente, há tantos [DbSet]s quantas as tabelas na base de dados, um por tabela;
- linha 15: definimos uma classe [RdvMedecinsInitializer] para inicializar a base de dados criada. Aqui, deriva da classe [DropCreateDataBaseAlways], que, como o próprio nome sugere, elimina a base de dados se esta já existir e, em seguida, recria-a. Isto é útil durante a fase de desenvolvimento da base de dados. O parâmetro da classe [DropCreateDataBaseAlways] é o tipo de contexto de persistência associado à base de dados. Podem ser utilizadas outras classes pai, além de [DropCreateDataBaseAlways], para a classe de inicialização:
- [DropCreateDatabaseIfModelChanges]: recria a base de dados se as entidades tiverem sido alteradas,
- [CreateDatabaseIfNotExists]: cria a base de dados se esta não existir;
Ainda precisamos de criar um programa principal. Será o seguinte [CreateDB_01.cs]:
using System;
using System.Data.Entity;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class CreateDB_01
{
static void Main(string[] args)
{
// we create the
Database.SetInitializer(new RdvMedecinsInitializer());
using (var context = new RdvMedecinsContext())
{
context.Database.Initialize(false);
}
}
}
}
- linha 12: [System.Data.Entity.DataBase] é uma classe que fornece métodos estáticos para gerir a base de dados associada a um contexto de persistência. O método estático [SetInitializer] permite especificar a classe de inicialização da base de dados. Isto não desencadeia a inicialização;
- linha 13: para trabalhar com um contexto de persistência, é necessário instanciá-lo. É isso que se faz aqui. Utiliza-se uma instrução using para que o contexto seja automaticamente fechado quando a instrução terminar. Portanto, na linha 17, o contexto é fechado;
- Linha 15: Acionamos explicitamente a geração da base de dados associada ao contexto de persistência [RdvMedecinsContext]. O parâmetro false indica que esta operação não deve ser realizada se já tiver sido feita para este contexto. Aqui, poderíamos facilmente ter definido o valor como true.
Ao trabalhar com uma base de dados, os parâmetros de ligação são geralmente armazenados no ficheiro [App.config]. Note que, por enquanto, eles não se encontram lá:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
</configuration>
Os elementos acima foram adicionados ao [App.config] quando a dependência do Entity Framework foi adicionada às referências do projeto.
Vamos executar o projeto (Ctrl-F5) após iniciar o SQL Server Express (isto é importante):
![]() | ![]() |
A execução deve ser concluída sem erros. Agora, vamos abrir o SQL Server Management Studio e atualizar a vista:
![]() |
Podemos ver que foi criada uma base de dados com o nome completo da classe [RdvMedecinsContext] e que esta contém uma tabela denominada [dbo.MEDECINS] (o nome que lhe atribuímos) com colunas que correspondem aos nomes dos campos da entidade [Medecin]. Se o código foi executado com sucesso, mas a base de dados acima não aparece, verifique o servidor incorporado (localdb)\v11.0 (ver página 19). Com o VS 2012 Pro, este servidor é utilizado se o SQL Server não estiver ativo quando o código é executado. Com o VS 2012 Express, não é.
Vamos examinar a estrutura da tabela [MEDECINS]:
- ela utiliza os nomes de campo da entidade [Medecin];
- a coluna [Id] é a chave primária. Esta é uma convenção do EF: se a entidade E tiver um campo Id ou Eid (MedecinId), então esta coluna é a chave primária na tabela associada;
- os tipos das colunas na tabela são os dos campos da entidade;
- para as colunas Título, Apelido e Nome próprio, foi utilizado um tipo [nvarchar(max)]. Poderíamos ser mais específicos: 5 caracteres para o título, 30 para o apelido e 30 para o nome próprio;
- As colunas Título, Apelido e Nome próprio podem ter um valor NULL. Vamos alterar isso.
Vamos analisar as propriedades da chave primária [Id]:
![]() |
Em [1], vemos que a chave primária é do tipo [Identity], o que significa que o seu valor é gerado automaticamente pelo SQL Server. Vamos adotar esta estratégia para todos os SGBDs.
Vamos depender menos das convenções do EF, utilizando anotações. O código da entidade em [Entities.cs] passa a ser o seguinte:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// data
[Key]
[Column("ID")]
public int Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Required]
[Column("VERSION")]
public int Version { get; set; }
}
}
- Linhas 2 e 3: As anotações encontram-se no espaço de nomes [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) e no espaço de nomes [System.ComponentModel.DataAnnotations.Schema] (Column). Anotações adicionais podem ser encontradas no URL [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
- linha 11: [Key] designa a chave primária;
- linha 12: [Column] define o nome da coluna correspondente ao campo;
- linha 14: [Required] indica que o campo é obrigatório (SQL NOT NULL);
- linha 15: [MaxLength] define o comprimento máximo da cadeia de caracteres, [MinLength] o seu comprimento mínimo;
Vamos executar o projeto com esta nova definição da entidade [Medecin]. A base de dados resultante é a seguinte:
![]() |
- as colunas têm os nomes que lhes atribuímos;
- a anotação [Required] foi traduzida para SQL NOT NULL;
- A anotação [MaxLength(N)] foi mapeada para um tipo SQL nvarchar(N).
Na aplicação NHibernate, a coluna [VERSION] existia para impedir o acesso simultâneo à mesma linha numa tabela. O princípio é o seguinte:
- Um processo P1 lê uma linha L da tabela [DOCTORS] no momento T1. A linha tem a versão V1;
- Um processo P2 lê a mesma linha L da tabela [DOCTORS] no momento T2. A linha tem a versão V1 porque o processo P1 ainda não confirmou a sua modificação;
- O processo P1 confirma a sua modificação na linha L. A versão da linha L passa então a ser V2 = V1 + 1;
- O processo P2 confirma a sua modificação na linha L. O ORM lança então uma exceção porque o processo P2 tem uma versão V1 da linha L que difere da versão V2 encontrada na base de dados.
Isto é chamado de gestão otimista de concorrência. Com o EF 5, um campo que desempenha esta função deve ter um de dois atributos: [Timestamp] ou [ConcurrencyCheck]. O SQL Server possui um tipo [timestamp]. Uma coluna deste tipo tem o seu valor gerado automaticamente pelo SQL Server sempre que uma linha é inserida ou modificada. Essa coluna pode então ser utilizada para gerir o acesso simultâneo. Voltando ao exemplo anterior, o processo P2 encontrará um timestamp diferente daquele que leu, porque, entretanto, a modificação feita pelo processo P1 terá alterado o mesmo.
A nossa entidade [Doctor] evolui da seguinte forma:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// data
[Key]
[Column("ID")]
public int Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
}
- Linhas 26–28: a nova coluna com o atributo [Timestamp] da linha 27. O tipo de campo deve ser byte[] (linha 28). O nome do campo pode ser qualquer um. Não definimos o atributo [Required] porque a aplicação não fornecerá este valor; em vez disso, será o próprio SGBD a fazê-lo.
Se executarmos o projeto com esta nova entidade, a base de dados evolui da seguinte forma:
![]() |
Temos um último ponto a abordar. O contexto de persistência «sabe» que uma entidade deve ser inserida na base de dados porque a sua chave primária é nula nesse momento. É a inserção na base de dados que irá atribuir um valor à chave primária. Aqui, o tipo int atribuído à chave primária [Id] não é adequado, pois este tipo não aceita o valor nulo. Por isso, atribuímos-lhe o tipo int?, que aceita valores int e o ponteiro nulo. A entidade [Medecin] utilizada será, portanto, a seguinte:
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
...
Ainda precisamos de ver como representar o conceito de uma chave estrangeira entre tabelas numa entidade.
3.4.2. A entidade [Creneau]
A tabela [CRENEAUX] lista os intervalos de tempo em que são possíveis marcações:
![]() |
![]() |
- ID: o número de identificação do intervalo de tempo — a chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- ID_MEDECIN: Número de identificação do médico a quem este horário pertence – chave estrangeira na coluna MEDECINS(ID).
- START_TIME: hora de início do intervalo de tempo
- MSTART: Minuto de início do intervalo de tempo
- HFIN: hora de fim do intervalo
- MFIN: Minutos de fim do intervalo
A segunda linha da tabela [SLOTS] (ver [1] acima) indica, por exemplo, que o intervalo n.º 2 começa às 8h20 e termina às 8h40 e pertence à médica n.º 1 (Sra. Marie PELISSIER).
Com o que sabemos, podemos definir a entidade [Creneau] da seguinte forma em [Entites.cs]:
[Table("CRENEAUX", Schema = "dbo")]
public class Creneau
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[Column("HDEBUT")]
public int Hdebut { get; set; }
[Required]
[Column("MDEBUT")]
public int Mdebut { get; set; }
[Required]
[Column("HFIN")]
public int Hfin { get; set; }
[Required]
[Column("MFIN")]
public int Mfin { get; set; }
[Required]
public virtual Medecin Medecin { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
A única alteração está nas linhas 20–21. O facto de a tabela [CRENEAUX] ter uma chave estrangeira na tabela [MEDECINS] reflete-se na entidade [Creneau] pela presença de uma referência à entidade [Medecin] na linha 21. O nome do campo é irrelevante; apenas o tipo importa. A propriedade deve ser declarada como virtual utilizando a palavra-chave virtual. Isto porque o EF precisa de redefinir todas as chamadas propriedades de navegação — ou seja, aquelas que correspondem a uma chave estrangeira e permitem a navegação entre tabelas.
Para testar a nova entidade, precisamos de fazer algumas alterações em [Context.cs]:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// the context
public class RdvMedecinsContext : DbContext
{
// entities
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
}
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
}
}
A linha 12 reflete o facto de o contexto ter mais uma entidade para gerir. Quando executamos o projeto, obtemos a seguinte nova base de dados:
![]() |
A tabela [CRENEAUX] foi efetivamente criada, e a novidade é a presença das chaves estrangeiras [1] e [2]. Os seus nomes foram gerados a partir dos nomes dos campos correspondentes na entidade (Medecin), com o sufixo «_Id». Para visualizar as propriedades desta chave estrangeira, tentamos modificá-la [3].
![]() |
A captura de ecrã acima mostra que [Medecin_Id] é uma chave estrangeira na tabela [CRENEAUX] e que faz referência à chave primária [ID] na tabela [MEDECINS].
Se criarmos as entidades para uma base de dados existente, a coluna da chave estrangeira não será necessariamente denominada [Medecin_Id]. Para outras colunas, vimos que a anotação [Column] resolveu este problema. Curiosamente, é mais complicado no caso de uma chave estrangeira. Devemos proceder da seguinte forma:
public class Creneau
{
// data
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
- linhas 5-7: criamos um campo do tipo chave estrangeira (int). Utilizando o atributo [Column], especificamos o nome da coluna que será a chave estrangeira na tabela associada à entidade;
- linha 9: adicionamos a anotação [ForeignKey] ao campo de tipo [Medecin]. O argumento desta anotação é o nome do campo (não da coluna) que está associado à coluna de chave estrangeira na tabela.
Ao executar o projeto desta vez, é criada a seguinte tabela:
![]() |
Acima, a coluna da chave estrangeira tem, de facto, o nome que lhe atribuímos. Note-se que os campos:
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
resultaram numa única coluna, a coluna [MEDECIN_ID]. No entanto, a presença do campo [MedecinId] é importante. Ao ler uma linha da tabela [SLOTS], esta receberá o valor da coluna [DOCTOR_ID], ou seja, o valor da chave estrangeira na tabela [DOCTORS]. Isto é frequentemente útil.
O campo [Medecin] acima reflete a relação muitos-para-um que liga a entidade [Creneau] à entidade [Medecin]. Vários objetos [Slot] estão ligados ao mesmo [Doctor]. A relação inversa — em que um único objeto [Doctor] está associado a vários objetos [Slot] — pode ser modelada utilizando um campo adicional na entidade [Doctor]:
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
...
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
Na linha 8, adicionámos o campo [Slots], que é uma coleção de objetos [Slot]. Este campo dar-nos-á acesso a todos os horários disponíveis do médico.
Quando executamos o projeto novamente, vemos que a tabela [DOCTORS] não sofreu alterações:
![]() |
Não foram adicionadas colunas. A relação de chave estrangeira entre a tabela [CRENEAUX] e a tabela [MEDECINS] é suficiente para que o EF gere os campos relacionados:
public class Medecin
{
...
public ICollection<Creneau> Creneaux { get; set; }
...
}
public class Creneau
{
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
Já sabemos o básico. Podemos terminar criando as outras duas entidades.
3.4.3. As entidades [Client] e [Appointment]
Com o que aprendemos, podemos escrever as entidades [Client] e [Appointment]. A entidade [Client] contém informações sobre os clientes geridos pela aplicação [DoctorAppointments].
![]() | ![]() |
- ID: o número de identificação do cliente — a chave primária da tabela
- VERSION: número que identifica a versão da linha na tabela. Este número é incrementado em 1 cada vez que é feita uma alteração na linha.
- LAST_NAME: o apelido do cliente
- FIRST NAME: o nome próprio do cliente
- TITLE: o seu título (Sra., Sra., Sr.)
A entidade [Cliente] poderia ser a seguinte:
[Table("CLIENTS", Schema = "dbo")]
public class Client
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// customer rvs
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
A classe [Client] é quase idêntica à classe [Doctor]. Podem derivar da mesma classe pai. O novo elemento encontra-se na linha 21. Reflete o facto de um cliente poder ter várias consultas e deriva da presença de uma chave estrangeira da tabela [RVS] para a tabela [CLIENTS].
A entidade [Rv] representa uma consulta:
![]() |
- ID: número que identifica de forma única a consulta – chave primária
- DAY: dia da consulta
- SLOT_ID: intervalo horário da consulta – chave estrangeira na coluna [ID] da tabela [SLOTS] – determina tanto o intervalo horário como o médico envolvido.
- CLIENT_ID: ID do cliente para quem a consulta foi marcada – chave estrangeira na coluna [ID] da tabela [CLIENTS]
A entidade [Rv] poderia ser a seguinte:
[Table("MEDECINS", Schema = "dbo")]
public class Rv
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[Column("JOUR")]
public DateTime Jour { get; set; }
[Column("CLIENT_ID")]
public int ClientId { get; set; }
[ForeignKey("ClientId")]
[Required]
public virtual Client Client { get; set; }
[Column("CRENEAU_ID")]
public int CreneauId { get; set; }
[ForeignKey("CreneauId")]
[Required]
public virtual Creneau Creneau { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
- linhas 5-7: chave primária;
- linhas 8-10: data do compromisso;
- linhas 11-12: a chave estrangeira da tabela [RVS] para a tabela [CLIENTS];
- linhas 13–15: o cliente com a marcação;
- linhas 16–17: a chave estrangeira da tabela [RVS] para a tabela [CRENEAUX];
- linhas 18–20: o intervalo de tempo da marcação;
- Linhas 21-23: o campo de controlo de acesso simultâneo.
Na linha 17, vemos uma relação muitos-para-um: um único intervalo de tempo pode corresponder a vários compromissos (não no mesmo dia). A relação inversa pode ser refletida na entidade [Creneau]:
public class Creneau
{
// niche Rvs
public ICollection<Rv> Rvs { get; set; }
...
}
Linha 4: a coleção de compromissos agendados para este intervalo de tempo.
Quando o projeto é executado, a base de dados gerada é a seguinte:
![]() |
As tabelas [DOCTORS] e [SLOTS] não sofreram alterações. As tabelas [CLIENTS] e [APPs] são as seguintes:
![]() | ![]() |
Era isto que se esperava. Ainda temos alguns detalhes para resolver:
3.4.4. Definir o nome da base de dados
Para definir o nome da base de dados gerada pelo EF, vamos utilizar uma cadeia de ligação definida no [App.config]. Este ficheiro de configuração é alterado da seguinte forma:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
<!-- connection chain on base -->
<connectionStrings>
<add name="RdvMedecinsContext"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- the factory provider -->
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider"
invariant="System.Data.SqlClient"
description=".Net Framework Data Provider for SqlServer"
type="System.Data.SqlClient.SqlClientFactory, System.Data,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/>
</DbProviderFactories>
</system.data>
</configuration>
- linhas 15–19: a cadeia de ligação à base de dados;
- linha 16: o atributo [name] utiliza o nome da classe [RdvMedecinsContext] utilizada para o contexto de persistência. É importante lembrar-se disto. Esta restrição pode ser contornada no construtor do contexto:
// manufacturer
public RdvMedecinsContext()
: base("monContexte")
{
}
Neste caso, podemos ter name= "myContext". É isso que utilizaremos no resto do documento.
- Linha 17: a cadeia de ligação. [Data Source]: o nome do servidor que aloja o SGBD; [Initial Catalog]: o nome da base de dados, neste caso [rdvmedecins-ef]; [User Id]: o proprietário da ligação; [Password]: a palavra-passe do proprietário. O leitor deve adaptar esta cadeia ao seu ambiente;
- Linhas 21–29: definem um [DbProviderFactory]. Não sei o que isto é. A julgar pelo nome, pode ser uma classe utilizada para gerar a camada [ADO.NET] que separa o EF do SGBD:
![]() |
Na verdade, estas linhas são desnecessárias para o SQL Server, mas tive de as adicionar para outros SGBDs. Por isso, estou a incluí-las aqui para referência. Não causam quaisquer problemas. O único ponto importante é a versão na linha 27. É a versão da DLL [System.Data] listada nas referências do projeto:
![]() |
Pronto. Estamos prontos. Executamos o projeto e obtemos a seguinte base de dados [rdvmedecins-ef]:
![]() |
Esta será a nossa base de dados final. Resta apenas preenchê-la com dados.
3.4.5. Preenchimento da base de dados
A classe de inicialização da base de dados pode ser utilizada para inserir dados na mesma:
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
protected override void Seed(RdvMedecinsContext context)
{
base.Seed(context);
// initialize the base
// our customers
Client[] clients ={
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
foreach (Client client in clients)
{
context.Clients.Add(client);
}
// the doctors
Medecin[] medecins ={
new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
};
foreach (Medecin medecin in medecins)
{
context.Medecins.Add(medecin);
}
// time slots
Creneau[] creneaux ={
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
};
foreach (Creneau creneau in creneaux)
{
context.Creneaux.Add(creneau);
}
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
}
}
}
- linha 6: a inicialização ocorre no método [Seed]. Este método existe na classe pai. É redefinido aqui. O argumento é o contexto de persistência da aplicação [RdvMedecinsContext];
- linha 8: o argumento é passado para a classe pai; é provável que a classe pai abra o contexto de persistência que lhe foi passado, uma vez que este contexto já não precisa de ser aberto posteriormente;
- linhas 11–16: criação de 4 clientes;
- linhas 17–20: estes são adicionados ao contexto de persistência, mais especificamente aos seus médicos. Repare no método [Add] que permite isto. Recorde a definição do contexto:
public class RdvMedecinsContext : DbContext
{
// entities
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...
Diz-se também que os Clientes foram anexados ao contexto, ou seja, são agora geridos pelo EF. Anteriormente, estavam desanexados. Existiam como objetos, mas não eram geridos pelo EF;
- linhas 21–27: criação de 4 médicos;
- linhas 28–31: adição dos mesmos ao contexto de persistência;
- linhas 33–70: criação de intervalos de tempo. Linhas 34–57 para o médico medecins[0], linhas 58–69 para o médico medecins[1]. Os outros médicos não têm intervalos de tempo;
- linhas 71–74: estes intervalos de tempo são colocados no contexto de persistência;
- linha 76: criação de uma consulta para o primeiro cliente com o primeiro intervalo de tempo e colocação no contexto de persistência.
Quando o projeto é executado, obtém-se a seguinte base de dados:
![]() | ![]() |
Acima, vemos a tabela [CLIENTS] preenchida.
3.4.6. Modificação de entidades
Atualmente, as classes [Doctor] e [Client] são quase idênticas. Na verdade, se removemos os campos adicionados para gestão de persistência com o EF 5, elas são idênticas. Vamos fazer com que derivem de uma classe [Person]. Estas duas entidades passam então a ser as seguintes:
// a person
public abstract class Personne
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
public override string ToString()
{
return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
...
}
// utility
private string dump(byte[] timestamp)
{
...
}
}
[Table("MEDECINS", Schema = "dbo")]
public class Medecin : Personne
{
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin {0}", base.ToString());
}
}
[Table("CLIENTS", Schema = "dbo")]
public class Client : Personne
{
// customer rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Client {0}", base.ToString());
}
}
Quando o projeto é executado, é gerada a mesma base de dados. O EF 5 mapeou cada uma das classes mais baixas na hierarquia de herança para uma tabela separada. Na verdade, o EF 5 possui diferentes estratégias de geração de tabelas para representar a herança de entidades d . Não iremos abordá-las aqui. Por exemplo, pode ler « Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type» na URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].
Vamos agora utilizar esta versão das entidades.
3.4.7. Adicionar restrições à base de dados
Há mais um detalhe a abordar. A tabela [RVS] para compromissos é a seguinte:
![]() |
Esta tabela deve ter uma restrição de unicidade: para um determinado dia, o horário de um médico só pode ser reservado uma vez para uma consulta. Em termos da tabela, isto significa que o par (DAY, SLOT_ID) deve ser único. Não sei se esta restrição pode ser expressa diretamente no código, seja nas entidades ou no contexto. É provável, mas ainda não verifiquei. Vamos adotar uma abordagem diferente. Vamos utilizar um cliente de gestão do SQL Server para adicionar esta restrição.
Usando o «SQL Server Management Studio», não encontrei uma maneira simples de adicionar esta restrição além de executar a instrução SQL que a cria:
![]() |
- em [1] criamos uma consulta SQL para a base de dados [rdvmedecins-ef];
- em [2], a consulta SQL que cria a restrição de unicidade;
- em [3], a execução desta consulta criou um novo índice na tabela [RVS].
Existem outras ferramentas de administração do SQL Server. Aqui, utilizaremos a ferramenta gratuita EMS SQL Manager for SQL Server [http://www.sqlmanager.net/fr/products/mssql/manager/download]. Depois de instalada, iniciamo-la:
![]() |
- em [1], registamos uma base de dados;
- em [2], ligamo-nos ao servidor (local);
- em [3], utilizando a autenticação do SQL Server;
- em [4], com o nome de utilizador sa;
- em 5, e a palavra-passe sqlserver2012;
- em [6], avançamos para o passo seguinte;
![]() |
- em [7], selecione a base de dados [rdvmedecins-ef];
- em [8], conclua o assistente;
- em [9], a base de dados aparece na árvore de bases de dados. Ligue-se a ela [10];
- em [11], está ligado.
O "SQL Manager Lite for SQL Server" permite-lhe criar a restrição única na tabela [RVS].
![]() |
- Em [1], pode ver a restrição única que criámos anteriormente;
- Em [2], elimine-a;
- Em [3], o índice correspondente a esta restrição de unicidade desapareceu.
Recriamos a restrição eliminada:
![]() |
- Em [1], criamos um novo índice para a tabela [RVS];
- em [2], atribuímos-lhe um nome;
- em [3], trata-se de uma restrição de exclusividade;
- em [4], nas colunas DAY e SLOT_ID;
O separador DDL fornece o código SQL a ser executado:
![]() |
- em [6], compilamos a instrução SQL;
![]() |
- em [7], confirmamos;
- em [8], o novo índice apareceu.
A interface fornecida pelo «SQL Manager Lite for SQL Server» é semelhante à do «SQL Server Management Studio». Estão disponíveis interfaces semelhantes para as bases de dados Oracle, PostgreSQL, Firebird e MySQL. Por conseguinte, continuaremos com esta família de ferramentas de administração de bases de dados.
Para aceder a informações sobre uma tabela, basta clicar duas vezes nela:
![]() |
As informações sobre a tabela selecionada estão disponíveis em separadores. Acima, vemos o separador [Campos] da tabela [CLIENTES]. O separador [Dados] apresenta o conteúdo da tabela:

3.4.8. A base de dados final
Agora temos a nossa base de dados final. Exportamos o seu script SQL para que possamos regenerá-la, se necessário.
![]() |
- em [1], início do assistente;
- em [2], o servidor;
- em [3], a base de dados a exportar;
![]() |
- em [4], especifique o nome do ficheiro onde o script SQL será guardado;
- em 5, especifique a sua codificação;
- em [6], especifique o que pretende extrair (tabelas, restrições, dados);
![]() |
- em [7], pode ajustar o script que será gerado;
- em [8], conclua o assistente.
O script foi gerado e carregado no editor de scripts. Pode visualizar o código SQL gerado. Iremos reconstruir a base de dados utilizando este script.
![]() |
- Em [1], elimine a base de dados;
- Em [2] e [3], recriamo-la;
![]() |
- Em [4], inicie sessão;
- em 5, execute o script SQL para criar a base de dados;
![]() |
- em [6], guardamos no «SQL Manager»;
- Em [7], ligamo-nos à base de dados que acabou de ser criada;
![]() |
- em [8], a base de dados não tem, de momento, quaisquer tabelas;
- Em [9a], abra um editor de scripts SQL;
![]() |
- Em [9b], abra o script SQL criado anteriormente;
- Em [10], execute-o;
![]() |
- em [11], as tabelas foram criadas;
- em [12], elas são preenchidas;
![]() |
- em [14], vemos a restrição única que criámos para a tabela [RVS].
Vamos agora trabalhar com esta base de dados existente. Se for destruída ou corrompida, sabemos como regenerá-la.
3.5. Trabalhar com a base de dados utilizando o Entity Framework
Iremos:
- adicionar, eliminar e modificar elementos da base de dados;
- consultar a base de dados utilizando o LINQ to Entities;
- gerir o acesso simultâneo ao mesmo elemento da base de dados;
- compreender os conceitos de Lazy Loading e Eager Loading;
- descobrir que as atualizações da base de dados através do contexto de persistência ocorrem dentro de uma transação.
3.5.1. Eliminar itens do contexto de persistência
Temos uma base de dados preenchida. Vamos esvaziá-la. Criamos uma nova classe [Erase.cs] no projeto atual [1]:
![]() |
A classe [Erase] é a seguinte:
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Erase
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// empty the current base
// our customers
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
// the doctors
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// save the persistence context
context.SaveChanges();
}
}
}
}
- linha 9: as operações num contexto de persistência são sempre realizadas dentro de um bloco [using]. Isto garante que, quando o bloco [using] terminar, o contexto tenha sido fechado;
- linha 13: percorremos o contexto de clientes [context.Clients]. Todos os clientes na base de dados serão colocados no contexto de persistência;
- linha 15: Para cada um deles, executamos a operação [Remove], que os remove do contexto. Na verdade, eles ainda estão no contexto, mas num estado «removido»;
- linhas 18–21: fazemos o mesmo para os médicos;
- linha 23: guardamos o contexto de persistência na base de dados.
Ao guardar o contexto na base de dados, as entidades no contexto que:
- possuem uma chave primária nula são sujeitas a uma operação SQL INSERT;
- estejam no estado «eliminado» são sujeitas a uma operação SQL DELETE;
- estejam no estado «modificado» são sujeitas a uma operação SQL UPDATE;
Como veremos mais adiante, estas operações SQL são executadas no âmbito de uma transação. Se alguma delas falhar, tudo o que foi feito anteriormente é revertido.
Vamos definir o programa [Erase] como o novo ponto de partida para o projeto [1] e, em seguida, executar o projeto.
![]() |
Vamos verificar a base de dados. Veremos que todas as tabelas estão vazias [2]. Isto é surpreendente, uma vez que apenas solicitámos a eliminação dos médicos e dos clientes. Foi através do mecanismo das chaves estrangeiras que as outras tabelas foram esvaziadas em cascata.
A definição da chave estrangeira da tabela [CRENEAUX] para a tabela [MEDECINS] foi definida da seguinte forma pelo fornecedor EF 5:
![]() |
- Em [1], selecione a tabela [CRENEAUX];
- em [2], selecione o separador «chaves estrangeiras»;
- em [3], edite a chave estrangeira individual;
![]() |
- em [4], no separador DDL, a definição SQL da restrição de chave estrangeira;
- em 5, a cláusula ON DELETE CASCADE garante que a eliminação de um médico resulte na eliminação dos intervalos de tempo associados a ele.
As restrições de chave estrangeira para a tabela [RVS] são definidas de forma semelhante:
- Linhas 1-6: A eliminação de um cliente eliminará também os compromissos a ele associados;
- Linhas 1-6: A eliminação de um intervalo de tempo eliminará também todos os compromissos a ele associados.
3.5.2. Adicionar itens ao contexto de persistência
Agora que esvaziamos a base de dados, vamos preenchê-la novamente. Adicionamos o programa [Fill.cs] [1] ao projeto.
![]() |
O programa [Fill.cs] é o seguinte:
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Fill
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// reset it
// our customers
Client[] clients ={
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
foreach (Client client in clients)
{
context.Clients.Add(client);
}
// the doctors
Medecin[] medecins ={
new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
};
foreach (Medecin medecin in medecins)
{
context.Medecins.Add(medecin);
}
// time slots
Creneau[] creneaux ={
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
};
foreach (Creneau creneau in creneaux)
{
context.Creneaux.Add(creneau);
}
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// save the persistence context
context.SaveChanges();
}
}
}
}
- linha 10: abrimos o contexto de persistência;
- linhas 13–20: as linhas das tabelas [CLIENTS] e [DOCTORS] são adicionadas ao contexto e, em seguida, removidas dele. Acabámos de ver que isto esvaziou completamente a base de dados;
- linhas 22–88: são adicionados elementos ao contexto de persistência. Todos eles têm uma chave primária nula. Serão, portanto, inseridos na base de dados;
- linha 90: as alterações feitas no contexto são sincronizadas com a base de dados. A base de dados será sujeita a uma série de operações SQL DELETE, seguidas de uma série de operações SQL INSERT;
Definimos o programa [Fill] como o novo objeto inicial para o projeto [1] e, em seguida, executamo-lo.
![]() |
Podemos ver em [2] que as tabelas foram preenchidas.
3.5.3. Exibição do conteúdo da base de dados
Vamos agora exibir o conteúdo da base de dados utilizando consultas LINQ to Entities. O LINQ (Language-Integrated Query) foi introduzido com o .NET Framework 3.5 em 2007. Funciona como uma extensão das linguagens .NET, o que significa que está integrado na linguagem e a sua sintaxe é validada pelo compilador. Permite-lhe consultar várias coleções utilizando uma sintaxe semelhante à do SQL (Structured Query Language) para consultas à base de dados. Existem diferentes versões do LINQ:
- LINQ to Objects, para consultar coleções na memória;
- LINQ to XML, para consultar XML;
- LINQ to Entity, para consultar bases de dados;
O LINQ depende de inúmeras extensões das linguagens .NET. Estas podem ser utilizadas fora do LINQ. Não as apresentaremos aqui, mas forneceremos simplesmente duas referências onde o leitor pode encontrar uma descrição aprofundada do LINQ:
- LINQ in Action, de Fabrice Marguerie, Steve Eichert e Jim Wooley, publicado pela Manning;
- LINQ Pocket Reference, de Joseph e Ben Albahari, publicado pela O’Reilly.
Li o primeiro e achei-o excelente. Ainda não li o segundo, mas li “C# 3.0 in a Nutshell”, dos mesmos autores, quando o LINQ foi lançado. Achei esse livro muito acima da média dos livros que costumo ler. Parece que os outros livros destes dois autores são do mesmo calibre. Também iremos utilizar o LINQPad, uma ferramenta de aprendizagem do LINQ criada por Joseph Albahari.
Vamos apresentar as entidades na base de dados. Para tal, adicionaremos dois métodos de apresentação às respetivas classes. Comecemos pela entidade [Doctor]:
// a doctor
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
// utility
private string dump(byte[] timestamp){
string str = "";
foreach (byte b in timestamp)
{
str += b;
}
return str;
}
}
- linhas 27–30: o método ToString da classe. Note que ele não exibe a coleção da linha 21;
- linhas 32–37: o método ShortIdentity, que faz o mesmo.
Aqui, precisamos de explicar os conceitos de carregamento preguiçoso (Lazy) e antecipado (Eager) para avaliar o impacto dos dois métodos anteriores. Vimos que uma entidade pode ter dependências de outra entidade. Estas dependências são de dois tipos:
- um-para-muitos, como acima, em que um médico está ligado a vários horários;
- muitos-para-um, como na entidade [Slot] abaixo, em que um ou mais horários estão ligados ao mesmo médico;
public class Creneau
{
// data
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
Quando as dependências são carregadas ao mesmo tempo que as entidades às quais estão associadas, isto denomina-se Carregamento Antecipado. Caso contrário, denomina-se Carregamento Pós-vinculação: as dependências são carregadas apenas quando são referenciadas pela primeira vez. Por predefinição, o EF 5 utiliza o Carregamento Pós-vinculação: as dependências não são carregadas ao mesmo tempo que a entidade.
Vejamos o nosso método [ToString] acima:
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
O método [ToString] não exibe a dependência [Slots] na linha 2. Se o fizesse, teria forçado o carregamento de todos os slots do médico antes da execução. Foi para evitar este carregamento dispendioso que a dependência não foi incluída na assinatura da entidade. De um modo geral, incluiremos duas assinaturas em cada entidade:
- um método ToString que exibirá a entidade e quaisquer dependências um-para-muitos. Conforme acabado de explicar, isto irá desencadear o carregamento da dependência;
- um método ShortIdentity que não fará referência a quaisquer dependências. Portanto, nenhuma dependência será carregada;
Os métodos de exibição para as outras entidades serão os seguintes:
A entidade [Client]:
public class Client
{
// data
...
// customer rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
}
- linhas 9–12: o método [ToString] não exibe a dependência na linha 6;
A entidade [Creneau]:
public class Creneau
{
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
// niche Rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
}
}
- linha 16: o método [ToString] faz referência à dependência na linha 9. Isto irá forçar o seu carregamento;
- linha 11: a dependência [Rvs] não é referenciada. Não será carregada;
- linhas 21-22: o método [ShortIdentity] já não faz referência à referência [Medecin] da linha 9. Por conseguinte, não será carregada.
A entidade [Rv]:
public class Rv
{
// data
...
[Column("CLIENT_ID")]
public int ClientId { get; set; }
[ForeignKey("ClientId")]
[Required]
public virtual Client Client { get; set; }
[Column("CRENEAU_ID")]
public int CreneauId { get; set; }
[ForeignKey("CreneauId")]
[Required]
public virtual Creneau Creneau { get; set; }
// signature
public override string ToString()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
}
}
- linhas 17–20: o método [ToString] faz referência às dependências nas linhas 9 e 14. Isto irá forçar o seu carregamento;
- linhas 17–20: o método [ShortIdentity] impede isso, pelo que as dependências não serão carregadas.
Em conclusão, devemos prestar atenção aos métodos [ToString] das entidades. Se não prestarmos atenção a isto, a exibição de uma tabela pode carregar metade da base de dados, caso a tabela tenha muitas dependências.
Com isso explicado, escrevemos o seguinte código novo [Dump.cs]:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class Dump
{
static void Main(string[] args)
{
// base dump
using (var context = new RdvMedecinsContext())
{
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// the doctors
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// time slots
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// dates
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
Iremos explicar as linhas 17–21, que apresentam as entidades [Client]. A explicação fornecida aplica-se igualmente às outras entidades.
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
- linha 3: a palavra-chave var foi introduzida com o C# 3.0. Permite evitar especificar o tipo exato de uma variável. O compilador infere então o tipo a partir do tipo da expressão atribuída à variável;
- linha 3: a expressão atribuída à variável clients é uma consulta LINQ to Entities. Inclui palavras-chave SQL portadas para o LINQ. A sintaxe utilizada aqui é a seguinte:
from variable in DbSet select variable
Uma sintaxe LINQ mais geral é
from variable in collection select variable
A coleção será percorrida e, para cada elemento nela, a variável será avaliada. Isto só acontece quando a variável [clients] da linha 3 é iterada pelo ciclo for/each nas linhas 4–7. Até que isto aconteça, a variável [clients] é apenas uma consulta não avaliada;
- linha 4: a consulta [clients] é iterada. Isto forçará a avaliação da consulta. As linhas da tabela [CLIENTS] serão introduzidas no contexto de persistência uma a uma;
- linha 6: o método [ToString] da entidade [Client] é utilizado para a apresentação. Não são carregadas quaisquer dependências;
Vamos passar às seguintes linhas de código:
- Linhas 24–28: as linhas da tabela [DOCTORS] são trazidas para o contexto de persistência e exibidas. Nenhuma dependência é carregada;
- linhas 31–35: as linhas da tabela [SLOTS] são trazidas para o contexto de persistência e exibidas. Vimos que o método [ToString] desta entidade exibe a dependência [Doctor]. No entanto, esta já está carregada. Portanto, não haverá recarregamento;
- linhas 38–42: as linhas da tabela [RVS] são trazidas para o contexto de persistência e exibidas. Vimos que o método [ToString] desta entidade exibe as dependências [Client] e [Slot]. No entanto, estas já estão carregadas. Portanto, não haverá novo carregamento.
Note-se que a ordem de exibição não é neutra. Se tivéssemos querido exibir as entidades [Rv] primeiro, o seu método [ToString] teria desencadeado o carregamento das entidades [Client] e [Creneau] ligadas a estes compromissos. As outras não teriam sido carregadas. Teriam sido carregadas mais tarde noutra vista. Isto tem um impacto no desempenho. O código anterior requer quatro instruções SQL para exibir todas as entidades. Agora, suponhamos que consultamos primeiro a tabela [RVS] de compromissos. É necessária uma primeira consulta SQL para a tabela [RVS]. Em seguida, o método [ToString] da entidade [Rv] irá acionar o potencial carregamento das entidades [Client] e [Slot] associadas. É necessária uma consulta SQL para cada uma. Assumindo que existem N2 clientes e N3 intervalos de tempo e que todas estas entidades são referenciadas na tabela [RVS], a sua exibição exigirá 1+N2+N3 consultas SQL. Portanto, o desempenho é inferior ao da versão que analisámos. Para apresentar a tabela [RVS] com as suas dependências, seria necessária uma junção de tabelas. Isto pode ser conseguido utilizando o LINQ. Voltaremos a este assunto com um exemplo. Por agora, lembremo-nos de que devemos prestar atenção às consultas SQL subjacentes ao nosso código LINQ.
Configuramos o projeto para executar este novo código [1] e [2] e, em seguida, executamo-lo:
![]() |
A saída da consola é a seguinte:
3.5.4. Aprender LINQ com o LINQPad
Acima, utilizámos consultas LINQ to Entity para apresentar o conteúdo das tabelas da base de dados. Joseph Albahari criou um programa para o ajudar a aprender as diferentes formas de LINQ. Vamos apresentá-lo agora.
O LINQPad está disponível no seguinte URL [http://www.linqpad.net/]. Depois de instalado, iniciamo-lo [1]:
![]() |
Os principiantes em LINQ podem começar com os exemplos na guia [Samples] [2], que oferece uma grande variedade de exemplos. Vamos selecionar o exemplo [3], que se abre numa nova janela [4]. O código completo do exemplo é o seguinte:
// Now for a simple LINQ-to-objects query expression (notice no semicolon):
from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word
// Feel free to edit this... (no-one's watching!) You'll be prompted to save any
// changes to a separate file.
//
// Tip: You can execute part of a query by highlighting it, and then pressing F5.
As linhas 3–5 são um exemplo de uma consulta LINQ to Objects. A consulta LINQ segue a sintaxe:
from variable in collection orderby élément1 select élément2
- variável refere-se ao elemento atual na coleção. No nosso exemplo, esta coleção é a lista de palavras resultante da divisão da string;
- a coleção é ordenada de acordo com o parâmetro elemento1 de orderby. No nosso exemplo, a coleção de palavras será ordenada por comprimento;
- a palavra-chave select especifica o que queremos extrair da variável do elemento atual na coleção. No nosso exemplo, será a palavra.
Vamos executar esta consulta LINQ:
![]() |
- em [1]: uma expressão LINQ é executada premindo [F5] ou utilizando o botão Executar;
- em [2]: o resultado. As palavras são apresentadas por ordem de comprimento. Este exemplo simples demonstra o poder do LINQ;
- em [3], pode descarregar outros exemplos, incluindo os do livro «LINQ in Action» [4];
![]() |
- em 5, escolhemos um exemplo do livro;
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
// Group words by length
var groups =
from word in words
orderby word ascending
group word by word.Length into lengthGroups
orderby lengthGroups.Key descending
select new { Length = lengthGroups.Key, Words = lengthGroups };
// Print each group out
foreach (var group in groups)
{
Console.WriteLine("Words of length " + group.Length);
foreach (string word in group.Words)
Console.WriteLine(" " + word);
}
- linha 4: uma nova consulta LINQ com novas palavras-chave;
- linha 5: a coleção consultada é a matriz de palavras da linha 1;
- linha 6: a coleção é ordenada por ordem alfabética por palavra;
- linha 7: a coleção é agrupada (por palavra-chave) numa nova coleção chamada lengthGroups. lengthGroups.Key representa o fator de agrupamento (por palavra-chave), neste caso o comprimento das palavras. lengthGroups agrupa palavras com o mesmo fator de agrupamento, ou seja, o mesmo comprimento;
- linha 8: a coleção lengthGroups é ordenada pela chave de agrupamento em ordem decrescente, ou seja, aqui pelo tamanho decrescente das palavras;
- linha 9: a partir desta coleção, são criados novos objetos (classes anónimas) com dois campos:
- Length: o comprimento das palavras,
- Words: as palavras com esse comprimento;
Aqui, podemos ver particularmente a vantagem da palavra-chave var na linha 4. Como utilizámos uma classe anónima na linha 9, não podemos especificar o tipo da variável groups. O compilador, no entanto, atribuirá um nome interno à classe anónima e utilizá-lo-á para tipar a variável groups. Poderá então determinar se a variável groups é utilizada corretamente
- Linha 12: Iteração sobre a consulta da linha 4. Só neste ponto é que ela é avaliada. Recorde-se que a sua execução produzirá uma coleção de objetos, especificada na linha 9;
- Linha 14: Exibimos a propriedade Length do elemento atual, ou seja, o comprimento das palavras;
- Linhas 15–17: Exibimos cada elemento da coleção Words, ou seja, o conjunto de palavras com o comprimento exibido anteriormente.
Quando executamos esta consulta, obtemos o seguinte resultado no LINQPad:
![]() |
Agora que vimos alguns exemplos de consultas [LINQ to Object], vamos analisar as consultas [LINQ to Entity] que nos permitirão consultar bases de dados. Primeiro, vamos ligar-nos à base de dados SQL Server que criámos e preenchemos:
![]() |
- em [1], adicionamos uma ligação a uma base de dados;
- em [2], o meio de acesso à fonte de dados. Para aceder à base de dados SQL Server, utilizaremos o [LINQPad Driver];
- em [3], também é possível recuperar um contexto de persistência [DbContext] definido num assembly .exe ou .dll (opção 3). Infelizmente, até à data (8 de outubro de 2012), o Entity Framework 5 não é suportado;
- em [4], é possível descarregar controladores para SGBDs que não sejam o SQL Server;
- Em 5, iremos descarregar o controlador para os SGBDs MySQL e Oracle;
![]() |
- em [6], o controlador descarregado;
- em [7], ligamo-nos a uma base de dados SQL Server;
![]() |
- em [8], a base de dados está no servidor local;
- em [9], ligamo-nos utilizando as credenciais sa / sqlserver2012;
- em [10], à base de dados [rdvmedecins-ef] que criámos;
- Em [11], pode testar a ligação;
- Em [12], conclua o assistente;
- Em [13], a ligação aparece no LINQPad.
As entidades foram criadas a partir da tabela [rdvmedecins-ef]. São as seguintes:
![]() |
- Em [1], [CLIENTS] representa o conjunto de entidades [Client]. Cada entidade possui:
- as propriedades (ID, TITLE, LAST_NAME, FIRST_NAME, TIMESTAMP),
- uma relação um-para-muitos [CLIENTRVS];
- Em [2], [CRENEAUXes] representa o conjunto de entidades [Creneau]. Cada entidade possui:
- as propriedades (ID, HORA_INICIO, HORA_MINIMA, HORA_FIM, HORA_MAXIMA, ID_MEDICO, CARIMBO_TEMPORAL),
- uma relação um-para-muitos [CRENEAURVS],
- uma relação muitos-para-um [DOCTOR];
- em [3], a entidade [MEDECINS] representa o conjunto de entidades [Medecin]. Cada entidade possui:
- as propriedades (ID, TITLE, LAST_NAME, FIRST_NAME, TIMESTAMP),
- uma relação um-para-muitos [DOCTOR-SLOTS];
- em [4], a entidade [RVS] representa o conjunto de entidades [Rv]. Cada entidade tem:
- as propriedades (ID, DIA, ID_CLIENTE, ID_SLOT, TIMESTAMP),
- uma relação muitos-para-um [CLIENT],
- uma relação muitos-para-um [SLOT].
Note que os nomes das propriedades acima são diferentes dos nomes que temos usado até agora. Isso não importa. O nosso objetivo é apenas aprender os princípios básicos das consultas a bases de dados.
Vamos ver como podemos consultar esta base de dados de entidades. Por exemplo, queremos uma lista de médicos ordenada pelo seu TÍTULO e APELIDO:
![]() |
- em [1], criamos uma nova consulta;
- em [2], o texto da consulta;
![]() |
- em [3], o resultado da consulta;
- em [4], a mesma consulta utilizando expressões lambda. Uma consulta com expressões lambda é menos legível do que uma consulta de texto, e poderá preferir evitá-las. No entanto, por vezes são indispensáveis, pois permitem certas coisas que as consultas de texto não permitem. Uma expressão lambda denota uma função com um parâmetro de entrada a e um parâmetro de saída b, na forma a=>b. O método OrderBy acima aceita uma função lambda como seu único parâmetro. Isto fornece o parâmetro pelo qual uma coleção deve ser ordenada. Assim, MEDECINS.OrderBy(m=>m.TITRE) é a lista de médicos ordenada pelos seus títulos. A instrução deve ser lida como um pipeline sobre uma coleção. A coleção de médicos é fornecida como entrada para o método OrderBy. Este método irá processar as entidades [Doctor] uma a uma. Na expressão lambda m=>m.TITLE, m representa a entrada para a função lambda. Pode ser nomeada como desejado. Aqui, a entrada para a função lambda será uma entidade [Doctor]. A função m=>m.TITLE lê-se da seguinte forma: se eu chamar m à minha entrada (uma entidade [Doctor]), então a minha saída é m.TITLE, ou seja, o título do médico. MEDECINS.OrderBy(m=>m.TITRE) é, por sua vez, uma coleção, a coleção de médicos ordenados pelos seus títulos. Esta nova coleção pode alimentar outro método, neste exemplo o método ThenBy. Este método funciona com o mesmo princípio. É utilizado para especificar parâmetros adicionais para ordenar a coleção.
Ler o código lambda equivalente ao código de texto que costumamos escrever é uma boa maneira de aprender;
![]() |
- em 5, a consulta SQL enviada à base de dados. Mais uma vez, vamos ler este código com atenção. Permite-nos avaliar o custo real de uma consulta LINQ.
A seguir, apresentamos alguns exemplos de consultas LINQ. Para cada uma, mostramos os resultados apresentados e o código lambda e SQL equivalente. Para compreender estas consultas, devemos recordar as relações muitos-para-um que ligam as entidades entre si. É através delas que navegamos de uma entidade para outra. São chamadas propriedades de navegação.
![]() |
// Clientes cujo título é "Sr." ordenados por ordem decrescente de nome
Resultados:
![]() |
LINQ | |
Lambda | |
SQL | |
// todos os horários associados ao médico
Resultados (parciais):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// todas as consultas com o cliente e o médico em questão
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// médicos sem consultas
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
Não existe uma consulta LINQ para este pedido. Deve utilizar expressões lambda. Esta traduz-se da seguinte forma: pego na coleção de médicos (DOCTORS) e retenho (Where) apenas aqueles médicos (m) para os quais não consigo encontrar uma consulta (rv) com esse médico (m) na coleção de consultas (APPOINTMENTS).
// Horários disponíveis da Sra. Pélissier
Resultados (parciais):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// Número de consultas da Sra. Pélissier em 8 de outubro de 2012
Resultados:
![]() |
LINQ | |
Lambda | |
SQL | |
// Lista de clientes que marcaram uma consulta com a Sra. Pélissier em 08/10/2012
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// número de horários por médico
Resultados:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
3.5.5. Modificar uma entidade associada ao contexto de persistência
Já abordámos as seguintes operações no contexto de persistência:
- adicionar um elemento ao contexto ([dbContext].[DbSet].Add);
- Remover um item do contexto ([dbContext].[DbSet].Remove);
- Consultar um contexto utilizando consultas LINQ.
Para sincronizar o contexto com a base de dados, escreva [dbContext].SaveChanges().
![]() | ![]() |
O código [ModifyAttachedEntity] demonstra como modificar uma entidade associada ao contexto:
using System;
using System.Data;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class ModifyAttachedEntity
{
static void Main(string[] args)
{
Client client1, client2, client3;
// 1st context
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// add a customer
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// follow-up
Console.WriteLine("client1--avant");
Console.WriteLine(client1);
// save context
context.SaveChanges();
// follow-up
Console.WriteLine("client1--après");
Console.WriteLine(client1);
}
// 2nd context
using (var context = new RdvMedecinsContext())
{
// retrieve client1 from client2
client2 = context.Clients.Find(client1.Id);
// follow-up
Console.WriteLine("client2");
Console.WriteLine(client2);
// modify client2
client2.Nom = "yy";
// save context
context.SaveChanges();
}
// 3rd context
using (var context = new RdvMedecinsContext())
{
// retrieve client2 from client3
client3 = context.Clients.Find(client2.Id);
// follow-up
Console.WriteLine("client3");
Console.WriteLine(client3);
}
}
}
}
- linha 15: o contexto da aplicação é aberto;
- linhas 18–25: o contexto é limpo. Mais precisamente, todas as entidades são carregadas no contexto a partir da base de dados e, em seguida, definidas como «eliminadas». Note-se que, nesta fase, a base de dados não sofreu alterações. Enquanto o contexto não for sincronizado com a base de dados, esta permanece inalterada. Recorde-se que a eliminação das entidades [Doctor] e [Client] é suficiente para esvaziar a base de dados através de eliminações em cascata;
- linhas 27–28: um novo cliente é adicionado à base de dados;
- linhas 30-31: o cliente é apresentado antes de ser guardado na base de dados;
- linha 33: o contexto é sincronizado com a base de dados. As entidades marcadas como «eliminadas» serão sujeitas a uma operação SQL DELETE, e a entidade adicionada a uma operação SQL INSERT;
- linhas 35-36: o cliente é exibido após a sincronização com a base de dados;
O resultado apresentado na consola é o seguinte:
Tenha em atenção os seguintes pontos:
- Antes da sincronização com a base de dados, o cliente não possui nem uma chave primária nem um carimbo de data/hora;
- após a sincronização, possui-as. Recorde-se aqui que a chave primária foi configurada para ser gerada pelo SQL Server. Da mesma forma, este SGBD gera automaticamente o carimbo de data/hora;
- linha 37: o contexto de persistência é fechado. As entidades que ele continha tornam-se «desvinculadas». Elas existem como objetos, mas não como entidades vinculadas a um contexto de persistência;
- linha 39: é iniciado um novo contexto vazio;
- linha 42: o cliente é recuperado diretamente da base de dados através da sua chave primária. É então trazido para o contexto. Se não for encontrado, o método Find devolve um ponteiro nulo;
- linhas 48–49: exibimo-lo;
Isto produz o seguinte resultado:
- Linha 47: Modificamos-o;
- linha 49: sincronizamos o contexto com a base de dados. O EF irá detetar que determinados elementos do contexto foram modificados desde que foram adicionados ao mesmo. Para estes elementos, irá gerar instruções SQL UPDATE para a base de dados. Assim, neste caso, a sincronização consistirá numa única instrução UPDATE;
- linha 50: o segundo contexto é fechado. A entidade client2 que estava associada ao contexto é agora desassociada dele;
- linha 52: é aberto um terceiro contexto vazio;
- linha 55: o único cliente da base de dados é trazido de volta para ele. Queremos verificar se a modificação feita nele no contexto anterior foi refletida na base de dados;
- linhas 57–58: o cliente é apresentado. Isto produz o seguinte resultado:
O nome do cliente foi efetivamente atualizado na base de dados. Note que a data e hora também foram atualizadas.
- linha 59: encerramos o contexto. A propósito, note que, ao contrário dos dois casos anteriores, não foi necessário sincronizar previamente o contexto com a base de dados (SaveChanges), uma vez que o contexto não tinha sido alterado.
3.5.6. Gestão de entidades destacadas
Voltemos à arquitetura em camadas de uma aplicação como a do estudo de caso:
![]() |
A camada [DAO] utiliza o ORM EF5 para aceder aos dados. Temos os blocos de construção básicos desta camada. Cada método irá abrir um contexto de persistência, realizar as operações necessárias (inserir, atualizar, eliminar, consultar) e, em seguida, fechá-lo. As entidades geridas pela camada [DAO] serão passadas para a camada web ASP.NET. Nesta camada, elas estão fora do contexto de persistência e, portanto, desligadas. Na camada web, um utilizador pode modificar estas entidades (adicionar, atualizar, eliminar). Quando regressam à camada [DAO], continuam desligadas. No entanto, a camada [DAO] terá de refletir as alterações feitas pelo utilizador na base de dados. Por conseguinte, terá de trabalhar com entidades desligadas. Vejamos os três casos possíveis:
Adicionar uma entidade desligada
Este é o procedimento padrão para uma adição. Basta adicionar (Add) a entidade desligada ao contexto, garantindo que a sua chave primária é nula.
Modificar uma entidade desanexada
Pode utilizar o seguinte código:
- O método [DbContext].Entry(entidade-desvinculada) irá adicionar a entidade ao contexto;
- o estado desta entidade é definido como «modificado», para que seja sujeita a uma instrução SQL UPDATE.
Eliminar uma entidade destacada
Pode utilizar o seguinte código:
- Linha 1: Adicionar a entidade com a mesma chave primária que a entidade destacada ao contexto;
- Linha 2: Eliminamo-la:
Note que isto requer um SELECT seguido de um DELETE na base de dados, enquanto que normalmente basta apenas um DELETE. Também pode seguir o exemplo de modificação de uma entidade desligada e escrever:
Como não consegui implementar o registo de operações SQL realizadas na base de dados, não sei se um método é preferível ao outro.
Eis um exemplo:
![]() | ![]() |
O código do programa [ModifyDetachedEntities] é o seguinte:
using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class ModifyDetachedEntities
{
static void Main(string[] args)
{
Client client1;
// empty the current base
Erase();
// add a customer
using (var context = new RdvMedecinsContext())
{
// customer creation
client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
// add customer to context
context.Clients.Add(client1);
// save the context
context.SaveChanges();
}
// basic view
Dump("1-----------------------------");
// client1 is not in the context - we modify it
client1.Nom = "y";
// new context
using (var context = new RdvMedecinsContext())
{
// here we have an empty context
// we put client1 in the context in a modified state
context.Entry(client1).State = EntityState.Modified;
// save the context
context.SaveChanges();
}
// basic view
Dump("2-----------------------------");
// remove out-of-context entity
using (var context = new RdvMedecinsContext())
{
// here we have a new empty context
// we put client1 in the context in a deleted state
context.Entry(client1).State = EntityState.Deleted;
// save the context
context.SaveChanges();
}
// basic view
Dump("3-----------------------------");
}
static void Erase()
{
// empties base
using (var context = new RdvMedecinsContext())
{
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// save the context
context.SaveChanges();
}
}
static void Dump(string str)
{
Console.WriteLine(str);
// displays the base
using (var context = new RdvMedecinsContext())
{
foreach (var rv in context.Rvs)
{
Console.WriteLine(rv);
}
foreach (var creneau in context.Creneaux)
{
Console.WriteLine(creneau);
}
foreach (var client in context.Clients)
{
Console.WriteLine(client);
}
foreach (var medecin in context.Medecins)
{
Console.WriteLine(medecin);
}
}
}
}
}
- linha 15: a base de dados é limpa;
- linhas 17–25: um cliente é adicionado à base de dados;
- linha 27: exibe o conteúdo da base de dados;
- Após a linha 25, o contexto de persistência já não existe. Por conseguinte, já não existem entidades associadas. A entidade client1 passou para o estado «desvinculado»;
- linha 29: o nome da entidade desanexada é modificado;
- linha 31: é aberto um novo contexto vazio;
- linha 35: a entidade desanexada client1 é colocada no contexto num estado «modificado»;
- linha 37: o contexto é sincronizado com a base de dados;
- linha 38: é fechado;
- linha 40: o banco de dados é exibido;
O nome do cliente foi atualizado com sucesso na base de dados. Note que a data e hora foram atualizadas;
- linha 42: abertura de um novo contexto vazio;
- linha 46: a entidade destacada client1 é colocada no contexto no estado «eliminado»;
- linha 48: o contexto é sincronizado com a base de dados;
- linha 49: é fechado;
- linha 51: o banco de dados é exibido;
A entidade foi efetivamente eliminada da base de dados.
Agora, vamos analisar os dois modos de carregar as dependências de uma entidade: Carregamento Lazy e Carregamento Eager.
3.5.7. Carregamento diferido e imediato
Vamos rever o esquema de dependência muitos-para-um das nossas quatro entidades:
![]() |
Acima, a entidade [Creneau] tem uma propriedade de navegação [Creneau.Medecin] que aponta para a entidade [Medecin]. Isto é chamado de dependência. Vimos que também existem dependências de um-para-muitos. O princípio aqui explicado aplica-se igualmente a elas.
Por predefinição, o EF 5 está no modo de carregamento diferido (Lazy Loading): quando traz uma entidade do banco de dados para o contexto de persistência, não traz as suas dependências. Estas serão carregadas quando forem utilizadas pela primeira vez. Esta é uma medida de senso comum. Se não fosse assim, trazer os compromissos para o contexto resultaria, com base nas dependências acima, em:
- as entidades [Time Slot] ligadas aos compromissos;
- as entidades [Doctor] ligadas a esses intervalos;
- as entidades [Clientes] ligadas aos compromissos.
Por vezes, no entanto, precisamos de uma entidade e das suas dependências. Iremos ilustrar ambos os modos de carregamento.
![]() | ![]() |
O código para [LazyEagerLoading] é o seguinte:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class LazyEagerLoading
{
// entities
static Medecin[] medecins;
static Client[] clients;
static Creneau[] creneaux;
static void Main(string[] args)
{
// initialize the base
InitBase();
Console.WriteLine("Initialisation terminée");
// eager loading
Creneau creneau;
int idCreneau = (int)creneaux[0].Id;
using (var context = new RdvMedecinsContext())
{
// crenel n° 0
creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// dependent display
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
}
// lazy loading - default mode
using (var context = new RdvMedecinsContext())
{
// crenel n° 0
creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// dependent display
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 2 suivante s'est produite : {0}", e);
}
}
static void InitBase()
{
// initialize the base
using (var context = new RdvMedecinsContext())
{
// empty the current base
...
// initialize the base
// our customers
clients = new Client[] {
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
...
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// save the persistence context
context.SaveChanges();
}
}
}
}
- linha 18: partimos de uma base conhecida, a que foi utilizada até agora. Após esta operação, as matrizes nas linhas 11–13 são preenchidas com entidades destacadas;
- linhas 21–22: focamo-nos no primeiro intervalo de tempo e no médico associado;
- linha 23: novo contexto;
- linha 26: colocamos o intervalo de tempo no contexto juntamente com a sua dependência (carregamento antecipado). Como este não é o modo padrão, temos de solicitar explicitamente esta dependência. O método Include permite-nos fazer isso. O seu parâmetro é o nome da dependência dentro da entidade trazida para o contexto. A consulta que traz a entidade para o contexto utiliza expressões lambda. O método Single permite-lhe especificar uma condição para recuperar uma única entidade. Aqui, pesquisamos na base de dados a entidade [Creneau] que tem a chave primária do intervalo #0;
- linha 27: exibimos a entidade recuperada. Vamos rever os dois métodos de gravação utilizados nas entidades:
// signature
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
}
- linhas 2-5: o método [ToString] exibe a dependência [Doctor]. Se esta ainda não estiver no contexto, será procurada na base de dados para a adicionar;
- linhas 8-11: o método [ShortIdentity] não apresenta a dependência [Doctor]. Por conseguinte, não será procurada na base de dados se não estiver no contexto;
Neste momento, a saída da consola é a seguinte:
- Linha 28: o contexto é fechado;
- Linhas 30–37: Tentamos escrever a dependência [Doctor] da entidade. Recorde-se como funciona o carregamento diferido (Lazy Loading): uma dependência é carregada na sua primeira utilização, caso não esteja presente. Aqui, está normalmente presente. A saída é a seguinte:
- linhas 39–44: num novo contexto, o slot n.º 0 é novamente pesquisado na base de dados e introduzido no contexto. Aqui, a dependência [Doctor] não é explicitamente solicitada. Por conseguinte, não será introduzida (Lazy Loading);
- linha 43: a identidade curta do slot é apresentada da seguinte forma:
Aqui, é importante utilizar ShortIdentity em vez de ToString para apresentar a entidade. Se for utilizado ToString, a dependência [Doctor] será apresentada e, para tal, será consultada na base de dados. No entanto, não queremos isso.
- Linha 44: o contexto está fechado;
- linhas 46–53: tentamos exibir a dependência da entidade. É importante fazer isto fora do contexto; caso contrário, ela será pesquisada na base de dados e encontrada. Aqui, estamos fora do contexto. A entidade [Creneau] está desanexada e a sua dependência [Medecin] está em falta (Lazy Loading). O que irá acontecer? A exibição no ecrã é a seguinte:
O EF detetou que faltava a dependência [Medecin]. Tentou carregá-la, mas como o contexto estava fechado, esta operação já não era possível. Iremos registar esta exceção [System.ObjectDisposedException], uma vez que é característica do carregamento de uma dependência fora de um contexto aberto.
Agora, vamos examinar o acesso simultâneo a entidades.
3.5.8. Concorrência no acesso a entidades
Vamos rever a definição da entidade [Client]:
public class Client
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// customer rvs
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
...
}
Vamos concentrar-nos no campo [Timestamp] na linha 23. Sabemos que o seu valor é gerado pelo SGBD. Também observámos que a anotação [Timestamp] na linha 22 faz com que o EF 5 utilize o campo anotado para gerir a concorrência no acesso às entidades. Vamos relembrar o que é a gestão da concorrência:
- um processo P1 lê uma linha L da tabela [DOCTORS] no momento T1. A linha tem o timestamp TS1;
- um processo P2 lê a mesma linha L da tabela [DOCTORS] no momento T2. A linha tem o timestamp TS1 porque o processo P1 ainda não confirmou a sua modificação;
- o processo P1 confirma a sua modificação na linha L. O carimbo de data/hora da linha L muda então para TS2;
- O processo P2 confirma a sua modificação na linha L. O ORM lança então uma exceção porque o processo P2 tem um carimbo de data/hora TS1 para a linha L que difere do carimbo de data/hora TS2 encontrado na base de dados.
Isto é chamado de gestão otimista de concorrência. Com o EF 5, um campo que desempenha esta função deve ter um de dois atributos: [Timestamp] ou [ConcurrencyCheck]. O SQL Server possui um tipo [timestamp]. Uma coluna deste tipo tem o seu valor gerado automaticamente pelo SQL Server aquando de qualquer inserção ou modificação de uma linha. Essa coluna pode então ser utilizada para gerir a concorrência.
Iremos ilustrar este acesso simultâneo com dois threads que irão modificar simultaneamente a mesma entidade [Client] na base de dados. O projeto evolui da seguinte forma:
![]() | ![]() |
O código do programa [ConcurrentAccess] é o seguinte:
using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// object exchanged with threads
class Data
{
public int Duree { get; set; }
public string Nom { get; set; }
public Client Client { get; set; }
}
// test program
class AccèsConcurrents
{
static void Main(string[] args)
{
Client client1;
using (var context = new RdvMedecinsContext())
{
// main thread
Thread.CurrentThread.Name = "main";
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// add a customer
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// follow-up
Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
// backup
context.SaveChanges();
// follow-up
Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
}
// we'll modify client1 with two threads
// thead t1
Thread t1 = new Thread(Modifie);
t1.Name = "t1";
t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
// thread t2
Thread t2 = new Thread(Modifie);
t2.Name = "t2";
t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
// we wait for the end of the 2 threads
Console.WriteLine("Thread {0} -- début attente fin des deux threads", Thread.CurrentThread.Name);
t1.Join();
t2.Join();
Console.WriteLine("Thread {0} -- fin attente fin des deux threads", Thread.CurrentThread.Name);
// the modification is displayed - only one was successful
using (var context = new RdvMedecinsContext())
{
// retrieve client1 from client2
Client client2 = context.Clients.Find(client1.Id);
Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
}
}
// thread
static void Modifie(object infos)
{
...
}
- linha 26: iniciamos um contexto vazio;
- linha 29: nomeamos o thread atual para o distinguir dos dois threads que serão criados mais tarde;
- linhas 31–38: as entidades [Doctor] e [Client] são definidas para o estado «deleted»;
- linhas 40–41: um cliente é adicionado ao contexto;
- linhas 43–44: exibimo-lo antes da sincronização do contexto;
- linha 46: sincronização do contexto com a base de dados: as entidades no estado «eliminado» serão removidas da base de dados. A entidade [Cliente] colocada no contexto será inserida na base de dados. Será o único elemento na base de dados;
- linhas 47-49: o cliente é exibido após a sincronização do contexto. Neste momento, as exibições no ecrã são as seguintes:
Note que, após a sincronização do contexto, o cliente possui uma chave primária e um carimbo de data/hora;
- linha 50: o contexto é fechado;
- linha 53: uma thread t1 é associada ao método [Modify] na linha 84. Isto significa que, quando for iniciada, irá executar o método [Modify];
- linha 54: é atribuído um nome à thread t1;
- linha 55: a thread t1 é iniciada. Os parâmetros são-lhe passados na forma de uma estrutura [Data] definida nas linhas 12–17:
- Duration: A thread irá parar X segundos antes de concluir a sua execução,
- Cliente: uma referência ao cliente a ser atualizado na base de dados,
- Nome: o nome a atribuir a este cliente;
- linhas 57–59: mesmo procedimento com um segundo thread. Em última análise, dois threads tentarão alterar o nome do mesmo cliente na base de dados;
- linhas 60–63: após iniciar as duas threads, a thread principal aguarda que elas terminem a execução;
- linha 62: à espera que o thread t1 termine;
- linha 63: aguardando que a thread t2 termine;
- linha 64: não sabemos em que ordem as duas threads irão terminar. O que é certo é que, na linha 64, elas já terminaram;
- linhas 66-72: num novo contexto, procuramos o cliente na base de dados para verificar o seu estado.
Agora vamos ver o que as duas threads t1 e t2 fazem. Elas executam o seguinte método [Modify]:
static void Modifie(object infos)
{
// parameter is retrieved
Data data = (Data)infos;
try
{
using (var context = new RdvMedecinsContext())
{
Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
// retrieve client1 from client2
Client client2 = context.Clients.Find(data.Client.Id);
Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
// modify client2
client2.Nom = data.Nom;
// we wait a little
Thread.Sleep(data.Duree);
// save changes
context.SaveChanges();
}
}
catch (Exception e)
{
// exception
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
}
// end of thread
Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
}
- linha 4: recuperar os parâmetros da thread (Duração, Nome, Cliente);
- linha 7: novo contexto;
- linha 11: o cliente é inserido no contexto;
- linhas 12–13: monitorização para verificar o estado do cliente;
- linha 15: alterar o seu nome;
- Linha 17: A thread faz uma pausa com uma duração de milissegundos. Isto tem um efeito interessante. A thread liberta o processador que a estava a executar, abrindo caminho para outra thread. No nosso exemplo, temos três threads: main, t1 e t2. A thread principal está em pausa, à espera que as threads t1 e t2 terminem. Assumindo que a thread t1 tem o processador primeiro, ela cede-o agora à thread t2. Isto fará com que a thread t2 leia exatamente os mesmos dados que a thread t1 — o mesmo cliente com o mesmo carimbo de data/hora;
- Linha 19: o contexto é sincronizado com a base de dados. Vamos supor novamente que a thread t1 acorda primeiro. Ela irá guardar o cliente com o nome "yy". Será capaz de o fazer porque tem o mesmo carimbo de data/hora que o da base de dados. Devido a esta atualização, o SGBD irá modificar o carimbo de data/hora. Quando a thread t2 acordar por sua vez, terá um cliente com um carimbo de data/hora diferente do que está agora na base de dados. A sua atualização será rejeitada.
As exibições no ecrã são as seguintes:
- linha 4: o cliente na base de dados;
- linha 9: o cliente tal como lido pela thread t2;
- linha 11: o cliente tal como lido pela thread t1. Ambas as threads leram, portanto, a mesma coisa;
- linha 12: a thread t2 termina primeiro. Conseguiu, portanto, realizar a sua atualização. O nome deve ter mudado para «zz»;
- linha 13: a thread t1 lança uma [System.Data.OptimisticConcurrencyException]. O EF detetou que não tinha o timestamp correto;
- linha 21: a thread t1 termina por sua vez;
- linha 22: a thread principal terminou a espera;
- linha 24: a thread principal exibe o cliente na base de dados. Foi, de facto, a thread t2 que venceu. O nome é «zz». Note-se que o carimbo de data/hora mudou.
Agora, vamos examinar outro aspeto: a transação que rege a sincronização do contexto de persistência com a base de dados.
3.5.9. Sincronização dentro de uma transação
A tabela [CRENEAUX] tem uma restrição de unicidade que adicionámos manualmente (ver secção 2.2.4, página 12):
Vamos proceder da seguinte forma: vamos adicionar duas consultas ao mesmo tempo para o mesmo médico, no mesmo dia e no mesmo intervalo de tempo. Vamos ver o que acontece.
O projeto evolui da seguinte forma:
![]() | ![]() |
O código do programa [SynchronisationTransaction] é o seguinte:
using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// test program
class SynchronisationTransaction
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
context.SaveChanges();
}
// create a customer
Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
// we create a doctor
Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
// we create a niche for this doctor
Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
// create two appointments for this doctor and this customer, same day, same time slot
Rv rv1 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
Rv rv2 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
try
{
// we put it all in the context of persistence
using (var context = new RdvMedecinsContext())
{
context.Clients.Add(client1);
context.Creneaux.Add(creneau1);
context.Medecins.Add(medecin1);
context.Rvs.Add(rv1);
context.Rvs.Add(rv2);
// save the context - you should have an exception
// because the underlying BD has a uniqueness constraint preventing
// to have two RDV on the same day, in the same slot
context.SaveChanges();
}
}
catch (Exception e)
{
Console.WriteLine("Erreur : {0}", e);
}
// if the save occurs in a transaction, then nothing must have been inserted in the database
// because of the previous exception - we check
using (var context = new RdvMedecinsContext())
{
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// the doctors
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// time slots
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// dates
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
- linhas 15–27: um contexto de persistência é utilizado para esvaziar a base de dados;
- linha 30: criação de um objeto [Client];
- linha 32: criação de um objeto [Doctor];
- linha 34: criação de um objeto [Slot];
- linha 36: criação de um objeto [Appointment];
- linha 37: criação de um segundo objeto [Appointment] idêntico ao anterior;
- linha 41: abertura de um novo contexto;
- linhas 43–47: os objetos criados anteriormente são anexados ao novo contexto. Note-se aqui que, ao ter em conta as dependências, poderíamos ter minimizado o número de operações Add. No entanto, o EF irá otimizar as instruções SQL INSERT a enviar para a base de dados;
- linha 51: o contexto é sincronizado com a base de dados. Tal como o comentário indica, a inserção de um dos dois compromissos deve falhar devido à restrição de unicidade na tabela [RVS]. Mas, além disso, se a sincronização ocorrer dentro de uma transação, tudo deve ser revertido. Portanto, não deve ocorrer nenhuma inserção. A base de dados deve permanecer vazia;
- linha 53: o contexto é fechado;
- linhas 61–90: exibição do conteúdo da base de dados. Deve estar vazio.
A exibição no ecrã é a seguinte:
- linha 1: exceção devido a uma violação da restrição de unicidade na tabela [RVS];
- linhas 9–12: a base de dados está, de facto, vazia. A sincronização do contexto com a base de dados ocorreu, portanto, no âmbito de uma transação.
Existem, sem dúvida, outros aspetos a explorar no EF 5. Mas sabemos o suficiente para regressarmos ao nosso estudo de uma arquitetura multicamadas. No início deste documento, o leitor encontrará referências a artigos e livros que lhe permitirão aprofundar os seus conhecimentos sobre o EF 5.
3.6. Estudo de uma arquitetura multicamadas baseada no EF 5
Voltamos ao nosso estudo de caso descrito na Secção 2. Trata-se de uma aplicação web ASP.NET estruturada da seguinte forma:
![]() |
Começaremos por construir a camada de acesso a dados [DAO]. Esta camada será baseada no EF5.
3.6.1. O novo projeto
Criamos um novo projeto de consola VS 2012 [RdvMedecins-SqlServer-02] na solução atual [1]:
![]() |
Adicionamos quatro pastas [2] a este projeto, nas quais iremos organizar o nosso código. A pasta [Entities] é uma cópia da pasta [Entities] do projeto anterior. Após a cópia, surgem erros porque não temos as referências corretas. Precisamos de adicionar uma referência ao Entity Framework 5. Para tal, seguiremos o método explicado na secção 3.4, página 21. A lista de referências fica da seguinte forma [3]:
![]() |
Nesta altura, o projeto já não deverá apresentar quaisquer erros de compilação. Do projeto anterior, copiamos também o ficheiro [App.config], que configura a ligação à base de dados:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
<!-- connection chain on base -->
<connectionStrings>
<add name="monContexte"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- the factory provider -->
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider"
invariant="System.Data.SqlClient"
description=".Net Framework Data Provider for SqlServer"
type="System.Data.SqlClient.SqlClientFactory, System.Data,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/>
</DbProviderFactories>
</system.data>
</configuration>
3.6.2. A Classe de Exceção
Iremos utilizar uma classe de exceção específica do projeto. Esta é a que será lançada pela camada [DAO]:
![]() |
A camada [DAO] irá capturar todas as exceções que lhe forem propagadas e encapsulá-las numa exceção do tipo [RdvMedecinsException]. Esta exceção será a seguinte:
using System;
namespace RdvMedecins.Exceptions
{
public class RdvMedecinsException : Exception
{
// properties
public int Code { get; set; }
// manufacturers
public RdvMedecinsException()
: base()
{
}
public RdvMedecinsException(string message)
: base(message)
{
}
public RdvMedecinsException(int code, string message)
: base(message)
{
Code = code;
}
public RdvMedecinsException(int code, string message, Exception ex)
: base(message, ex)
{
Code = code;
}
// identity
public override string ToString()
{
if (InnerException == null)
{
return string.Format("RdvMedecinsException[{0},{1}]", Code, base.Message);
}
else
{
return string.Format("RdvMedecinsException[{0},{1},{2}]", Code, base.Message, base.InnerException.Message);
}
}
}
}
- Linha 5: A classe estende a classe [Exception];
- linha 9: adiciona um código de erro à sua classe base;
- linhas 12–32: os vários construtores incorporam o campo [Code].
O projeto evolui da seguinte forma:
![]() |
3.6.3. A camada [DAO]
![]() |
A camada [DAO] fornece uma interface para a camada [ASP.NET]. Para identificar isto, consulte as páginas web da aplicação:
![]() |
- em [1] acima, a lista suspensa foi preenchida com a lista de médicos. A camada [DAO] fornecerá esta lista;
- em [2], a camada [DAO] fornecerá;
- a lista de consultas de um médico para um determinado dia,
- uma lista dos horários disponíveis de um médico,
- informações adicionais sobre o médico selecionado;
![]() |
- em [3], a lista suspensa de clientes será fornecida pela camada [DAO];
![]() |
- em [4], o utilizador confirma uma marcação. A camada [DAO] deve ser capaz de a adicionar à base de dados. Deve também ser capaz de fornecer informações adicionais sobre o cliente selecionado;
![]() |
- em 5, o utilizador elimina um compromisso. A camada [DAO] deve permitir isso.
Com esta informação, a interface [IDao] da camada [DAO] poderia ser a seguinte:
using System;
using System.Collections.Generic;
using RdvMedecins.Entites;
namespace RdvMedecins.Dao
{
public interface IDao
{
// customer list
List<Client> GetAllClients();
// list of doctors
List<Medecin> GetAllMedecins();
// list of physician slots
List<Creneau> GetCreneauxMedecin(int idMedecin);
// list of RV from a given doctor on a given day
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// add a RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// delete a RV
void SupprimerRv(int idRv);
// find a T entity via its primary key
T Find<T>(int id) where T : class;
}
}
Os métodos nas linhas 10–20 derivam da análise que acabámos de realizar. O método na linha 22 existe para lidar com o facto de estarmos a trabalhar com carregamento diferido. Se, na camada [ASP.NET], precisarmos de uma dependência de uma entidade, iremos recuperá-la da base de dados utilizando este método.
A implementação [Dao] desta interface será a seguinte:
using System;
using System.Collections.Generic;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using RdvMedecins.Models;
namespace RdvMedecins.Dao
{
public class Dao : IDao
{
//customer list
public List<Client> GetAllClients()
{
// customer list
List<Client> clients = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// customer list
clients = context.Clients.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(1, "GetAllClients", ex);
}
// we return the result
return clients;
}
// list of doctors
public List<Medecin> GetAllMedecins()
{
// list of doctors
List<Medecin> medecins = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// list of doctors
medecins = context.Medecins.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(2, "GetAllMedecins", ex);
}
// we return the result
return medecins;
}
// list of time slots for a given doctor
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
...
}
// list of a doctor's RV for a given day
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
...
}
// add a RV to the list
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
...
}
// delete a RV
public void SupprimerRv(int idRv)
{
...
}
// find a customer
public Client FindClient(int id)
{
...
}
// find a niche
public Creneau FindCreneau(int id)
{
...
}
// find a doctor
public Medecin FindMedecin(int id)
{
....
}
// find an rv
public Rv FindRv(int id){
...
}
}
}
Vamos explicar o método [GetAllClients], que deve devolver uma lista de todos os clientes:
- linhas 18–31: a pesquisa de clientes é realizada dentro de um bloco try/catch. O mesmo se aplica a todos os métodos subsequentes;
- linha 21: abertura de um novo contexto;
- linha 24: as entidades [Client] são carregadas no contexto e colocadas numa lista.
O método [GetAllMedecins], que devolve uma lista de todos os médicos, é semelhante (linhas 37–57).
O método [GetCreneauxMedecin] é o seguinte:
// list of time slots for a given doctor
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
// list of slots
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the doctor back with his slots
Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
// list of doctor's slots
return medecin.Creneaux.ToList<Creneau>();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
}
}
- linha 9: abertura de um novo contexto de persistência;
- linha 11: pesquisa o médico cuja chave primária é conhecida. Solicita que a dependência [Creneaux] — uma coleção dos horários do médico — seja incluída. Se o médico não existir, o método Single lança uma exceção;
- linha 13: devolve a lista de horários.
O método [GetRvMedecinJour] deve devolver a lista de consultas de um médico para um determinado dia. O seu código poderia ser o seguinte:
// list of a doctor's RV for a given day
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
// rv list
List<Rv> rvs = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the doctor back
Medecin medecin = context.Medecins.Find(idMedecin);
if (medecin == null)
{
throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
}
// appointment list
rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
}
// we return the result
return rvs;
}
- linha 13: recuperamos o médico com a chave primária fornecida;
- linhas 14–17: se não existir, lançar uma exceção;
- linha 19: a consulta LINQ para recuperar as consultas deste médico;
O método [AddAppointment] deve adicionar uma consulta à base de dados e devolver a chave primária do item inserido. O seu código poderia ser o seguinte:
// add a RV to the list
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
// rdv n° added
int idRv;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the slot back
Creneau creneau = context.Creneaux.Find(idCreneau);
if (creneau == null)
{
throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
}
// we get the customer back
Client client = context.Clients.Find(idClient);
if (client == null)
{
throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
}
// niche creation
Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
// added in context
context.Rvs.Add(rv);
// save context
context.SaveChanges();
// retrieve the primary key of the added rv
idRv = (int)rv.Id;
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(7, "AjouterRv", ex);
}
// result
return idRv;
}
- linha 12: pesquisa o horário de consulta na base de dados;
- linhas 13–16: se não for encontrado, é lançada uma exceção;
- linha 18: procurar o cliente da consulta na base de dados;
- linhas 19–22: se não for encontrado, lançar uma exceção;
- linha 24: criar um objeto [Rv] com as informações necessárias;
- linha 26: adicioná-lo ao contexto de persistência;
- linha 28: sincronizamos o contexto de persistência com a base de dados. O compromisso será então guardado na base de dados;
- linha 30: sabemos que, após a sincronização da base de dados, as chaves primárias dos itens inseridos estão disponíveis. Recuperamos a chave do compromisso adicionado;
- linha 31: fechamos o contexto de persistência.
O método [DeleteAppointment] deve eliminar um compromisso cuja chave primária lhe seja passada.
// delete a RV
public void SupprimerRv(int idRv)
{
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we recover the Rv
Rv rv = context.Rvs.Find(idRv);
if (rv == null)
{
throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
}
// deletion Rv
context.Rvs.Remove(rv);
// save context
context.SaveChanges();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(8, "SupprimerRv", ex);
}
}
- linha 7: novo contexto de persistência;
- linha 10: o compromisso a ser eliminado é passado para o contexto;
- linhas 11–15: se não existir, é lançada uma exceção;
- linha 16: remova-o do contexto;
- linha 18: sincronizar o contexto com a base de dados;
- linha 19: fechar o contexto.
O método [Find<T>] permite pesquisar na base de dados uma entidade do tipo T utilizando a sua chave primária. O seu código poderia ser o seguinte:
public T Find<T>(int id) where T : class
{
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
return context.Set<T>().Find(id);
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(20, "Find<T>", ex);
}
}
- Linha 8: O método Set<T> permite-lhe recuperar um DbSet<T> ao qual pode aplicar os métodos habituais.
O projeto evolui da seguinte forma:
![]() |
3.6.4. Testar a camada [DAO]
Iremos criar um programa de teste para a camada [DAO]. A arquitetura de teste será a seguinte:
![]() |
Um programa de consola solicita ao [Spring.net] que instancie a camada [DAO]. Depois de feito isso, testa as várias funcionalidades da interface da camada [DAO]. Em vez de um programa de consola, teria sido preferível escrever um programa de teste ao estilo NUnit. Um programa de teste para a camada [DAO] poderia ter o seguinte aspeto:
using System;
using System.Collections.Generic;
using RdvMedecins.Dao;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using Spring.Context.Support;
namespace RdvMedecins.Tests
{
class Program
{
public static void Main()
{
IDao dao = null;
try
{
// instantiation layer [DAO] via Spring
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
// customer display
List<Client> clients = dao.GetAllClients();
DisplayClients("Liste des clients :", clients);
// physician display
List<Medecin> medecins = dao.GetAllMedecins();
DisplayMedecins("Liste des médecins :", medecins);
// list of time slots for doctor no. 0
List<Creneau> creneaux = dao.GetCreneauxMedecin((int)medecins[0].Id);
DisplayCreneaux(string.Format("Liste des créneaux horaires du médecin {0}", medecins[0]), creneaux);
// list of doctor's appointments for a given day
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
// add a RV to doctor n°1 in slot n° 0
Console.WriteLine(string.Format("Ajout d'un RV au médecin {0} avec client {1} le 23/11/2013", medecins[0], clients[0]));
int idRv1 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
Console.WriteLine("Rdv ajouté");
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
// add an appointment in an already occupied slot - must trigger an exception
int idRv2;
Console.WriteLine("Ajout d'un RV dans un créneau déjà occupé");
try
{
idRv2 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
Console.WriteLine("Rdv ajouté");
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
}
catch (RdvMedecinsException ex)
{
Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
}
// delete an appointment
Console.WriteLine(string.Format("Suppression du RV n° {0}", idRv1));
dao.SupprimerRv(idRv1);
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
}
catch (Exception ex)
{
Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
}
//break
Console.ReadLine();
}
// utility methods - display lists
public static void DisplayClients(string Message, List<Client> clients)
{
Console.WriteLine(Message);
foreach (Client c in clients)
{
Console.WriteLine(c.ShortIdentity());
}
}
public static void DisplayMedecins(string Message, List<Medecin> medecins)
{
...
}
public static void DisplayCreneaux(string Message, List<Creneau> creneaux)
{
...
}
public static void DisplayRvs(string Message, List<Rv> rvs)
{
...
}
}
}
- linha 14: a referência à camada [DAO]. Para tornar o teste independente da implementação real da camada [DAO], esta referência é do tipo [IDao] (a interface) em vez de ser do tipo [Dao] (a classe);
- linha 18: a camada [DAO] é instanciada pelo Spring. Voltaremos à configuração necessária para tornar isto possível. Convertemos a referência de objeto devolvida pelo Spring numa referência do tipo de interface [IDao];
- Linhas 21–22: exibem os clientes;
- linhas 25–26: exibem os médicos;
- linhas 29-30: exibem a lista de horários disponíveis para o médico n.º 0;
- linha 33: exibe as consultas do médico n.º 0 para 23 de novembro de 2013. Não deve haver nenhuma;
- linha 37: adiciona uma consulta para o médico n.º 0 em 23/11/2013;
- linha 39: exibe as consultas do médico n.º 0 em 23/11/2013. Deveria haver uma;
- linha 46: a mesma consulta é adicionada uma segunda vez. Deve ocorrer uma exceção;
- Linha 57: Apaga a única consulta que foi adicionada;
- Linha 58: Apresenta as consultas do médico n.º 0 em 23/11/2013. Não deveria haver nenhuma.
3.6.5. Configuração do Spring.net
No programa de teste acima, abordámos brevemente a instrução que instancia a camada [DAO]:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
A classe [ContextRegistry] é uma classe Spring no namespace [Spring.Context.Support]. Para utilizar o Spring, precisamos de adicionar a sua DLL às referências do projeto. Procedemos da seguinte forma:
![]() |
- em [1], procure pacotes utilizando a ferramenta [NuGet];
![]() |
- em [2], procure pacotes online;
- em [3], introduza a palavra-chave «spring» na caixa de pesquisa;
- em [4], são apresentados os pacotes cuja descrição contém esta palavra-chave. Aqui, [Spring.Core] é o que precisamos. Instalamo-lo.
As referências do projeto alteram-se da seguinte forma:
![]() |
O pacote [Spring.Core] tinha uma dependência do pacote [Common.Logging]. Este também foi carregado. Nesta altura, o projeto já não deverá apresentar quaisquer erros.
Isso não significa, no entanto, que vá funcionar. Primeiro, precisamos de configurar o Spring no ficheiro [App.config]. Esta é a parte mais complicada do projeto. O novo ficheiro [App.config] é o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
<!-- spring -->
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
<!-- common logging-->
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<!-- Entity Framework -->
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters>
<parameter value="v11.0" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
<!-- Connection chains -->
<connectionStrings>
<add name="monContexte" connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</DbProviderFactories>
</system.data>
<!-- spring configuration -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
<!-- configuration common.logging -->
<logging>
<factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
<arg key="showLogName" value="true" />
<arg key="showDataTime" value="true" />
<arg key="level" value="DEBUG" />
<arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
</factoryAdapter>
</logging>
</configuration>
Vamos começar por remover tudo o que já é conhecido: Entity Framework, cadeias de ligação, ProviderFactory. O ficheiro evolui da seguinte forma:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" ... />
<!-- spring -->
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
<!-- common logging-->
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</sectionGroup>
</configSections>
...
<!-- spring configuration -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
<!-- configuration common.logging -->
<common>
<logging>
<factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
<arg key="showLogName" value="true" />
<arg key="showDataTime" value="true" />
<arg key="level" value="DEBUG" />
<arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
</factoryAdapter>
</logging>
</common>
</configuration>
- linhas 3–15: definem secções de configuração;
- linha 8: define a classe que irá gerir a secção <spring><context> do ficheiro XML (linhas 19–21);
- linha 9: define a classe que irá gerir a secção <spring><objects> do ficheiro XML (linhas 22–24);
- linha 13: define a classe que irá gerir a secção <common><logging> do ficheiro XML (linhas 27–36);
- Linhas 7–14: são estáveis. Não precisam de ser alteradas noutro projeto;
- linhas 18–25: configuração do Spring. É estável, exceto pelas linhas 22–24, que definem os objetos que o Spring irá instanciar;
- linha 23: definição de um objeto. O atributo id é arbitrário. É o identificador do objeto. O atributo type especifica a classe a ser instanciada no formato «nome completo da classe, Assembly que contém a classe». A classe aqui é aquela que implementa a camada [DAO]: [RdvMedecins.Dao.Dao]. Para encontrar o seu assembly, consulte as propriedades do projeto:
![]() |
Em [1], o nome do assembly a ser fornecido;
- linhas 27–36: a configuração «Common Logging» é estável. Poderá ser necessário modificar o nível de registo na linha 32. Após a fase de depuração, pode definir o nível para INFO.
Em última análise, embora complexo à primeira vista, o ficheiro de configuração do Spring acaba por ser simples. As únicas alterações necessárias são:
- linhas 22–24, que definem os objetos a instanciar;
- linha 32: o nível de registo.
No programa de teste, a instrução que instancia a camada [DAO] é a seguinte:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
[ContextRegistry] é uma classe Spring que utiliza a configuração Spring especificada num ficheiro [Web.config] ou [App.config]. Aqui, utilizará a seguinte secção do ficheiro [App.config]:
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
- ContextRegistry.GetContext() utiliza o contexto definido nas linhas 2–4. A linha 3 significa que os objetos Spring estão definidos na secção [spring/objects] do ficheiro de configuração. Esta secção corresponde às linhas 5–7;
- ContextRegistry.GetContext().GetObject("rdvmedecinsDao") utiliza a secção nas linhas 5–7. Devolve uma referência ao objeto com o atributo id="rdvmedecinsDao". Este é o objeto definido na linha 6. O Spring irá então instanciar a classe definida pelo atributo type utilizando o seu construtor sem parâmetros. Este construtor deve, portanto, existir. Uma vez feito isto, a referência ao objeto criado é devolvida ao código de chamada. Se o objeto for solicitado uma segunda vez no código, o Spring simplesmente devolve uma referência ao primeiro objeto criado. Este é o padrão de design conhecido como singleton.
A construção de objetos pode ser mais complexa. Pode utilizar um construtor com parâmetros ou especificar a inicialização de determinados campos do objeto depois de este ter sido criado. Para mais informações sobre este tópico, consulte o artigo «Tutorial Spring IOC para .NET» em [http://tahe.developpez.com/dotnet/springioc/].
Depois de fazer isto, podemos executar a aplicação. Os resultados no ecrã são os seguintes:
Os resultados são os esperados. Vamos agora considerar a nossa camada [DAO] como válida. O tutorial poderia terminar aqui. Até agora, abordámos:
- os conceitos básicos do ORM do Entity Framework 5;
- uma camada [DAO] que utiliza este ORM.
Vamos recordar o nosso estudo de caso descrito no início deste documento. Começamos com uma aplicação existente com a seguinte arquitetura:
![]() |
que queremos transformar nisto:
![]() |
onde o EF5 substituiu o NHibernate. Acabámos de construir a camada [DAO2]. Na verdade, esta não tem a mesma interface que a camada [DAO1], cuja interface era mais limitada:
public interface IDao
{
// customer list
List<Client> GetAllClients();
// list of doctors
List<Medecin> GetAllMedecins();
// list of physician slots
List<Creneau> GetCreneauxMedecin(int idMedecin);
// list of RV from a given doctor on a given day
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// add a RV to the list
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// delete a RV
void SupprimerRv(int idRv);
}
A camada [DAO2] adicionou o seguinte método a esta interface:
// find a T entity via its primary key
T Find<T>(int id) where T : class;
Este método foi adicionado porque o ORM do EF 5 opera no modo de carregamento diferido (Lazy Loading) por predefinição. As entidades chegam à camada [ASP.NET] sem as suas dependências. O método acima permite-nos recuperá-las, se necessário, e, em alguns casos, precisamos mesmo delas. O NHibernate também opera no modo Lazy Loading por predefinição, mas eu tinha-o utilizado no modo Eager Loading. As entidades chegavam à camada [ASP.NET] com as suas dependências.
Vamos concluir a migração da aplicação ASP.NET/NHibernate para a aplicação ASP.NET/EF 5. No entanto, uma vez que isto já não diz respeito ao EF5, não iremos comentar o código web. Iremos simplesmente explicar como configurar a aplicação web e testá-la. Está disponível no site deste tutorial.
3.6.6. Gerar a DLL da camada [DAO]
Na seguinte arquitetura:
![]() |
a camada [ASP.NET] terá as camadas à sua direita disponíveis na forma de DLLs. Vamos, portanto, criar a DLL da camada [DAO].
![]() |
- Em [1], selecione o programa de teste e, em [2], não o inclua na DLL que será gerada;
- Em [3], nas propriedades do projeto, especifique que o assembly a ser criado é uma DLL;
- Em [4], no menu do VS, especificamos que iremos gerar um assembly [Release], que contém menos informações do que um assembly [Debug];
![]() |
- Em 5, gere novamente o assembly do projeto. A DLL será gerada;
- Em [6], exiba todos os ficheiros do projeto;
![]() |
- Em [7], a DLL para o projeto da camada [DAO]. Esta é a que o projeto web ASP.NET irá utilizar;
- Em [8], atualizamos a visualização do projeto;
![]() |
- em [9], as DLLs da pasta [Release] são reunidas numa pasta externa [lib] [10]. É daí que o projeto web irá obter as suas referências.
3.6.7. A camada [ASP.NET]
Aqui explicaremos como portar a aplicação [ASP.NET / NHibernate] para a aplicação [ASP.NET / EF 5]. Trabalharemos com o Visual Studio Express 2012 for Web, disponível gratuitamente em [http://www.microsoft.com/visualstudio/fra/downloads].
Começaremos com o projeto web existente criado com o VS 2010.
![]() |
- Em [1], abrimos o projeto existente:
- Em [2], o projeto carregado tem as seguintes referências [3]:
- [NHibernate] é a DLL do framework NHibernate,
- [Spring.Core] é a DLL da estrutura Spring.net,
- [log4net] é a DLL da estrutura de registo log4net. Esta estrutura é utilizada pelo Spring.net,
- [MySql.Data] é o controlador ADO.NET para o SGBD MySQL,
- [rdvmedecins] é a DLL para a camada [DAO] construída com o NHibernate;
- em [4], alteramos o nome do projeto e, em 5, removemos as referências anteriores;
![]() |
- em [6], adicionamos referências ao projeto;
- em [7], no assistente, utilizamos a opção [Procurar];
![]() |
- em [8], selecionamos todas as DLLs do projeto n.º 2 previamente colocadas na pasta [lib];
- em [9], um resumo que confirmamos;
- em [10], o projeto web com as suas novas referências.
Depois de fazer isto, o projeto fica com o seguinte aspeto:
![]() |
- Em [1], o código para gerir as páginas web está dividido entre os dois ficheiros [Global.asax] e [Default.aspx]. O código de utilitários foi colocado na pasta [Entities]. Por fim, a aplicação é configurada pelo ficheiro [Web.config];
- em [2], geramos a compilação do projeto;
- em [3], surgem erros.
Vamos examinar os erros, por exemplo, o seguinte:
![]()
e a sua explicação:
![]()
O tipo de [medecin.Id] é int?, enquanto o método [GetCreneauxMedecin] é do tipo int. Por isso, é necessário um cast. Este erro ocorre repetidamente ao longo do código porque as entidades no projeto ASP.NET/NHibernate tinham chaves primárias do tipo int, enquanto as do projeto ASP.NET/EF 5 são do tipo int?. Corrigimos todos os erros deste tipo e regeneramos o projeto. Deixam então de existir erros.
Há mais um detalhe a resolver antes de executar o projeto: a instanciação da camada [DAO] pela estrutura Spring. Isto é feito em [Global.asax]:
protected void Application_Start(object sender, EventArgs e)
{
// caching of certain database data
try
{
// layer instantiation [dao]
Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
...
}
catch (Exception ex)
{...
}
}
No programa de teste da camada [DAO], a camada [DAO] foi instanciada da seguinte forma:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
Os dois métodos são idênticos. Recorde-se que esta instanciação da camada [DAO] dependia de uma configuração especificada em [App.config]. Substituímos então o conteúdo atual do [Web.config] do projeto web pelo do [App.config] do projeto da camada [DAO] para garantir a mesma configuração.
Estamos prontos para a primeira execução. A página inicial é apresentada [1]:
![]() |
- em [2], introduzimos uma data de marcação e submetemos;
![]() |
- em [3], ocorre um erro.
Ao examinar a mensagem de erro apresentada pela página, verificamos que a exceção relatada está relacionada com o Lazy Loading: tentámos carregar uma dependência de um objeto enquanto o contexto de persistência que o geria estava fechado. O objeto encontra-se agora num estado «desligado». Este erro deve-se ao facto de o NHibernate ter sido utilizado no modo Eager Loading, enquanto o EF 5 funciona por predefinição no modo Lazy Loading. Na linha destacada a vermelho acima:
- rdv representa um objeto [Rv] que foi carregado sem as suas dependências;
- Para avaliar rdv.Creneau.Id, a aplicação tenta carregar a dependência rdv.Creneau. No entanto, uma vez que já não nos encontramos nesse contexto, tal não é possível, daí a exceção.
Aqui, a solução é simples. Linha 108: Criamos uma entrada num dicionário com a chave primária do intervalo de horário da marcação como chave. No entanto, verifica-se que a entidade [Rv] encapsula a chave primária do intervalo associado. Por isso, escrevemos:
dicoRvPris[(int)rdv.CreneauId] = rdv;
Tentamos executar o código novamente. Desta vez, o erro é o seguinte:
![]() |
O erro é semelhante. Linha 132: tentamos carregar a dependência [Client] de um objeto [Rv] na camada ASP.NET, o que está fora de contexto. Temos de recuperar o objeto [Client] da base de dados. Para resolver este problema, a interface [IDao] foi melhorada com o seguinte método:
// find a T entity via its primary key
T Find<T>(int id) where T : class;
Isto permitir-nos-á recuperar dependências. Assim, a linha errada acima será reescrita da seguinte forma:
Client client = Global.Dao.Find<Client>(agenda.Creneaux[i].Rdv.ClientId);
Mais uma vez, observamos a vantagem de as entidades incorporarem as suas chaves estrangeiras. Aqui, a entidade [Rdv] dá-nos acesso à chave estrangeira da dependência [Creneau] associada. Com estas duas correções feitas, a aplicação funciona. O leitor é convidado a testar a aplicação [RdvMedecins-SqlServer-03] disponível nos downloads de exemplos no site deste artigo.
3.7. Conclusão
Conseguimos portar com sucesso uma aplicação ASP.NET / NHibernate:
![]() |
para uma aplicação ASP.NET / EF 5:
![]() |
Embora esta arquitetura devesse ter-nos permitido manter a camada [ASP.NET] intacta, tivemos de a modificar por duas razões:
- as entidades não eram exatamente iguais. O tipo de chave primária para as entidades NHibernate era
int, enquanto que para o EF 5 eraint?</span>**<span style="color: #000000">. Isto levou-nos a introduzir conversões no código web; - o modo de carregamento das entidades não era o mesmo para os dois ORMs: Eager Loading para o NHibernate, Lazy Loading para o EF 5. Isto levou-nos a melhorar a interface da camada [DAO] com um método genérico que nos permite recuperar uma entidade através da sua chave primária.
No entanto, a migração revelou-se bastante simples, justificando mais uma vez — se é que era necessária prova — a arquitetura em camadas e a injeção de dependências com o Spring ou outra estrutura de injeção de dependências.
Vamos agora avaliar o impacto de uma mudança de SGBD na arquitetura anterior. Iremos portar todos os projetos anteriores para quatro outros SGBDs:
- Oracle Database Express Edition 11g Release 2;
- MySQL 5.5.28;
- PostgreSQL 9.2.1;
- Firebird 2.1.
O código permanecerá inalterado. Apenas os seguintes elementos serão alterados:
- a definição nas entidades do campo utilizado para controlar o acesso simultâneo a uma entidade;
- os ficheiros de configuração [App.config] ou [Web.config];
Iremos comentar apenas os elementos que estão a sofrer alterações.


















































































































































