3. Caso di studio con SQL Server Express 2012
3.1. Introduzione
La maggior parte degli esempi disponibili online per Entity Framework sono esempi che utilizzano SQL Server. Ciò è del tutto normale. Probabilmente si tratta del DBMS più utilizzato nel mondo .NET aziendale. Seguiremo questa tendenza. Gli esempi saranno poi estesi a tutti i database menzionati nella sezione 1.2.
3.2. Installazione degli strumenti
Non descriveremo l'installazione degli strumenti. Ciò richiederebbe un gran numero di screenshot che diventerebbero rapidamente obsoleti. Si tratta di un compito (certamente non sempre facile) che lasciamo al lettore.
Dobbiamo installare i seguenti strumenti:
- il DBMS SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Scaricare la versione "With Tools", che include uno strumento di amministrazione con il DBMS:
Una volta installato il DBMS, lo avviamo:
![]() |
![]() |
- [1]: Dal menu Start, avvia "SQL Server Configuration Manager";
- [2]: In questo gestore, avviare il server;
- [3]: Ora è in esecuzione.
Ora avvia lo strumento di amministrazione di SQL Server:
![]() |
- [1]: Dal menu Start, avvia "SQL Server Management Studio";
- [2]: lo strumento di amministrazione.
Ci collegheremo al server:
![]() |
- In [1], aprire Esplora oggetti;
- in [2], inserisci i parametri di connessione:
- [3]: il server (locale) (notare le parentesi obbligatorie) si riferisce al server installato sul computer,
- [4]: Seleziona Autenticazione Windows. Per effettuare questa connessione devi essere un amministratore del computer,
![]() |
- [6]: La connessione è stata stabilita;
- [7]: Vuoi modificare alcune proprietà del server;
![]() |
- [8]: Richiediamo che siano disponibili due modalità di autenticazione:
- autenticazione Windows, come appena utilizzata. Un utente Windows con le autorizzazioni appropriate può quindi effettuare l'accesso,
- autenticazione SQL Server. L'utente deve essere uno di quelli registrati nel sistema di gestione del database;
Una volta fatto ciò, possiamo verificare le proprietà del server;
- [9]: Modificare le proprietà dell'utente "sa" (amministratore di sistema);
![]() |
- in [10], impostare una password per l'utente. Nel resto di questo documento, la password è sqlserver2012;
![]() |
- In [10], concedere loro l'autorizzazione a connettersi;
- in [11], la connessione è abilitata. Ora è possibile confermare la procedura guidata;
- In [12], disconnettersi dal server.
Ora, ci ricolleghiamo utilizzando le credenziali sa/sqlserver2012:
![]() |
- In [1], ci ricolleghiamo;
- in [2], durante l'autenticazione di SQL Server;
- In [3], l'utente è sa;
- in [4], la sua password è sqlserver2012;
- in 5, effettuiamo l'accesso;
![]() |
- in [6], abbiamo effettuato l'accesso.
Ora creeremo un database di prova:
![]() |
- in [1], creare un nuovo database;
- in [2], assegnagli il nome demo;
- in [3], clicca su "Convalida";
![]() |
- in [4], il database viene creato;
- in 5, creare una nuova tabella nel database "demo";
![]() |
![]() |
![]() |
![]() |
- in [6], definiamo una tabella con due colonne, ID e NAME;
- in [7], definiamo la colonna [ID] come chiave primaria;
- in [8], la chiave primaria è rappresentata da una chiave;
- in [9], la tabella viene salvata;
- In [10], le diamo un nome;
- In [11], per far apparire la tabella nel database [demo], è necessario aggiornare il database;
- in [12], la tabella [PERSONNES] è stata creata con successo.
Ora sappiamo abbastanza sull'uso di SQL Server Management Studio.
3.3. Il server integrato (localdb)\v11.0
VS Express 2012 include un SQL Server integrato. In questo caso si presume che VS Express 2012 sia stato installato [http://www.microsoft.com/visualstudio/fra/downloads]. Avviare VS 2012 [1]:
![]() |
Avviare SQL Server 2012 Management Studio [2] ed effettuare l'accesso [3].
![]() |
- In [4], connettersi al server (localdb)\v11.0;
- In 5, utilizzare l'autenticazione Windows;
- In [6], una volta stabilita la connessione, vengono visualizzati i database del server. Come in precedenza, è possibile creare un nuovo database.
Non useremo questo server incorporato in VS 2012.
3.4. Creazione del database dalle entità
Entity Framework 5 Code First consente di creare un database dalle entità. È proprio questo che vedremo ora. Utilizzando VS Express 2012, creiamo un progetto console iniziale in C#:
![]() |
![]() |
- in [1], la definizione del progetto;
- in [2], il progetto creato.
Tutti i nostri progetti dovranno includere , la DLL di Entity Framework 5. La aggiungiamo:
![]() |
- in [1], lo strumento NuGet consente di scaricare le dipendenze;
![]() |
- in [2], scarichiamo la dipendenza Entity Framework;
- in [3], il riferimento è stato aggiunto al progetto.
Per ulteriori informazioni, è possibile visualizzare le proprietà del riferimento aggiunto:
![]() |
- in [1], la versione della DLL. È necessaria la versione 5;
- in [2], la sua posizione nel file system: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll dove <solution> è la cartella della soluzione VS. Tutti i pacchetti aggiunti da NuGet andranno nella cartella <solution>/packages;
- in [3], è stato creato un file [packages.config]. Il suo contenuto è il seguente:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>
Elenca i pacchetti importati da NuGet.
Torniamo al progetto VS e creiamo una cartella [Models] all'interno del progetto:
![]() |
- in [1], aggiungendo una cartella al progetto;
- in [2], verrà denominata [Models].
Continueremo a seguire questa prassi di collocare le definizioni delle nostre entità nella cartella [Models].
Per creare le nostre entità, useremo la definizione del database MySQL 5 utilizzata nel progetto NHibernate. Rivediamo il ruolo delle entità EF:
![]() |
Le entità devono rispecchiare le tabelle del database. Il livello di accesso ai dati utilizza queste entità invece di lavorare direttamente con le tabelle. Iniziamo con la tabella [DOCTORS]:
3.4.1. L'entità [Medecin]
Contiene informazioni sui medici gestiti dall'applicazione [RdvMedecins].
![]() | ![]() |
- ID: il numero ID del medico — la chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- LAST_NAME: il cognome del medico
- FIRST NAME: il nome del medico
- TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)
Potremmo iniziare con la seguente 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; }
}
- Riga 3: La classe [Medecin] è associata alla tabella [MEDECINS] nel database. Questa tabella si troverà in uno schema denominato "dbo".
Inseriamo questa classe in un file denominato [Entities.cs] [1]. È qui che inseriremo tutte le nostre entità.
![]() |
Sempre nella cartella [Models], creiamo il seguente file [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>
{
}
}
- riga 8: la classe [RdvMedecinsContext] rappresenterà il contesto di persistenza, ovvero l'insieme delle entità gestite dall'ORM. Deve derivare dalla classe [System.Data.Entity.DbContext];
- riga 11: il campo [Medecins] rappresenta le entità [Medecin] nel contesto di persistenza. È di tipo DbSet<Medecin>. Generalmente ci sono tanti [DbSet] quanti sono i tavoli nel database, uno per tavolo;
- riga 15: definiamo una classe [RdvMedecinsInitializer] per inizializzare il database creato. Qui, essa deriva dalla classe [DropCreateDataBaseAlways], che, come suggerisce il nome, elimina il database se già esiste e poi lo ricrea. Ciò è utile durante la fase di sviluppo del database. Il parametro della classe [DropCreateDataBaseAlways] è il tipo di contesto di persistenza associato al database. Per la classe di inizializzazione è possibile utilizzare altre classi padre oltre a [DropCreateDataBaseAlways]:
- [DropCreateDatabaseIfModelChanges]: ricrea il database se le entità sono state modificate,
- [CreateDatabaseIfNotExists]: crea il database se non esiste;
Dobbiamo ancora creare un programma principale. Sarà il seguente [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);
}
}
}
}
- riga 12: [System.Data.Entity.DataBase] è una classe che fornisce metodi statici per la gestione del database associato a un contesto di persistenza. Il metodo statico [SetInitializer] consente di specificare la classe di inizializzazione del database. Ciò non avvia l'inizializzazione;
- riga 13: per lavorare con un contesto di persistenza, è necessario istanziarlo. Questo è ciò che viene fatto qui. Viene utilizzata un'istruzione using in modo che il contesto venga chiuso automaticamente al termine dell'istruzione. Pertanto, alla riga 17, il contesto viene chiuso;
- Riga 15: inneschiamo esplicitamente la generazione del database associato al contesto di persistenza [RdvMedecinsContext]. Il parametro false indica che questa operazione non deve essere eseguita se è già stata effettuata per questo contesto. Qui, avremmo potuto altrettanto facilmente impostarlo su true.
Quando si lavora con un database, i parametri di connessione sono generalmente memorizzati nel file [App.config]. Si noti che per ora non sono presenti:
<?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>
Gli elementi sopra riportati sono stati aggiunti al file [App.config] quando la dipendenza Entity Framework è stata aggiunta ai riferimenti del progetto.
Eseguiamo il progetto (Ctrl-F5) dopo aver avviato SQL Server Express (questo è importante):
![]() | ![]() |
L'esecuzione dovrebbe completarsi senza errori. Ora apriamo SQL Server Management Studio e aggiorniamo la vista:
![]() |
Possiamo vedere che è stato creato un database con il nome completo della classe [RdvMedecinsContext] e che contiene una tabella denominata [dbo.MEDECINS] (il nome che le abbiamo dato) con colonne che corrispondono ai nomi dei campi dell'entità [Medecin]. Se il codice è stato eseguito correttamente ma il database sopra indicato non compare, controllare il server incorporato (localdb)\v11.0 (vedere pagina 19). Con VS 2012 Pro, questo server viene utilizzato se SQL Server non è attivo al momento dell'esecuzione del codice. Con VS 2012 Express, non è così.
Esaminiamo la struttura della tabella [MEDECINS]:
- utilizza i nomi dei campi dell'entità [Medecin];
- la colonna [Id] è la chiave primaria. Questa è una convenzione EF: se l'entità E ha un campo Id o Eid (MedecinId), allora questa colonna è la chiave primaria nella tabella associata;
- i tipi delle colonne nella tabella sono quelli dei campi dell'entità;
- per le colonne Titolo, Cognome e Nome è stato utilizzato un tipo [nvarchar(max)]. Potremmo essere più specifici: 5 caratteri per il titolo, 30 per il cognome e il nome;
- Le colonne Titolo, Cognome e Nome possono avere un valore NULL. Modificheremo questa impostazione.
Diamo un'occhiata alle proprietà della chiave primaria [Id]:
![]() |
In [1] vediamo che la chiave primaria è di tipo [Identity], il che significa che il suo valore viene generato automaticamente da SQL Server. Adotteremo questa strategia per tutti i DBMS.
Ci affideremo meno alle convenzioni EF utilizzando le annotazioni. Il codice dell'entità in [Entities.cs] diventa il seguente:
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; }
}
}
- Righe 2 e 3: Le annotazioni si trovano nello spazio dei nomi [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) e nello spazio dei nomi [System.ComponentModel.DataAnnotations.Schema] (Column). Ulteriori annotazioni sono disponibili all'URL [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
- riga 11: [Key] indica la chiave primaria;
- riga 12: [Column] imposta il nome della colonna corrispondente al campo;
- riga 14: [Required] indica che il campo è obbligatorio (SQL NOT NULL);
- riga 15: [MaxLength] imposta la lunghezza massima della stringa, [MinLength] la sua lunghezza minima;
Eseguiamo il progetto con questa nuova definizione dell'entità [Medecin]. Il database risultante è il seguente:
![]() |
- le colonne hanno i nomi che abbiamo loro assegnato;
- l'annotazione [Required] è stata tradotta in SQL NOT NULL;
- L'annotazione [MaxLength(N)] è stata mappata al tipo SQL nvarchar(N).
Nell'applicazione NHibernate, la colonna [VERSION] era presente per impedire l'accesso simultaneo alla stessa riga in una tabella. Il principio è il seguente:
- Un processo P1 legge una riga L dalla tabella [DOCTORS] al tempo T1. La riga ha versione V1;
- Un processo P2 legge la stessa riga L dalla tabella [DOCTORS] al momento T2. La riga ha versione V1 perché il processo P1 non ha ancora confermato la sua modifica;
- il processo P1 conferma la sua modifica alla riga L. La versione della riga L passa quindi a V2 = V1 + 1;
- Il processo P2 conferma la propria modifica alla riga L. L'ORM genera quindi un'eccezione perché il processo P2 ha una versione V1 della riga L che differisce dalla versione V2 presente nel database.
Questo si chiama gestione ottimistica della concorrenza. Con EF 5, un campo che svolge questo ruolo deve avere uno dei due attributi: [Timestamp] o [ConcurrencyCheck]. SQL Server ha un tipo [timestamp]. Il valore di una colonna di questo tipo viene generato automaticamente da SQL Server ogni volta che viene inserita o modificata una riga. Tale colonna può quindi essere utilizzata per gestire l'accesso concorrente. Per riprendere l'esempio precedente, il processo P2 troverà un timestamp diverso da quello che ha letto, perché nel frattempo la modifica apportata dal processo P1 lo avrà cambiato.
La nostra entità [Doctor] si evolve come segue:
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; }
}
}
- Righe 26–28: la nuova colonna con l'attributo [Timestamp] della riga 27. Il tipo di campo deve essere byte[] (riga 28). Il nome del campo può essere qualsiasi cosa. Non impostiamo l'attributo [Required] perché non sarà l'applicazione a fornire questo valore, bensì il DBMS stesso.
Se eseguiamo il progetto con questa nuova entità, il database si evolve come segue:
![]() |
C'è un ultimo punto da affrontare. Il contesto di persistenza "sa" che un'entità deve essere inserita nel database perché la sua chiave primaria è nulla in quel momento. È l'inserimento nel database che assegnerà un valore alla chiave primaria. In questo caso, il tipo int assegnato alla chiave primaria [Id] non è adatto perché questo tipo non accetta il valore nullo. Gli assegniamo quindi il tipo int?, che accetta i valori int oltre al puntatore null. L'entità [Medecin] utilizzata sarà quindi la seguente:
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
...
Dobbiamo ancora vedere come rappresentare il concetto di chiave esterna tra tabelle in un'entità.
3.4.2. L'entità [Creneau]
La tabella [CRENEAUX] elenca le fasce orarie in cui sono possibili gli appuntamenti:
![]() |
![]() |
- ID: il numero ID della fascia oraria — la chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- ID_MEDECIN: numero identificativo del medico a cui appartiene questa fascia oraria – chiave esterna sulla colonna MEDECINS(ID).
- START_TIME: ora di inizio della fascia oraria
- MSTART: minuto di inizio della fascia oraria
- HFIN: ora di fine della fascia oraria
- MFIN: minuti di fine della fascia oraria
La seconda riga della tabella [SLOTS] (vedi [1] sopra) indica, ad esempio, che la fascia n. 2 inizia alle 8:20 e termina alle 8:40 e appartiene al medico n. 1 (la dott.ssa Marie PELISSIER).
Con queste informazioni, possiamo definire l'entità [Creneau] come segue in [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; }
}
L'unica modifica riguarda le righe 20–21. Il fatto che la tabella [CRENEAUX] abbia una chiave esterna sulla tabella [MEDECINS] si riflette nell'entità [Creneau] con la presenza di un riferimento all'entità [Medecin] alla riga 21. Il nome del campo è irrilevante; conta solo il tipo. La proprietà deve essere dichiarata virtuale utilizzando la parola chiave virtual. Questo perché EF deve ridefinire tutte le cosiddette proprietà di navigazione, ovvero quelle corrispondenti a una chiave esterna e che consentono la navigazione tra le tabelle.
Per testare la nuova entità, dobbiamo apportare alcune modifiche in [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>
{
}
}
La riga 12 riflette il fatto che il contesto ha un'entità in più da gestire. Quando eseguiamo il progetto, otteniamo il seguente nuovo database:
![]() |
La tabella [CRENEAUX] è stata effettivamente creata e la novità è la presenza delle chiavi esterne [1] e [2]. I loro nomi sono stati generati dai nomi dei campi corrispondenti nell'entità (Medecin) con il suffisso "_Id". Per visualizzare le proprietà di questa chiave esterna, proviamo a modificarla [3].
![]() |
Lo screenshot sopra mostra che [Medecin_Id] è una chiave esterna nella tabella [CRENEAUX] e che fa riferimento alla chiave primaria [ID] nella tabella [MEDECINS].
Se creiamo le entità per un database esistente, la colonna della chiave esterna non si chiamerà necessariamente [Medecin_Id]. Per le altre colonne, abbiamo visto che l'annotazione [Column] ha risolto questo problema. Stranamente, per una chiave esterna è più complicato. Dobbiamo procedere come segue:
public class Creneau
{
// data
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
- righe 5-7: creiamo un campo di tipo chiave esterna (int). Utilizzando l'attributo [Column], specifichiamo il nome della colonna che fungerà da chiave esterna nella tabella associata all'entità;
- riga 9: aggiungiamo l'annotazione [ForeignKey] al campo di tipo [Medecin]. L'argomento di questa annotazione è il nome del campo (non della colonna) associato alla colonna della chiave esterna nella tabella.
L'esecuzione del progetto in questo caso crea la seguente tabella:
![]() |
Come si vede sopra, la colonna della chiave esterna porta effettivamente il nome che le abbiamo assegnato. Si noti che i campi:
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
hanno portato alla creazione di una sola colonna, la colonna [MEDECIN_ID]. Tuttavia, la presenza del campo [MedecinId] è importante. Quando si legge una riga dalla tabella [SLOTS], essa riceverà il valore della colonna [DOCTOR_ID], ovvero il valore della chiave esterna nella tabella [DOCTORS]. Ciò è spesso utile.
Il campo [Medecin] sopra riportato riflette la relazione molti-a-uno che collega l'entità [Creneau] all'entità [Medecin]. Più oggetti [Slot] sono collegati allo stesso [Doctor]. La relazione inversa, in cui un singolo oggetto [Doctor] è associato a più oggetti [Slot], può essere modellata utilizzando un campo aggiuntivo nell'entità [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; }
Alla riga 8 abbiamo aggiunto il campo [Slots], che è una raccolta di oggetti [Slot]. Questo campo ci consentirà di accedere a tutte le fasce orarie disponibili del medico.
Quando eseguiamo nuovamente il progetto, vediamo che la tabella [DOCTORS] non è cambiata:
![]() |
Non è stata aggiunta alcuna colonna. La relazione di chiave esterna tra la tabella [CRENEAUX] e la tabella [MEDECINS] è sufficiente affinché EF generi i campi correlati:
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; }
...
}
Conosciamo le basi. Possiamo concludere creando le altre due entità.
3.4.3. Le entità [Client] e [Appointment]
Con ciò che abbiamo imparato, possiamo scrivere le entità [Client] e [Appointment]. L'entità [Client] contiene informazioni sui clienti gestiti dall'applicazione [DoctorAppointments].
![]() | ![]() |
- ID: il numero ID del cliente, ovvero la chiave primaria della tabella
- VERSION: numero che identifica la versione della riga nella tabella. Questo numero viene incrementato di 1 ogni volta che viene apportata una modifica alla riga.
- LAST_NAME: il cognome del cliente
- FIRST NAME: il nome del cliente
- TITOLO: il titolo (Sig.ra, Sig.ra, Sig.)
L'entità [Cliente] potrebbe essere la seguente:
[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; }
}
La classe [Client] è quasi identica alla classe [Doctor]. Potrebbero derivare dalla stessa classe padre. Il nuovo elemento si trova alla riga 21. Esso riflette il fatto che un cliente può avere più appuntamenti e deriva dalla presenza di una chiave esterna dalla tabella [RVS] alla tabella [CLIENTS].
L'entità [Rv] rappresenta un appuntamento:
![]() |
- ID: numero che identifica in modo univoco l'appuntamento – chiave primaria
- DAY: giorno dell'appuntamento
- SLOT_ID: fascia oraria dell'appuntamento – chiave esterna sulla colonna [ID] della tabella [SLOTS] – determina sia la fascia oraria che il medico coinvolto.
- CLIENT_ID: ID del cliente per il quale è stato fissato l’appuntamento – chiave esterna sulla colonna [ID] della tabella [CLIENTS]
L'entità [Rv] potrebbe essere la seguente:
[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; }
}
- righe 5-7: chiave primaria;
- righe 8-10: data dell'appuntamento;
- righe 11-12: la chiave esterna dalla tabella [RVS] alla tabella [CLIENTS];
- righe 13–15: il cliente con l'appuntamento;
- righe 16–17: la chiave esterna dalla tabella [RVS] alla tabella [CRENEAUX];
- righe 18–20: la fascia oraria dell'appuntamento;
- Righe 21-23: il campo di controllo dell'accesso simultaneo.
Nella riga 17, vediamo una relazione molti-a-uno: una singola fascia oraria può corrispondere a più appuntamenti (non nello stesso giorno). La relazione inversa può essere riflessa nell'entità [Creneau]:
public class Creneau
{
// niche Rvs
public ICollection<Rv> Rvs { get; set; }
...
}
Riga 4: la raccolta degli appuntamenti programmati per questa fascia oraria.
Quando il progetto viene eseguito, il database generato è il seguente:
![]() |
Le tabelle [DOCTORS] e [SLOTS] non sono cambiate. Le tabelle [CLIENTS] e [APPs] sono le seguenti:
![]() | ![]() |
Questo è ciò che ci aspettavamo. Abbiamo ancora alcuni dettagli da chiarire:
3.4.4. Impostazione del nome del database
Per impostare il nome del database generato da EF, useremo una stringa di connessione definita in [App.config]. Questo file di configurazione cambia come segue:
<?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>
- righe 15–19: la stringa di connessione al database;
- riga 16: l'attributo [name] utilizza il nome della classe [RdvMedecinsContext] utilizzata per il contesto di persistenza. È importante ricordarlo. Questo vincolo può essere aggirato nel costruttore del contesto:
// manufacturer
public RdvMedecinsContext()
: base("monContexte")
{
}
In questo caso, possiamo impostare name= "myContext". Questo è il valore che useremo nel resto del documento.
- Riga 17: la stringa di connessione. [Data Source]: il nome del server che ospita il DBMS; [Initial Catalog]: il nome del database, in questo caso [rdvmedecins-ef]; [User Id]: il proprietario della connessione; [Password]: la password del proprietario. Il lettore dovrebbe adattare questa stringa al proprio ambiente;
- Righe 21–29: definiscono un [DbProviderFactory]. Non so cosa sia. A giudicare dal nome, potrebbe essere una classe utilizzata per generare il livello [ADO.NET] che separa EF dal DBMS:
![]() |
In realtà, queste righe non sono necessarie per SQL Server, ma ho dovuto aggiungerle per altri DBMS. Quindi le includo qui come riferimento. Non causano alcun problema. L'unico punto importante è la versione nella riga 27. È la versione della DLL [System.Data] elencata nei riferimenti del progetto:
![]() |
Ecco fatto. Siamo pronti. Eseguiamo il progetto e otteniamo il seguente database [rdvmedecins-ef]:
![]() |
Questo sarà il nostro database finale. Non resta che inserirvi i dati.
3.4.5. Compilazione del database
La classe di inizializzazione del database può essere utilizzata per inserire dati al suo interno:
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] });
}
}
}
- riga 6: l'inizializzazione avviene nel metodo [Seed]. Questo metodo esiste nella classe padre. Qui viene ridefinito. L'argomento è il contesto di persistenza dell'applicazione [RdvMedecinsContext];
- riga 8: l'argomento viene passato alla classe padre; è probabile che la classe padre apra il contesto di persistenza che le è stato passato, poiché questo contesto non dovrà più essere aperto in seguito;
- righe 11–16: creazione di 4 client;
- righe 17–20: questi vengono aggiunti al contesto di persistenza, più specificamente ai suoi medici. Si noti il metodo [Add] che lo rende possibile. Ricordiamo la definizione del contesto:
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; }
...
Si dice anche che i Clients sono stati collegati al contesto, ovvero ora sono gestiti da EF. In precedenza, erano scollegati. Esistevano come oggetti ma non erano gestiti da EF;
- righe 21–27: creazione di 4 medici;
- righe 28–31: aggiunta al contesto di persistenza;
- righe 33–70: creazione di intervalli di tempo. Righe 34–57 per il medico medecins[0], righe 58–69 per il medico medecins[1]. Gli altri medici non hanno intervalli di tempo;
- righe 71–74: questi intervalli di tempo vengono inseriti nel contesto di persistenza;
- riga 76: creazione di un appuntamento per il primo cliente con la prima fascia oraria e inserimento nel contesto di persistenza.
Quando il progetto viene eseguito, si ottiene il seguente database:
![]() | ![]() |
Sopra, vediamo la tabella [CLIENTS] popolata.
3.4.6. Modifica delle entità
Attualmente, le classi [Doctor] e [Client] sono quasi identiche. Infatti, se rimuoviamo i campi aggiunti per la gestione della persistenza con EF 5, sono identiche. Le faremo derivare da una classe [Person]. Queste due entità diventano quindi le seguenti:
// 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 il progetto viene eseguito, viene generato lo stesso database. EF 5 ha mappato ciascuna delle classi più basse nella gerarchia di ereditarietà a una tabella separata. Infatti, EF 5 dispone di diverse strategie di generazione delle tabelle per rappresentare l'ereditarietà delle entità in un . Non le tratteremo in questa sede. Ad esempio, è possibile leggere " Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type" all'URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].
Ora useremo questa versione delle entità.
3.4.7. Aggiunta di vincoli al database
C'è un altro dettaglio da affrontare. La tabella [RVS] per gli appuntamenti è la seguente:
![]() |
Questa tabella deve avere un vincolo di unicità: per un dato giorno, la fascia oraria di un medico può essere prenotata solo una volta per un appuntamento. In termini di tabella, ciò significa che la coppia (GIORNO, ID_FASCIA_ORARIA) deve essere unica. Non so se questo vincolo possa essere espresso direttamente nel codice, né sulle entità né nel contesto. È probabile, ma non ho verificato. Adotteremo un approccio diverso. Useremo un client di gestione di SQL Server per aggiungere questo vincolo.
Utilizzando "SQL Server Management Studio", non ho trovato un modo semplice per aggiungere questo vincolo se non quello di eseguire l'istruzione SQL che lo crea:
![]() |
- in [1] creiamo una query SQL per il database [rdvmedecins-ef];
- in [2], la query SQL che crea il vincolo di unicità;
- in [3], l'esecuzione di questa query ha creato un nuovo indice nella tabella [RVS].
Esistono altri strumenti di amministrazione di SQL Server. In questo caso, utilizzeremo lo strumento gratuito EMS SQL Manager for SQL Server [http://www.sqlmanager.net/fr/products/mssql/manager/download]. Una volta installato, lo avviamo:
![]() |
- in [1], registriamo un database;
- in [2], ci connettiamo al server (locale);
- in [3], utilizzando l'autenticazione SQL Server;
- in [4], con il nome utente sa;
- in 5, e la password sqlserver2012;
- in [6], si procede al passaggio successivo;
![]() |
- in [7], selezionare il database [rdvmedecins-ef];
- in [8], completare la procedura guidata;
- in [9], il database appare nella struttura ad albero dei database. Connettiti ad esso [10];
- in [11], la connessione è stabilita.
"SQL Manager Lite for SQL Server" consente di creare il vincolo univoco sulla tabella [RVS].
![]() |
- In [1] è possibile vedere il vincolo di unicità creato in precedenza;
- In [2], eliminarlo;
- In [3], l'indice corrispondente a questo vincolo di unicità è scomparso.
Ricreiamo il vincolo eliminato:
![]() |
- In [1], creiamo un nuovo indice per la tabella [RVS];
- in [2], gli assegniamo un nome;
- in [3], si tratta di un vincolo di unicità;
- in [4], sulle colonne DAY e SLOT_ID;
La scheda DDL fornisce il codice SQL da eseguire:
![]() |
- in [6], compiliamo l'istruzione SQL;
![]() |
- in [7], confermiamo;
- in [8], il nuovo indice è apparso.
L'interfaccia fornita da "SQL Manager Lite for SQL Server" è simile a quella di "SQL Server Management Studio". Interfacce simili sono disponibili per i database Oracle, PostgreSQL, Firebird e MySQL. Continueremo quindi con questa famiglia di strumenti di amministrazione dei database.
Per accedere alle informazioni su una tabella, è sufficiente fare doppio clic su di essa:
![]() |
Le informazioni relative alla tabella selezionata sono disponibili nelle schede. Sopra, vediamo la scheda [Campi] della tabella [CLIENTI]. La scheda [Dati] mostra il contenuto della tabella:

3.4.8. Il database finale
Ora disponiamo del nostro database finale. Esportiamo il relativo script SQL in modo da poterlo rigenerare se necessario.
![]() |
- in [1], avvio della procedura guidata;
- in [2], il server;
- in [3], il database da esportare;
![]() |
- in [4], specificare il nome del file in cui verrà salvato lo script SQL;
- in 5, specificare la codifica;
- in [6], specificare cosa si desidera estrarre (tabelle, vincoli, dati);
![]() |
- in [7], è possibile perfezionare lo script che verrà generato;
- in [8], completare la procedura guidata.
Lo script è stato generato e caricato nell'editor di script. È possibile visualizzare il codice SQL generato. Ricostruiremo il database utilizzando questo script.
![]() |
- In [1], eliminare il database;
- In [2] e [3], lo ricreiamo;
![]() |
- In [4], effettuare l'accesso;
- in 5, eseguire lo script SQL per creare il database;
![]() |
- in [6], lo salviamo in "SQL Manager";
- In [7], ci colleghiamo al database appena creato;
![]() |
- in [8], il database al momento non contiene tabelle;
- In [9a], apri un editor di script SQL;
![]() |
- In [9b], aprire lo script SQL creato in precedenza;
- In [10], eseguirlo;
![]() |
- in [11], le tabelle sono state create;
- in [12], sono state popolate;
![]() |
- in [14], vediamo il vincolo univoco che abbiamo creato per la tabella [RVS].
Ora lavoreremo con questo database esistente. Se dovesse andare perso o danneggiarsi, sappiamo come rigenerarlo.
3.5. Lavorare con il database utilizzando Entity Framework
Faremo quanto segue:
- aggiungere, eliminare e modificare elementi del database;
- eseguire query sul database utilizzando LINQ to Entities;
- gestire l'accesso simultaneo allo stesso elemento del database;
- comprendere i concetti di Lazy Loading ed Eager Loading;
- scoprire che gli aggiornamenti del database tramite il contesto di persistenza avvengono all'interno di una transazione.
3.5.1. Eliminazione di elementi dal contesto di persistenza
Abbiamo un database popolato. Lo svuoteremo. Creiamo una nuova classe [Erase.cs] nel progetto corrente [1]:
![]() |
La classe [Erase] è la seguente:
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();
}
}
}
}
- riga 9: le operazioni su un contesto di persistenza vengono sempre eseguite all'interno di un blocco [using]. Ciò garantisce che, al termine del blocco [using], il contesto sia stato chiuso;
- riga 13: si esegue un'iterazione sul contesto dei client [context.Clients]. Tutti i client presenti nel database verranno inseriti nel contesto di persistenza;
- riga 15: per ciascuno di essi, eseguiamo l'operazione [Remove], che li rimuove dal contesto. In realtà, essi sono ancora nel contesto ma in uno stato "rimosso";
- righe 18–21: facciamo lo stesso per i medici;
- riga 23: salviamo il contesto di persistenza nel database.
Quando si salva il contesto nel database, le entità nel contesto che:
- hanno una chiave primaria nulla sono soggette a un'operazione SQL INSERT;
- si trovano in uno stato "eliminato" sono soggette a un'operazione SQL DELETE;
- si trovano in uno stato "modificato" sono soggette a un'operazione SQL UPDATE;
Come vedremo in seguito, queste operazioni SQL vengono eseguite all'interno di una transazione. Se una di esse fallisce, tutto ciò che è stato fatto in precedenza viene annullato.
Facciamo del programma [Erase] il nuovo punto di partenza per il progetto [1] e poi eseguiamo il progetto.
![]() |
Controlliamo il database. Vedremo che tutte le tabelle sono vuote [2]. Questo è sorprendente, dato che abbiamo semplicemente chiesto di eliminare i medici e i clienti. È grazie al meccanismo delle chiavi esterne che le altre tabelle sono state svuotate in modo a cascata.
La definizione della chiave esterna dalla tabella [CRENEAUX] alla tabella [MEDECINS] è stata definita come segue dal provider EF 5:
![]() |
- In [1], selezionare la tabella [CRENEAUX];
- in [2], selezionare la scheda delle chiavi esterne;
- in [3], modificare la singola chiave esterna;
![]() |
- in [4], nella scheda DDL, la definizione SQL del vincolo di chiave esterna;
- in 5, la clausola ON DELETE CASCADE garantisce che l'eliminazione di un medico comporti l'eliminazione delle fasce orarie ad esso associate.
I vincoli di chiave esterna per la tabella [RVS] sono definiti in modo analogo:
- Righe 1-6: L'eliminazione di un cliente comporterà anche l'eliminazione degli appuntamenti ad esso associati;
- Righe 1-6: L'eliminazione di una fascia oraria comporterà anche l'eliminazione di tutti gli appuntamenti ad essa associati.
3.5.2. Aggiunta di elementi al contesto di persistenza
Ora che abbiamo svuotato il database, lo riempiremo nuovamente. Aggiungiamo il programma [Fill.cs] [1] al progetto.
![]() |
Il programma [Fill.cs] è il seguente:
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();
}
}
}
}
- riga 10: apriamo il contesto di persistenza;
- righe 13–20: le righe delle tabelle [CLIENTS] e [DOCTORS] vengono aggiunte al contesto e poi rimosse da esso. Abbiamo appena visto che questo ha svuotato completamente il database;
- righe 22–88: gli elementi vengono aggiunti al contesto di persistenza. Tutti hanno una chiave primaria nulla. Verranno quindi inseriti nel database;
- riga 90: le modifiche apportate al contesto vengono sincronizzate con il database. Il database sarà oggetto di una serie di operazioni SQL DELETE seguite da una serie di operazioni SQL INSERT;
Impostiamo il programma [Fill] come nuovo oggetto di partenza per il progetto [1] e poi lo eseguiamo.
![]() |
In [2] possiamo vedere che le tabelle sono state popolate.
3.5.3. Visualizzazione del contenuto del database
Ora visualizzeremo il contenuto del database utilizzando le query LINQ to Entities. LINQ (Language-Integrated Query) è stato introdotto con .NET Framework 3.5 nel 2007. Funziona come un'estensione dei linguaggi .NET, il che significa che è integrato nel linguaggio e la sua sintassi è convalidata dal compilatore. Consente di interrogare varie raccolte utilizzando una sintassi simile a quella di SQL (Structured Query Language) per l'interrogazione dei database. Esistono diverse versioni di LINQ:
- LINQ to Objects, per l'interrogazione di raccolte in memoria;
- LINQ to XML, per l'interrogazione di XML;
- LINQ to Entity, per l'interrogazione di database;
LINQ si basa su numerose estensioni dei linguaggi .NET. Queste possono essere utilizzate al di fuori di LINQ. Non le presenteremo qui, ma forniremo semplicemente due riferimenti dove il lettore potrà trovare una descrizione approfondita di LINQ:
- LINQ in Action, di Fabrice Marguerie, Steve Eichert e Jim Wooley, pubblicato da Manning;
- LINQ Pocket Reference, di Joseph e Ben Albahari, pubblicato da O’Reilly.
Ho letto il primo e l’ho trovato eccellente. Non ho letto il secondo, ma ho letto “C# 3.0 in a Nutshell” degli stessi autori quando è stato rilasciato LINQ. Ho trovato quel libro di gran lunga superiore alla media dei libri che leggo di solito. Sembra che gli altri libri di questi due autori siano dello stesso calibro. Useremo anche LINQPad, uno strumento di apprendimento di LINQ scritto da Joseph Albahari.
Visualizzeremo le entità nel database. Per farlo, aggiungeremo due metodi di visualizzazione alle loro classi. Cominciamo con l'entità [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;
}
}
- righe 27–30: il metodo ToString della classe. Si noti che non visualizza la collezione della riga 21;
- righe 32–37: il metodo ShortIdentity, che fa la stessa cosa.
A questo punto, è necessario spiegare i concetti di caricamento pigro (Lazy Loading) e caricamento immediato (Eager Loading) per valutare l’impatto dei due metodi precedenti. Abbiamo visto che un’entità può avere dipendenze da un’altra entità. Queste dipendenze sono di due tipi:
- uno-a-molti, come sopra, dove un medico è collegato a più fasce orarie;
- molti-a-uno, come nell'entità [Slot] qui sotto, dove uno o più slot sono collegati allo stesso medico;
public class Creneau
{
// data
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
Quando le dipendenze vengono caricate contemporaneamente alle entità a cui sono associate, si parla di Eager Loading. In caso contrario, si parla di Lazy Loading: le dipendenze vengono caricate solo quando vengono referenziate per la prima volta. Per impostazione predefinita, EF 5 utilizza il Lazy Loading: le dipendenze non vengono caricate contemporaneamente all'entità.
Diamo un'occhiata al nostro metodo [ToString] sopra riportato:
// 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();
}
Il metodo [ToString] non mostra la dipendenza [Slots] alla riga 2. Se l'avesse fatto, avrebbe forzato il caricamento di tutti gli slot del medico prima dell'esecuzione. È proprio per evitare questo caricamento dispendioso che la dipendenza non è stata inclusa nella firma dell'entità. In generale, includeremo due firme in ogni entità:
- un metodo ToString che visualizzerà l'entità e qualsiasi dipendenza uno-a-molti. Come appena spiegato, questo attiverà il caricamento della dipendenza;
- un metodo ShortIdentity che non farà riferimento ad alcuna dipendenza. Pertanto, non verrà caricata alcuna dipendenza;
I metodi di visualizzazione per le altre entità saranno i seguenti:
L'entità [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();
}
}
- righe 9–12: il metodo [ToString] non visualizza la dipendenza alla riga 6;
L'entità [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));
}
}
- riga 16: il metodo [ToString] fa riferimento alla dipendenza alla riga 9. Questo ne forzerà il caricamento;
- riga 11: la dipendenza [Rvs] non è referenziata. Non verrà caricata;
- righe 21-22: il metodo [ShortIdentity] non fa più riferimento al riferimento [Medecin] della riga 9. Pertanto, non verrà caricato.
L'entità [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));
}
}
- righe 17–20: il metodo [ToString] fa riferimento alle dipendenze alle righe 9 e 14. Ciò ne imporrà il caricamento;
- righe 17–20: il metodo [ShortIdentity] impedisce ciò, quindi le dipendenze non verranno caricate.
In conclusione, dobbiamo prestare attenzione ai metodi [ToString] delle entità. Se non prestiamo attenzione a questo aspetto, la visualizzazione di una tabella può caricare metà del database se la tabella ha molte dipendenze.
Detto questo, scriviamo il seguente nuovo codice [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);
}
}
}
}
}
Spiegheremo le righe 17–21, che visualizzano le entità [Client]. La spiegazione fornita si applica anche alle altre entità.
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
- riga 3: la parola chiave var è stata introdotta con C# 3.0. Consente di evitare di specificare il tipo esatto di una variabile. Il compilatore deduce quindi il tipo dal tipo dell'espressione assegnata alla variabile;
- riga 3: l'espressione assegnata alla variabile clients è una query LINQ to Entities. Include parole chiave SQL portate in LINQ. La sintassi utilizzata qui è la seguente:
from variable in DbSet select variable
Una sintassi LINQ più generale è
from variable in collection select variable
La collezione verrà attraversata e, per ogni elemento in essa contenuto, la variabile verrà valutata. Ciò avviene solo quando la variabile [clients] della riga 3 viene iterata dal ciclo for/each nelle righe 4–7. Fino a quando ciò non accade, la variabile [clients] è semplicemente una query non valutata;
- riga 4: la query [clients] viene iterata. Ciò forzerà la valutazione della query. Le righe della tabella [CLIENTS] verranno inserite nel contesto di persistenza una per una;
- riga 6: il metodo [ToString] dell'entità [Client] viene utilizzato per la visualizzazione. Non vengono caricate dipendenze;
Passiamo alle righe di codice seguenti:
- Righe 24–28: le righe della tabella [DOCTORS] vengono inserite nel contesto di persistenza e visualizzate. Non vengono caricate dipendenze;
- righe 31–35: le righe della tabella [SLOTS] vengono inserite nel contesto di persistenza e visualizzate. Abbiamo visto che il metodo [ToString] di questa entità visualizza la dipendenza [Doctor]. Tuttavia, questa è già caricata. Pertanto, non ci sarà alcun ricaricamento;
- righe 38–42: le righe della tabella [RVS] vengono inserite nel contesto di persistenza e visualizzate. Abbiamo visto che il metodo [ToString] di questa entità visualizza le dipendenze [Client] e [Slot]. Tuttavia, queste sono già caricate. Pertanto, non ci sarà alcun nuovo caricamento.
Si noti che l'ordine di visualizzazione non è neutro. Se avessimo voluto visualizzare prima le entità [Rv], il loro metodo [ToString] avrebbe innescato il caricamento delle entità [Client] e [Creneau] collegate a questi appuntamenti. Le altre non sarebbero state caricate. Sarebbero state caricate in un secondo momento in un'altra vista. Ciò ha un impatto sulle prestazioni. Il codice precedente richiede quattro istruzioni SQL per visualizzare tutte le entità. Supponiamo ora di interrogare prima la tabella [RVS] degli appuntamenti. È necessaria una prima query SQL per la tabella [RVS]. Successivamente, il metodo [ToString] dell'entità [Rv] attiverà il potenziale caricamento delle entità associate [Client] e [Slot]. È richiesta una query SQL per ciascuna. Supponendo che ci siano N2 clienti e N3 fasce orarie e che tutte queste entità siano referenziate nella tabella [RVS], la visualizzazione richiederà 1+N2+N3 query SQL. Pertanto, le prestazioni sono inferiori rispetto alla versione che abbiamo esaminato. Per visualizzare la tabella [RVS] con le sue dipendenze, sarebbe necessario un join di tabelle. Ciò può essere ottenuto utilizzando LINQ. Torneremo su questo argomento con un esempio. Per ora, ricordiamo che dobbiamo prestare attenzione alle query SQL alla base del nostro codice LINQ.
Configuriamo il progetto per eseguire questo nuovo codice [1] e [2], quindi lo eseguiamo:
![]() |
L'output della console è il seguente:
3.5.4. Imparare LINQ con LINQPad
In precedenza, abbiamo utilizzato le query LINQ to Entity per visualizzare il contenuto delle tabelle del database. Joseph Albahari ha scritto un programma per aiutarti a imparare le diverse forme di LINQ. Te lo presenteremo ora.
LINQPad è disponibile al seguente URL [http://www.linqpad.net/]. Una volta installato, lo avviamo [1]:
![]() |
I principianti di LINQ possono iniziare con gli esempi presenti nella scheda [Samples] [2], che offre un'ampia varietà di esempi. Selezioniamo l'esempio [3], che si aprirà in una nuova finestra [4]. Il codice completo dell'esempio è il seguente:
// 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.
Le righe 3–5 sono un esempio di query LINQ to Objects. La query LINQ segue la sintassi:
from variable in collection orderby élément1 select élément2
- variabile si riferisce all'elemento corrente nella collezione. Nel nostro esempio, questa collezione è l'elenco di parole risultante dalla stringa suddivisa;
- la collezione è ordinata in base al parametro element1 di orderby. Nel nostro esempio, la collezione di parole sarà ordinata per lunghezza;
- la parola chiave select specifica ciò che vogliamo estrarre dalla variabile dell'elemento corrente nella collezione. Nel nostro esempio, questa sarà la parola.
Eseguiamo questa query LINQ:
![]() |
- in [1]: un'espressione LINQ viene eseguita premendo [F5] o utilizzando il pulsante Esegui;
- in [2]: la visualizzazione. Le parole vengono visualizzate in ordine di lunghezza. Questo semplice esempio dimostra la potenza di LINQ;
- in [3], è possibile scaricare altri esempi, compresi quelli tratti dal libro "LINQ in Action" [4];
![]() |
- in 5, scegliamo un esempio tratto dal libro;
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);
}
- riga 4: una nuova query LINQ con nuove parole chiave;
- riga 5: la raccolta oggetto della query è l'array di parole della riga 1;
- riga 6: la collezione viene ordinata in ordine alfabetico per parola;
- riga 7: la collezione è raggruppata (in base alla parola chiave) in una nuova collezione chiamata lengthGroups. lengthGroups.Key rappresenta il fattore di raggruppamento (parola chiave), in questo caso la lunghezza delle parole. lengthGroups raggruppa le parole con lo stesso fattore di raggruppamento, ovvero la stessa lunghezza;
- riga 8: la collezione lengthGroups è ordinata in base alla chiave di raggruppamento in ordine decrescente, quindi in questo caso in base alla dimensione decrescente delle parole;
- riga 9: da questa raccolta vengono creati nuovi oggetti (classi anonime) con due campi:
- Length: la lunghezza delle parole,
- Words: le parole di quella lunghezza;
Qui possiamo vedere in particolare il vantaggio della parola chiave var nella riga 4. Poiché abbiamo usato una classe anonima nella riga 9, non possiamo specificare il tipo della variabile groups. Il compilatore, tuttavia, assegnerà un nome interno alla classe anonima e lo userà per tipizzare la variabile groups. Sarà quindi in grado di determinare se la variabile groups è usata correttamente
- Riga 12: iterazione sulla query della riga 4. È solo a questo punto che viene valutata. Ricordiamo che la sua esecuzione produrrà una raccolta di oggetti, specificata alla riga 9;
- Riga 14: Visualizziamo la proprietà Length dell'elemento corrente, ovvero la lunghezza delle parole;
- Righe 15–17: visualizziamo ciascun elemento della raccolta Words, ovvero l'insieme di parole con la lunghezza visualizzata in precedenza.
Quando eseguiamo questa query, otteniamo il seguente risultato in LINQPad:
![]() |
Ora che abbiamo visto alcuni esempi di query [LINQ to Object], diamo un'occhiata alle query [LINQ to Entity] che ci consentiranno di interrogare i database. Per prima cosa, ci collegheremo al database SQL Server che abbiamo creato e popolato:
![]() |
- in [1], aggiungiamo una connessione a un database;
- in [2], il metodo di accesso all'origine dati. Per accedere al database SQL Server, useremo il [LINQPad Driver];
- in [3], è anche possibile recuperare un contesto di persistenza [DbContext] definito in un assembly .exe o .dll (opzione 3). Purtroppo, ad oggi (8 ottobre 2012), Entity Framework 5 non è supportato;
- in [4], è possibile scaricare i driver per DBMS diversi da SQL Server;
- In 5, scaricheremo il driver per i DBMS MySQL e Oracle;
![]() |
- in [6], il driver scaricato;
- in [7], ci connettiamo a un database SQL Server;
![]() |
- in [8], il database si trova sul server locale;
- in [9], ci colleghiamo utilizzando le credenziali sa / sqlserver2012;
- in [10], al database [rdvmedecins-ef] che abbiamo creato;
- In [11], è possibile verificare la connessione;
- In [12], completare la procedura guidata;
- In [13], la connessione appare in LINQPad.
Le entità sono state create dalla tabella [rdvmedecins-ef]. Sono le seguenti:
![]() |
- In [1], [CLIENTS] rappresenta l'insieme delle entità [Client]. Ogni entità ha:
- le proprietà (ID, TITOLO, COGNOME, NOME, TIMESTAMP),
- una relazione uno-a-molti [CLIENTRVS];
- In [2], [CRENEAUXes] rappresenta l'insieme delle entità [Creneau]. Ogni entità ha:
- le proprietà (ID, START_TIME, MIN_TIME, END_TIME, MAX_TIME, DOCTOR_ID, TIMESTAMP),
- una relazione uno-a-molti [CRENEAURVS],
- una relazione molti-a-uno [DOCTOR];
- in [3], l'entità [MEDECINS] rappresenta l'insieme delle entità [Medecin]. Ogni entità ha:
- le proprietà (ID, TITOLO, COGNOME, NOME, TIMESTAMP),
- una relazione uno-a-molti [DOCTOR-SLOTS];
- in [4], l'entità [RVS] rappresenta l'insieme delle entità [Rv]. Ogni entità ha:
- le proprietà (ID, DAY, CLIENT_ID, SLOT_ID, TIMESTAMP),
- una relazione molti-a-uno [CLIENT],
- una relazione molti-a-uno [SLOT].
Si noti che i nomi delle proprietà sopra riportate sono diversi da quelli che abbiamo usato finora. Non ha importanza. Vogliamo solo imparare i principi di base delle interrogazioni sui database.
Vediamo come possiamo eseguire una query su questo database di entità. Ad esempio, vogliamo un elenco di medici ordinato per TITOLO e COGNOME:
![]() |
- in [1], creiamo una nuova query;
- in [2], il testo della query;
![]() |
- in [3], il risultato della query;
- in [4], la stessa query utilizzando espressioni lambda. Una query con espressioni lambda è meno leggibile di una query testuale, e potresti preferire evitarle. Tuttavia, a volte sono indispensabili perché consentono alcune operazioni che le query testuali non consentono. Un'espressione lambda denota una funzione con un parametro di input a e un parametro di output b, nella forma a=>b. Il metodo OrderBy sopra riportato accetta una funzione lambda come unico parametro. Questo fornisce il parametro in base al quale una collezione deve essere ordinata. Pertanto, MEDECINS.OrderBy(m=>m.TITRE) è l'elenco dei medici ordinato in base ai loro titoli. L'istruzione deve essere letta come una pipeline su una collezione. La collezione di medici viene fornita come input al metodo OrderBy. Questo metodo elaborerà le entità [Doctor] una per una. Nell'espressione lambda m=>m.TITLE, m rappresenta l'input della funzione lambda. Può essere denominato a piacere. In questo caso, l'input della funzione lambda sarà un'entità [Doctor]. La funzione m=>m.TITLE si legge come segue: se chiamo m il mio input (un'entità [Doctor]), allora il mio output è m.TITLE, ovvero il titolo del medico. MEDECINS.OrderBy(m=>m.TITRE) è a sua volta una collezione, la collezione dei medici ordinati in base al loro titolo. Questa nuova collezione può essere utilizzata in un altro metodo, in questo esempio il metodo ThenBy. Questo metodo funziona secondo lo stesso principio. Viene utilizzato per specificare parametri aggiuntivi per l'ordinamento della collezione.
Leggere il codice lambda equivalente al codice testuale che digitiamo di solito è un buon modo per impararlo;
![]() |
- in 5, la query SQL inviata al database. Anche in questo caso, leggeremo attentamente questo codice. Ci permette di valutare il costo effettivo di una query LINQ.
Di seguito presentiamo alcuni esempi di query LINQ. Per ciascuna di esse, mostriamo i risultati visualizzati e il codice lambda e SQL equivalente. Per comprendere queste query, dobbiamo ricordare le relazioni molti-a-uno che collegano le entità tra loro. È attraverso queste che navighiamo da un'entità all'altra. Sono chiamate proprietà di navigazione.
![]() |
// Clienti il cui titolo è "Mr" ordinati in ordine decrescente per nome
Risultati:
![]() |
LINQ | |
Lambda | |
SQL | |
// tutte le fasce orarie con il medico associato
Risultati (parziali):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// tutti gli appuntamenti relativi al cliente e al medico in questione
Risultati:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// medici senza appuntamenti
Risultati:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
Non esiste una query LINQ per questa richiesta. È necessario utilizzare espressioni lambda. Questa si legge come segue: prendo la collezione di medici (DOCTORS) e mantengo (Where) solo quei medici (m) per i quali non riesco a trovare un appuntamento (rv) con quel medico (m) nella collezione degli appuntamenti (APPOINTMENTS).
// Fasce orarie della sig.ra Pélissier
Risultati (parziali):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// Numero di appuntamenti della sig.ra Pélissier l'8 ottobre 2012
Risultati:
![]() |
LINQ | |
Lambda | |
SQL | |
// Elenco dei clienti che hanno fissato un appuntamento con la sig.ra Pélissier l'8 ottobre 2012
Risultati:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// numero di fasce orarie per medico
Risultati:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
3.5.5. Modifica di un'entità associata al contesto di persistenza
Abbiamo trattato le seguenti operazioni sul contesto di persistenza:
- aggiunta di un elemento al contesto ([dbContext].[DbSet].Add);
- Rimuovere un elemento dal contesto ([dbContext].[DbSet].Remove);
- Eseguire una query sul contesto utilizzando le query LINQ.
Per sincronizzare il contesto con il database, scrivere [dbContext].SaveChanges().
![]() | ![]() |
Il codice [ModifyAttachedEntity] mostra come modificare un'entità associata al contesto:
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);
}
}
}
}
- riga 15: viene aperto il contesto dell'applicazione;
- righe 18–25: il contesto viene cancellato. Più precisamente, tutte le entità vengono caricate nel contesto dal database e quindi impostate allo stato "eliminato". Si noti che in questa fase il database non è cambiato. Finché il contesto non viene sincronizzato con il database, il database rimane invariato. Ricordiamo che l'eliminazione delle entità [Doctor] e [Client] è sufficiente a svuotare il database tramite eliminazioni a cascata;
- righe 27–28: un nuovo cliente viene aggiunto al database;
- righe 30-31: il cliente viene visualizzato prima di essere salvato nel database;
- riga 33: il contesto viene sincronizzato con il database. Le entità contrassegnate come "eliminate" saranno soggette a un'operazione SQL DELETE, mentre l'entità aggiunta a un'operazione SQL INSERT;
- righe 35-36: il cliente viene visualizzato dopo la sincronizzazione con il database;
Il risultato visualizzato nella console è il seguente:
Si notino i seguenti punti:
- Prima della sincronizzazione con il database, il client non possiede né una chiave primaria né un timestamp;
- dopo la sincronizzazione, li possiede. Ricordiamo qui che la chiave primaria era configurata per essere generata da SQL Server. Allo stesso modo, questo DBMS genera automaticamente il timestamp;
- riga 37: il contesto di persistenza viene chiuso. Le entità in esso contenute diventano "distaccate". Esse esistono come oggetti ma non come entità collegate a un contesto di persistenza;
- riga 39: viene avviato un nuovo contesto vuoto;
- riga 42: il client viene recuperato direttamente dal database tramite la sua chiave primaria. Viene quindi inserito nel contesto. Se non viene trovato, il metodo Find restituisce un puntatore nullo;
- righe 48–49: lo visualizziamo;
Questo produce il seguente risultato:
- Riga 47: lo modifichiamo;
- riga 49: sincronizziamo il contesto con il database. EF rileverà che alcuni elementi del contesto sono stati modificati da quando sono stati aggiunti. Per questi elementi, genererà istruzioni SQL UPDATE per il database. Quindi, in questo caso, la sincronizzazione consisterà in una singola istruzione UPDATE;
- riga 50: il secondo contesto viene chiuso. L'entità client2 che era collegata al contesto viene ora scollegata da esso;
- riga 52: viene aperto un terzo contesto vuoto;
- riga 55: il singolo client dal database viene nuovamente inserito al suo interno. Vogliamo verificare se la modifica apportata nel contesto precedente è stata riportata nel database;
- righe 57–58: il client viene visualizzato. Questo produce il seguente risultato:
Il nome del cliente è stato effettivamente aggiornato nel database. Si noti che anche il suo timestamp è stato aggiornato.
- riga 59: chiudiamo il contesto. Da notare, tra l'altro, che a differenza dei due casi precedenti, non è stato necessario sincronizzare preventivamente il contesto con il database (SaveChanges) poiché il contesto non era stato modificato.
3.5.6. Gestione delle entità distaccate
Torniamo all'architettura a livelli di un'applicazione come quella del caso di studio:
![]() |
Il livello [DAO] utilizza l'ORM EF5 per accedere ai dati. Abbiamo gli elementi costitutivi di base di questo livello. Ogni metodo aprirà un contesto di persistenza, eseguirà le operazioni necessarie (inserimento, aggiornamento, eliminazione, query) e poi lo chiuderà. Le entità gestite dal livello [DAO] saranno passate al livello web ASP.NET. In questo livello, esse si trovano al di fuori del contesto di persistenza e sono quindi distaccate. Nel livello web, un utente può modificare queste entità (aggiungere, aggiornare, eliminare). Quando tornano al livello [DAO], sono ancora distaccate. Tuttavia, il livello [DAO] dovrà riflettere le modifiche apportate dall'utente nel database. Dovrà quindi lavorare con entità distaccate. Esaminiamo i tre casi possibili:
Aggiunta di un'entità distaccata
Questa è la procedura standard per un'aggiunta. È sufficiente aggiungere (Add) l'entità distaccata al contesto, assicurandosi che la sua chiave primaria sia null.
Modifica di un'entità distaccata
È possibile utilizzare il codice seguente:
- Il metodo [DbContext].Entry(entità-scollegata) aggiungerà l'entità al contesto;
- lo stato di questa entità viene impostato su "modified" in modo che sia soggetta a un'istruzione SQL UPDATE.
Eliminazione di un'entità distaccata
È possibile utilizzare il codice seguente:
- Riga 1: Aggiungi al contesto l'entità con la stessa chiave primaria dell'entità scollegata;
- Riga 2: La eliminiamo:
Si noti che ciò richiede un SELECT seguito da un DELETE nel database, mentre normalmente è sufficiente un solo DELETE. È anche possibile seguire l'esempio della modifica di un'entità distaccata e scrivere:
Poiché non sono riuscito a implementare la registrazione delle operazioni SQL eseguite sul database, non so se un metodo sia preferibile all'altro.
Ecco un esempio:
![]() | ![]() |
Il codice del programma [ModifyDetachedEntities] è il seguente:
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);
}
}
}
}
}
- riga 15: il database viene svuotato;
- righe 17–25: viene aggiunto un cliente al database;
- riga 27: visualizza il contenuto del database;
- Dopo la riga 25, il contesto di persistenza non esiste più. Pertanto, non ci sono più entità associate. L'entità client1 è passata allo stato "distaccato";
- riga 29: il nome dell'entità distaccata viene modificato;
- riga 31: viene aperto un nuovo contesto vuoto;
- riga 35: l'entità distaccata client1 viene inserita nel contesto in uno stato "modificato";
- riga 37: il contesto viene sincronizzato con il database;
- riga 38: viene chiuso;
- riga 40: viene visualizzato il database;
Il nome del cliente è stato aggiornato correttamente nel database. Si noti che il timestamp è stato aggiornato;
- riga 42: apertura di un nuovo contesto vuoto;
- riga 46: l'entità distaccata client1 viene inserita nel contesto in stato "eliminato";
- riga 48: il contesto viene sincronizzato con il database;
- riga 49: viene chiuso;
- riga 51: viene visualizzato il database;
L'entità è stata effettivamente eliminata dal database.
Ora esamineremo le due modalità di caricamento delle dipendenze di un'entità: il caricamento pigro (Lazy Loading) e il caricamento anticipato (Eager Loading).
3.5.7. Caricamento differito e immediato
Rivediamo lo schema di dipendenza molti-a-uno delle nostre quattro entità:
![]() |
Sopra, l'entità [Creneau] ha una proprietà di navigazione [Creneau.Medecin] che punta all'entità [Medecin]. Questa è chiamata dipendenza. Abbiamo visto che esistono anche dipendenze uno-a-molti. Il principio spiegato qui si applica anche a queste.
Per impostazione predefinita, EF 5 è in modalità Lazy Loading: quando importa un'entità nel contesto di persistenza dal database, non ne importa le dipendenze. Queste verranno caricate al momento del loro primo utilizzo. Si tratta di una misura dettata dal buon senso. Se così non fosse, importare gli appuntamenti nel contesto comporterebbe, in base alle dipendenze sopra indicate, che:
- le entità [Time Slot] collegate agli appuntamenti;
- le entità [Doctor] collegate a tali slot;
- le entità [Clients] collegate agli appuntamenti.
A volte, tuttavia, abbiamo bisogno di un'entità e delle sue dipendenze. Illustreremo entrambe le modalità di caricamento.
![]() | ![]() |
Il codice per [LazyEagerLoading] è il seguente:
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();
}
}
}
}
- riga 18: partiamo da una base nota, quella utilizzata finora. Dopo questa operazione, gli array nelle righe 11–13 vengono riempiti con entità separate;
- righe 21–22: ci concentriamo sulla prima fascia oraria e sul medico associato;
- riga 23: nuovo contesto;
- riga 26: inseriamo lo slot temporale nel contesto insieme alla sua dipendenza (eager loading). Poiché questa non è la modalità predefinita, dobbiamo richiedere esplicitamente questa dipendenza. Il metodo Include ci permette di farlo. Il suo parametro è il nome della dipendenza all'interno dell'entità inserita nel contesto. La query che inserisce l'entità nel contesto utilizza espressioni lambda. Il metodo Single consente di specificare una condizione per recuperare una singola entità. Qui, cerchiamo nel database l'entità [Creneau] che ha la chiave primaria dello slot n. 0;
- riga 27: visualizziamo l'entità recuperata. Esaminiamo i due metodi di scrittura utilizzati nelle entità:
// 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));
}
- righe 2-5: il metodo [ToString] visualizza la dipendenza [Doctor]. Se questa non è già presente nel contesto, verrà cercata nel database per aggiungerla;
- righe 8-11: il metodo [ShortIdentity] non visualizza la dipendenza [Doctor]. Pertanto, se non è presente nel contesto, non verrà cercata nel database;
A questo punto, l'output della console è il seguente:
- Riga 28: il contesto è chiuso;
- Righe 30–37: tentiamo di scrivere la dipendenza [Doctor] dell'entità. Ricordiamo come funziona il Lazy Loading: una dipendenza viene caricata al suo primo utilizzo se non è presente. In questo caso, è normalmente presente. L'output è il seguente:
- righe 39–44: in un nuovo contesto, lo slot n. 0 viene nuovamente cercato nel database e inserito nel contesto. Qui, la dipendenza [Doctor] non è esplicitamente richiesta. Non verrà quindi inserita (Lazy Loading);
- riga 43: l'identità breve dello slot viene visualizzata come segue:
In questo caso, è importante utilizzare ShortIdentity anziché ToString per visualizzare l'entità. Se si utilizza ToString, verrà visualizzata la dipendenza [Doctor] e, per farlo, verrà effettuata una ricerca nel database. Tuttavia, non è ciò che vogliamo.
- Riga 44: il contesto è chiuso;
- righe 46–53: tentiamo di visualizzare la dipendenza dell'entità. È importante farlo fuori dal contesto; altrimenti, verrà cercata nel database e trovata. Qui siamo fuori dal contesto. L'entità [Creneau] è distaccata e la sua dipendenza [Medecin] è mancante (Lazy Loading). Cosa succederà? La visualizzazione sullo schermo è la seguente:
EF ha rilevato che mancava la dipendenza [Medecin]. Ha tentato di caricarla, ma poiché il contesto era chiuso, questa operazione non era più possibile. Prenderemo nota di questa eccezione [System.ObjectDisposedException] poiché è caratteristica del caricamento di una dipendenza al di fuori di un contesto aperto.
Esaminiamo ora l'accesso concorrente alle entità.
3.5.8. Concorrenza nell'accesso alle entità
Rivediamo la definizione dell'entità [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
...
}
Ci concentreremo sul campo [Timestamp] nella riga 23. Sappiamo che il suo valore è generato dal DBMS. Abbiamo anche notato che l'annotazione [Timestamp] nella riga 22 induce EF 5 a utilizzare il campo annotato per gestire la concorrenza nell'accesso alle entità. Ricordiamo cos'è la gestione della concorrenza:
- un processo P1 legge una riga L dalla tabella [DOCTORS] al tempo T1. La riga ha il timestamp TS1;
- un processo P2 legge la stessa riga L dalla tabella [DOCTORS] al tempo T2. La riga ha il timestamp TS1 perché il processo P1 non ha ancora confermato la sua modifica;
- il processo P1 conferma la sua modifica alla riga L. Il timestamp della riga L cambia quindi in TS2;
- Il processo P2 conferma la sua modifica alla riga L. L'ORM genera quindi un'eccezione perché il processo P2 ha un timestamp TS1 per la riga L che differisce dal timestamp TS2 presente nel database.
Questo si chiama gestione ottimistica della concorrenza. Con EF 5, un campo che svolge questo ruolo deve avere uno dei due attributi: [Timestamp] o [ConcurrencyCheck]. SQL Server ha un tipo [timestamp]. Il valore di una colonna di questo tipo viene generato automaticamente da SQL Server al momento dell'inserimento o della modifica di una riga. Una colonna di questo tipo può quindi essere utilizzata per gestire la concorrenza.
Illustreremo questo accesso concorrente con due thread che modificheranno simultaneamente la stessa entità [Client] nel database. Il progetto si evolve come segue:
![]() | ![]() |
Il codice del programma [ConcurrentAccess] è il seguente:
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)
{
...
}
- riga 26: iniziamo un contesto vuoto;
- riga 29: assegniamo un nome al thread corrente per distinguerlo dai due thread che verranno creati in seguito;
- righe 31–38: le entità [Doctor] e [Client] vengono impostate allo stato "deleted";
- righe 40–41: un client viene aggiunto al contesto;
- righe 43–44: visualizzarlo prima della sincronizzazione del contesto;
- riga 46: sincronizzazione del contesto con il database: le entità nello stato "deleted" saranno rimosse dal database. L'entità [Client] inserita nel contesto sarà inserita nel database. Sarà l'unico elemento nel database;
- righe 47-49: il cliente viene visualizzato dopo la sincronizzazione del contesto. A questo punto, le schermate sono le seguenti:
Si noti che dopo la sincronizzazione del contesto, il cliente ha una chiave primaria e un timestamp;
- riga 50: il contesto viene chiuso;
- riga 53: un thread t1 è associato al metodo [Modify] alla riga 84. Ciò significa che quando verrà lanciato, eseguirà il metodo [Modify];
- riga 54: al thread t1 viene assegnato un nome;
- riga 55: il thread t1 viene avviato. I parametri gli vengono passati sotto forma di una struttura [Data] definita alle righe 12–17:
- Durata: il thread si fermerà X secondi prima di completare la sua esecuzione,
- Client: un riferimento al client da aggiornare nel database,
- Name: il nome da assegnare a questo cliente;
- righe 57–59: stessa procedura con un secondo thread. In definitiva, due thread tenteranno di modificare il nome dello stesso cliente nel database;
- righe 60–63: dopo aver avviato i due thread, il thread principale attende che terminino l'esecuzione;
- riga 62: in attesa che il thread t1 finisca;
- riga 63: attesa del completamento del thread t2;
- riga 64: non sappiamo in quale ordine i due thread finiranno. Quello che è certo è che alla riga 64 hanno finito;
- righe 66-72: in un nuovo contesto, cerchiamo il client nel database per verificarne lo stato.
Ora vediamo cosa fanno i due thread t1 e t2. Essi eseguono il seguente metodo [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);
}
- riga 4: recupera i parametri del thread (Durata, Nome, Cliente);
- riga 7: nuovo contesto;
- riga 11: il client viene inserito nel contesto;
- righe 12–13: monitoraggio per verificare lo stato del client;
- riga 15: modifica del nome;
- Riga 17: il thread si mette in pausa per un numero di millisecondi. Questo ha un effetto interessante. Il thread libera il processore che lo stava eseguendo, lasciando spazio a un altro thread. Nel nostro esempio, abbiamo tre thread: main, t1 e t2. Il thread principale è in pausa, in attesa che i thread t1 e t2 terminino. Supponendo che il thread t1 abbia il processore per primo, ora lo cede al thread t2. Ciò farà sì che il thread t2 legga esattamente gli stessi dati del thread t1: lo stesso client con lo stesso timestamp;
- Riga 19: il contesto viene sincronizzato con il database. Supponiamo di nuovo che il thread t1 si riattivi per primo. Salverà il client con il nome "yy". Potrà farlo perché ha lo stesso timestamp presente nel database. A causa di questo aggiornamento, il DBMS modificherà il timestamp. Quando il thread t2 si riattiverà a sua volta, avrà un client con un timestamp diverso da quello ora presente nel database. Il suo aggiornamento verrà rifiutato.
Le schermate visualizzate sono le seguenti:
- riga 4: il client nel database;
- riga 9: il client come letto dal thread t2;
- riga 11: il client letto dal thread t1. Entrambi i thread hanno quindi letto la stessa cosa;
- riga 12: il thread t2 termina per primo. È stato quindi in grado di eseguire il proprio aggiornamento. Il nome deve essere cambiato in "zz";
- riga 13: il thread t1 genera un'eccezione [System.Data.OptimisticConcurrencyException]. EF ha rilevato che non disponeva del timestamp corretto;
- riga 21: il thread t1 termina a sua volta;
- riga 22: il thread principale ha terminato l'attesa;
- riga 24: il thread principale visualizza il client nel database. È infatti il thread t2 ad aver vinto. Il nome è "zz". Si noti che il timestamp è cambiato.
Ora, esaminiamo un altro aspetto: la transazione che governa la sincronizzazione del contesto di persistenza con il database.
3.5.9. Sincronizzazione all'interno di una transazione
La tabella [CRENEAUX] ha un vincolo di unicità che abbiamo aggiunto manualmente (vedere la sezione 2.2.4, pagina 12):
Procederemo come segue: aggiungeremo due appuntamenti contemporaneamente per lo stesso medico, nello stesso giorno e nella stessa fascia oraria. Vediamo cosa succede.
Il progetto si evolve come segue:
![]() | ![]() |
Il codice del programma [SynchronisationTransaction] è il seguente:
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);
}
}
}
}
}
- righe 15–27: viene utilizzato un contesto di persistenza per svuotare il database;
- riga 30: creazione di un oggetto [Client];
- riga 32: creazione di un oggetto [Doctor];
- riga 34: creazione di un oggetto [Slot];
- riga 36: creazione di un oggetto [Appointment];
- riga 37: creazione di un secondo oggetto [Appointment] identico al precedente;
- riga 41: apertura di un nuovo contesto;
- righe 43–47: gli oggetti creati in precedenza vengono associati al nuovo contesto. Si noti che, tenendo conto delle dipendenze, avremmo potuto ridurre al minimo il numero di operazioni Add. Tuttavia, EF ottimizzerà le istruzioni SQL INSERT da inviare al database;
- riga 51: il contesto è sincronizzato con il database. Come indica il commento, l'inserimento di uno dei due appuntamenti deve fallire a causa del vincolo di unicità sulla tabella [RVS]. Ma oltre a ciò, se la sincronizzazione avviene all'interno di una transazione, tutto deve essere annullato. Pertanto, non dovrebbe avvenire alcun inserimento. Il database deve rimanere vuoto;
- riga 53: il contesto viene chiuso;
- righe 61–90: visualizzazione del contenuto del database. Deve essere vuoto.
La visualizzazione sullo schermo è la seguente:
- riga 1: eccezione dovuta a una violazione del vincolo di unicità sulla tabella [RVS];
- righe 9–12: il database è effettivamente vuoto. La sincronizzazione del contesto con il database è quindi avvenuta all'interno di una transazione.
Ci sono indubbiamente altri aspetti da esplorare in EF 5. Ma ne sappiamo abbastanza per tornare al nostro studio di un'architettura multistrato. All'inizio di questo documento, il lettore troverà riferimenti ad articoli e libri che gli consentiranno di approfondire la propria conoscenza di EF 5.
3.6. Studio di un'architettura multistrato basata su EF 5
Torniamo al nostro caso di studio descritto nella Sezione 2. Si tratta di un'applicazione web ASP.NET strutturata come segue:
![]() |
Inizieremo con la creazione del livello di accesso ai dati [DAO]. Questo livello sarà basato su EF5.
3.6.1. Il nuovo progetto
Creiamo un nuovo progetto console VS 2012 [RdvMedecins-SqlServer-02] nella soluzione corrente [1]:
![]() |
Aggiungiamo quattro cartelle [2], nelle quali organizzeremo il nostro codice. La cartella [Entities] è una copia della cartella [Entities] del progetto precedente. Dopo la copia, compaiono degli errori perché non disponiamo dei riferimenti corretti. Dobbiamo aggiungere un riferimento a Entity Framework 5. Per farlo, seguiremo il metodo spiegato nella sezione 3.4, pagina 21. L'elenco dei riferimenti diventa il seguente [3]:
![]() |
A questo punto, il progetto non dovrebbe più presentare errori di compilazione. Dal progetto precedente, copiamo anche il file [App.config], che configura la connessione al database:
<?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. La classe di eccezione
Utilizzeremo una classe di eccezione specifica del progetto. Questa è quella che verrà generata dal livello [DAO]:
![]() |
Il livello [DAO] intercetterà tutte le eccezioni che gli vengono propagate e le incapsulerà in un'eccezione di tipo [RdvMedecinsException]. Tale eccezione sarà la seguente:
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);
}
}
}
}
- Riga 5: la classe estende la classe [Exception];
- riga 9: aggiunge un codice di errore alla sua classe base;
- righe 12–32: i vari costruttori incorporano il campo [Code].
Il progetto si evolve come segue:
![]() |
3.6.3. Il livello [DAO]
![]() |
Il livello [DAO] fornisce un'interfaccia al livello [ASP.NET]. Per identificarlo, osserva le pagine Web dell'applicazione:
![]() |
- nel punto [1] sopra, l'elenco a discesa è stato popolato con l'elenco dei medici. Il livello [DAO] fornirà questo elenco;
- in [2], il livello [DAO] fornirà;
- l'elenco degli appuntamenti di un medico per un determinato giorno,
- un elenco delle fasce orarie disponibili di un medico,
- ulteriori informazioni sul medico selezionato;
![]() |
- in [3], l'elenco a discesa dei clienti sarà fornito dal livello [DAO];
![]() |
- in [4], l'utente conferma un appuntamento. Il livello [DAO] deve essere in grado di aggiungerlo al database. Deve inoltre essere in grado di fornire ulteriori informazioni sul cliente selezionato;
![]() |
- in 5, l'utente cancella un appuntamento. Il livello [DAO] deve consentirlo.
Con queste informazioni, l'interfaccia [IDao] del livello [DAO] potrebbe essere la seguente:
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;
}
}
I metodi nelle righe 10–20 derivano dall'analisi appena eseguita. Il metodo nella riga 22 serve a gestire il fatto che stiamo lavorando con il caricamento differito. Se, nel livello [ASP.NET], abbiamo bisogno di una dipendenza da un'entità, la recupereremo dal database utilizzando questo metodo.
L'implementazione [Dao] di questa interfaccia sarà la seguente:
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){
...
}
}
}
Spieghiamo il metodo [GetAllClients], che dovrebbe restituire un elenco di tutti i clienti:
- righe 18–31: la ricerca dei client viene eseguita all'interno di un blocco try/catch. Lo stesso vale per tutti i metodi successivi;
- riga 21: apertura di un nuovo contesto;
- riga 24: le entità [Client] vengono caricate nel contesto e inserite in un elenco.
Il metodo [GetAllMedecins], che restituisce un elenco di tutti i medici, è simile (righe 37–57).
Il metodo [GetCreneauxMedecin] è il seguente:
// 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);
}
}
- riga 9: apertura di un nuovo contesto di persistenza;
- riga 11: ricerca del medico di cui è nota la chiave primaria. Richiesta di includere la dipendenza [Creneaux], ovvero una raccolta delle fasce orarie del medico. Se il medico non esiste, il metodo Single genera un'eccezione;
- riga 13: restituisce l'elenco delle fasce orarie.
Il metodo [GetRvMedecinJour] deve restituire l'elenco degli appuntamenti di un medico per un dato giorno. Il suo codice potrebbe essere il seguente:
// 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;
}
- riga 13: recuperiamo il medico con la chiave primaria specificata;
- righe 14–17: se non esiste, genera un'eccezione;
- riga 19: la query LINQ per recuperare gli appuntamenti di questo medico;
Il metodo [AddAppointment] deve aggiungere un appuntamento al database e restituire la chiave primaria dell'elemento inserito. Il suo codice potrebbe essere il seguente:
// 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;
}
- riga 12: ricerca della fascia oraria dell'appuntamento nel database;
- righe 13–16: se non viene trovato, viene generata un'eccezione;
- riga 18: ricerca del cliente dell'appuntamento nel database;
- righe 19–22: se non viene trovato, genera un'eccezione;
- riga 24: crea un oggetto [Rv] con le informazioni necessarie;
- riga 26: aggiungilo al contesto di persistenza;
- riga 28: sincronizziamo il contesto di persistenza con il database. L'appuntamento verrà quindi salvato nel database;
- riga 30: sappiamo che dopo la sincronizzazione del database, le chiavi primarie degli elementi inseriti sono disponibili. Recuperiamo quella relativa all'appuntamento aggiunto;
- riga 31: chiudiamo il contesto di persistenza.
Il metodo [DeleteAppointment] deve eliminare un appuntamento per il quale gli viene passata la chiave primaria.
// 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);
}
}
- riga 7: nuovo contesto di persistenza;
- riga 10: l'appuntamento da eliminare viene passato al contesto;
- righe 11–15: se non esiste, viene generata un'eccezione;
- riga 16: rimuoverlo dal contesto;
- riga 18: sincronizza il contesto con il database;
- riga 19: chiudere il contesto.
Il metodo [Find<T>] consente di cercare nel database un'entità di tipo T utilizzando la sua chiave primaria. Il codice potrebbe essere il seguente:
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);
}
}
- Riga 8: Il metodo Set<T> consente di recuperare un DbSet<T> al quale è possibile applicare i metodi usuali.
Il progetto si evolve come segue:
![]() |
3.6.4. Test del livello [DAO]
Creeremo un programma di test per il livello [DAO]. L'architettura di test sarà la seguente:
![]() |
Un programma da console chiede a [Spring.net] di istanziare il livello [DAO]. Una volta fatto ciò, verifica le varie funzionalità dell'interfaccia del livello [DAO]. Anziché un programma da console, sarebbe stato preferibile scrivere un programma di test in stile NUnit. Un programma di test per il livello [DAO] potrebbe apparire così:
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)
{
...
}
}
}
- riga 14: il riferimento al livello [DAO]. Per rendere il test indipendente dall'effettiva implementazione del livello [DAO], questo riferimento è di tipo [IDao] (l'interfaccia) anziché di tipo [Dao] (la classe);
- riga 18: il livello [DAO] viene istanziato da Spring. Torneremo sulla configurazione necessaria per renderlo possibile. Convertiamo il riferimento all'oggetto restituito da Spring in un riferimento di tipo interfaccia [IDao];
- Righe 21–22: visualizza i clienti;
- righe 25–26: visualizzano i medici;
- righe 29-30: visualizzano l'elenco delle fasce orarie per il medico n. 0;
- riga 33: visualizza gli appuntamenti del medico n. 0 per il 23 novembre 2013. Non dovrebbero essercene;
- riga 37: aggiunge un appuntamento per il medico n. 0 il 23/11/2013;
- riga 39: visualizza gli appuntamenti del medico n. 0 del 23/11/2013. Dovrebbe essercene uno;
- riga 46: lo stesso appuntamento viene aggiunto una seconda volta. Dovrebbe verificarsi un'eccezione;
- Riga 57: Elimina l'unico appuntamento che è stato aggiunto;
- Riga 58: visualizza gli appuntamenti per il medico n. 0 del 23/11/2013. Non dovrebbe essercene nessuno.
3.6.5. Configurazione di Spring.net
Nel programma di test sopra riportato, abbiamo accennato brevemente all'istruzione che istanzia il livello [DAO]:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
La classe [ContextRegistry] è una classe Spring nel namespace [Spring.Context.Support]. Per utilizzare Spring, dobbiamo aggiungere la sua DLL ai riferimenti del progetto. Procediamo come segue:
![]() |
- in [1], cercare i pacchetti utilizzando lo strumento [NuGet];
![]() |
- in [2], cerca i pacchetti online;
- in [3], inserisci la parola chiave "spring" nella casella di ricerca;
- in [4], vengono visualizzati i pacchetti la cui descrizione contiene questa parola chiave. Qui, [Spring.Core] è quello che ci serve. Lo installiamo.
I riferimenti del progetto cambiano come segue:
![]() |
Il pacchetto [Spring.Core] dipendeva dal pacchetto [Common.Logging]. Anche questo è stato caricato. A questo punto, il progetto non dovrebbe più presentare errori.
Ciò non significa però che funzionerà. Dobbiamo prima configurare Spring nel file [App.config]. Questa è la parte più complessa del progetto. Il nuovo file [App.config] è il seguente:
<?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>
Iniziamo rimuovendo tutto ciò che è già noto: Entity Framework, stringhe di connessione, ProviderFactory. Il file si evolve come segue:
<?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>
- righe 3–15: definiscono le sezioni di configurazione;
- riga 8: definisce la classe che gestirà la sezione <spring><context> del file XML (righe 19–21);
- riga 9: definisce la classe che gestirà la sezione <spring><objects> del file XML (righe 22–24);
- riga 13: definisce la classe che gestirà la sezione <common><logging> del file XML (righe 27–36);
- Righe 7–14: sono stabili. Non è necessario modificarle in un altro progetto;
- righe 18–25: configurazione Spring. È stabile tranne che per le righe 22–24, che definiscono gli oggetti che Spring istanzierà;
- riga 23: definizione di un oggetto. L'attributo id è arbitrario. Si tratta dell'identificatore dell'oggetto. L'attributo type specifica la classe da istanziare nel formato «nome completo della classe, assembly contenente la classe». La classe in questione è quella che implementa il livello [DAO]: [RdvMedecins.Dao.Dao]. Per individuare il relativo assembly, controllare le proprietà del progetto:
![]() |
In [1], il nome dell'assembly da fornire;
- righe 27–36: la configurazione "Common Logging" è stabile. Potrebbe essere necessario modificare il livello di registrazione alla riga 32. Dopo la fase di debug, è possibile impostare il livello su INFO.
In definitiva, sebbene a prima vista possa sembrare complesso, il file di configurazione Spring risulta essere semplice. Le uniche modifiche necessarie sono:
- righe 22–24, che definiscono gli oggetti da istanziare;
- riga 32: il livello di logging.
Nel programma di test, l'istruzione che istanzia il livello [DAO] è la seguente:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
[ContextRegistry] è una classe Spring che utilizza la configurazione Spring specificata in un file [Web.config] o [App.config]. In questo caso, utilizzerà la seguente sezione del file [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() utilizza il contesto definito nelle righe 2–4. La riga 3 indica che gli oggetti Spring sono definiti nella sezione [spring/objects] del file di configurazione. Questa sezione è costituita dalle righe 5–7;
- ContextRegistry.GetContext().GetObject("rdvmedecinsDao") utilizza la sezione nelle righe 5-7. Restituisce un riferimento all'oggetto con l'attributo id="rdvmedecinsDao". Questo è l'oggetto definito nella riga 6. Spring istanzierà quindi la classe definita dall'attributo type utilizzando il suo costruttore senza parametri. Questo costruttore deve quindi esistere. Una volta fatto ciò, il riferimento all'oggetto creato viene restituito al codice chiamante. Se l'oggetto viene richiesto una seconda volta nel codice, Spring restituisce semplicemente un riferimento al primo oggetto creato. Questo è il modello di progettazione noto come singleton.
La costruzione di oggetti può essere più complessa. È possibile utilizzare un costruttore con parametri o specificare l'inizializzazione di determinati campi dell'oggetto una volta che l'oggetto è stato creato. Per ulteriori informazioni su questo argomento, consultare l'articolo "Spring IOC Tutorial for .NET" all'indirizzo [http://tahe.developpez.com/dotnet/springioc/].
Una volta fatto questo, possiamo eseguire l'applicazione. I risultati sullo schermo sono i seguenti:
I risultati sono quelli previsti. Ora considereremo valido il nostro livello [DAO]. Il tutorial potrebbe terminare qui. Finora abbiamo trattato:
- le nozioni di base dell'ORM Entity Framework 5;
- un livello [DAO] che utilizza questo ORM.
Ricordiamo il nostro caso di studio descritto all'inizio di questo documento. Partiamo da un'applicazione esistente con la seguente architettura:
![]() |
che vogliamo trasformare in questa:
![]() |
dove EF5 ha sostituito NHibernate. Abbiamo appena creato il livello [DAO2]. In realtà, non ha la stessa interfaccia del livello [DAO1], la cui interfaccia era più limitata:
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);
}
Il livello [DAO2] ha aggiunto il seguente metodo a questa interfaccia:
// find a T entity via its primary key
T Find<T>(int id) where T : class;
Questo metodo è stato aggiunto perché l'ORM EF 5 opera in modalità Lazy Loading per impostazione predefinita. Le entità arrivano nel livello [ASP.NET] senza le loro dipendenze. Il metodo sopra riportato ci permette di recuperarle se necessario e, in alcuni casi, ne abbiamo effettivamente bisogno. Anche NHibernate opera in modalità Lazy Loading per impostazione predefinita, ma io l'avevo utilizzato in modalità Eager Loading. Le entità arrivavano nel livello [ASP.NET] con le loro dipendenze.
Completeremo il porting dell'applicazione ASP.NET/NHibernate all'applicazione ASP.NET/EF 5. Tuttavia, poiché questo non riguarda più EF5, non commenteremo il codice web. Spiegheremo semplicemente come configurare l'applicazione web e testarla. È disponibile sul sito web di questo tutorial.
3.6.6. Generazione della DLL del livello [DAO]
Nella seguente architettura:
![]() |
il livello [ASP.NET] avrà a disposizione i livelli alla sua destra sotto forma di DLL. Creeremo quindi la DLL del livello [DAO].
![]() |
- In [1], selezionare il programma di test e in [2], non includerlo nella DLL che verrà generata;
- In [3], nelle proprietà del progetto, specificare che l'assembly da creare è una DLL;
- In [4], nel menu di VS, specifichiamo che genereremo un assembly [Release], che contiene meno informazioni rispetto a un assembly [Debug];
![]() |
- In 5, rigenerare l'assembly del progetto. Verrà generata la DLL;
- In [6], visualizzare tutti i file di progetto;
![]() |
- In [7], la DLL per il progetto del livello [DAO]. Questa è quella che verrà utilizzata dal progetto web ASP.NET;
- In [8], aggiorniamo la vista del progetto;
![]() |
- in [9], le DLL dalla cartella [Release] vengono raccolte in una cartella esterna [lib] [10]. È da qui che il progetto web recupererà i propri riferimenti.
3.6.7. Il livello [ASP.NET]
Qui spiegheremo come effettuare il porting dell'applicazione [ASP.NET / NHibernate] all'applicazione [ASP.NET / EF 5]. Lavoreremo con Visual Studio Express 2012 for Web, disponibile gratuitamente all'indirizzo [http://www.microsoft.com/visualstudio/fra/downloads].
Inizieremo con il progetto web esistente creato con VS 2010.
![]() |
- In [1], apriamo il progetto esistente:
- In [2], il progetto caricato presenta i seguenti riferimenti [3]:
- [NHibernate] è la DLL del framework NHibernate,
- [Spring.Core] è la DLL del framework Spring.net,
- [log4net] è la DLL per il framework di loggaggio log4net. Questo framework è utilizzato da Spring.net,
- [MySql.Data] è il driver ADO.NET per il DBMS MySQL,
- [rdvmedecins] è la DLL per il livello [DAO] realizzato con NHibernate;
- in [4], modifichiamo il nome del progetto e in 5 rimuoviamo i riferimenti precedenti;
![]() |
- in [6], aggiungiamo i riferimenti al progetto;
- in [7], nella procedura guidata, utilizziamo l'opzione [Sfoglia];
![]() |
- in [8], selezioniamo tutte le DLL del progetto n. 2 precedentemente inserite nella cartella [lib];
- in [9], un riepilogo che confermiamo;
- in [10], il progetto web con i suoi nuovi riferimenti.
Una volta fatto ciò, il progetto si presenta come segue:
![]() |
- In [1], il codice per la gestione delle pagine web è suddiviso tra i due file [Global.asax] e [Default.aspx]. Il codice di utilità è stato inserito nella cartella [Entities]. Infine, l'applicazione è configurata dal file [Web.config];
- in [2], generiamo l'assembly del progetto;
- in [3], compaiono degli errori.
Esaminiamo gli errori, ad esempio il seguente:
![]()
e la relativa spiegazione:
![]()
Il tipo di [medecin.Id] è int?, mentre il metodo [GetCreneauxMedecin] è di tipo int. Pertanto, è necessario un cast. Questo errore si verifica ripetutamente in tutto il codice perché le entità nel progetto ASP.NET/NHibernate avevano chiavi primarie di tipo int, mentre quelle nel progetto ASP.NET/EF 5 sono di tipo int?. Correggiamo tutti gli errori di questo tipo e rigeneriamo il progetto. A questo punto non ci sono più errori.
C'è un altro dettaglio da affrontare prima di eseguire il progetto: l'istanziazione del livello [DAO] da parte del framework Spring. Ciò avviene in [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)
{...
}
}
Nel programma di test del livello [DAO], il livello [DAO] è stato istanziato come segue:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
I due metodi sono identici. Ricordiamo che questa istanziazione del livello [DAO] si basava su una configurazione specificata in [App.config]. Sostituiamo quindi l'attuale contenuto di [Web.config] del progetto web con quello di [App.config] dal progetto del livello [DAO] per garantire la stessa configurazione.
Siamo pronti per la prima esecuzione. Viene visualizzata la home page [1]:
![]() |
- in [2], inseriamo una data per l'appuntamento e inviamo;
![]() |
- in [3], si verifica un errore.
Esaminando il messaggio di errore visualizzato dalla pagina, si nota che l'eccezione segnalata è correlata al Lazy Loading: si è tentato di caricare una dipendenza di un oggetto mentre il contesto di persistenza che lo gestiva era stato chiuso. L'oggetto si trova ora in uno stato "detached". Questo errore è dovuto al fatto che NHibernate è stato utilizzato in modalità Eager Loading, mentre EF 5 funziona in modalità Lazy Loading per impostazione predefinita. Nella riga evidenziata in rosso sopra:
- rdv rappresenta un oggetto [Rv] che è stato caricato senza le sue dipendenze;
- Per valutare rdv.Creneau.Id, l'applicazione tenta di caricare la dipendenza rdv.Creneau. Tuttavia, poiché non ci troviamo più nel contesto, ciò non è possibile, da qui l'eccezione.
In questo caso, la soluzione è semplice. Riga 108: creiamo una voce in un dizionario con la chiave primaria dello slot dell'appuntamento come chiave. Tuttavia, risulta che l'entità [Rv] incapsula la chiave primaria dello slot associato. Quindi scriviamo:
dicoRvPris[(int)rdv.CreneauId] = rdv;
Proviamo a eseguire nuovamente il codice. Questa volta, l'errore è il seguente:
![]() |
L'errore è simile. Riga 132: si tenta di caricare la dipendenza [Client] di un oggetto [Rv] nel livello ASP.NET, il che è fuori contesto. È necessario recuperare l'oggetto [Client] dal database. Per risolvere questo problema, l'interfaccia [IDao] è stata potenziata con il seguente metodo:
// find a T entity via its primary key
T Find<T>(int id) where T : class;
Questo ci consentirà di recuperare le dipendenze. Pertanto, la riga errata sopra riportata verrà riscritta come segue:
Client client = Global.Dao.Find<Client>(agenda.Creneaux[i].Rdv.ClientId);
Ancora una volta, notiamo il vantaggio delle entità che incorporano le loro chiavi esterne. Qui, l'entità [Rdv] ci dà accesso alla chiave esterna della dipendenza associata [Creneau]. Una volta apportate queste due correzioni, l'applicazione funziona. Il lettore è invitato a testare l'applicazione [RdvMedecins-SqlServer-03] disponibile nei download degli esempi sul sito web di questo articolo.
3.7. Conclusione
Abbiamo portato con successo un'applicazione ASP.NET / NHibernate:
![]() |
in un'applicazione ASP.NET / EF 5:
![]() |
Sebbene questa architettura avrebbe dovuto consentirci di mantenere intatto il livello [ASP.NET], abbiamo dovuto modificarlo per due motivi:
- le entità non erano esattamente le stesse. Il tipo di chiave primaria per le entità NHibernate era
int, mentre per EF 5 eraint?</span>**<span style="color: #000000">. Questo ci ha portato a introdurre cast nel codice web; - la modalità di caricamento delle entità non era la stessa per i due ORM: Eager Loading per NHibernate, Lazy Loading per EF 5. Ciò ci ha portato a potenziare l'interfaccia del livello [DAO] con un metodo generico che ci permette di recuperare un'entità tramite la sua chiave primaria.
Ciononostante, il porting si è rivelato piuttosto semplice, giustificando ancora una volta — se mai ce ne fosse stato bisogno — l'architettura a livelli e l'iniezione di dipendenze con Spring o un altro framework di iniezione di dipendenze.
Ora valuteremo l'impatto di un cambio di DBMS sull'architettura precedente. Porteremo tutti i progetti precedenti su altri quattro DBMS:
- Oracle Database Express Edition 11g Release 2;
- MySQL 5.5.28;
- PostgreSQL 9.2.1;
- Firebird 2.1.
Il codice rimarrà invariato. Cambieranno solo i seguenti elementi:
- la definizione nelle entità del campo utilizzato per controllare l'accesso simultaneo a un'entità;
- i file di configurazione [App.config] o [Web.config];
Commenteremo solo gli elementi che stanno cambiando.


















































































































































