Skip to content

3. Casestudy met SQL Server Express 2012

3.1. Introduction

De voorbeelden die op internet te vinden zijn voor Entity Framework zijn voor het overgrote deel voorbeelden met SQL Server. Dat is vrij normaal. Waarschijnlijk is dit de meest voorkomende SGBD in de bedrijfswereld. We zullen deze trend volgen. De voorbeelden zullen vervolgens worden uitgebreid naar alle databases die in paragraaf 1.2 worden genoemd.

3.2. Installatie van de tools

We zullen de installatie van de tools niet beschrijven. Dit zou namelijk een groot aantal schermafbeeldingen vereisen, die al snel verouderd raken. Dit is een taak (die, toegegeven, niet altijd even eenvoudig is) die we aan de lezer overlaten.

We moeten de volgende tools installeren:

  • de SGBD SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Download de versie „With Tools”, die samen met de SGBD een beheertool bevat:
 

Zodra SGBD is geïnstalleerd, starten we het programma:

  • [1]: start in het Startmenu de "SQL Server-configuratiemanager";
  • [2]: start in deze configuratiemanager de server;
  • [3]: de server is gestart.

We starten nu het beheertool van de SQL-server:

  • [1]: start in het Startmenu "SQL Server Management Studio";
  • [2]: het beheerprogramma.

We gaan verbinding maken met de server:

  • in [1] openen we de objectverkenner;
  • in [2] voeren we de verbindingsparameters in:
  • [3]: de (lokale) server (let op de vereiste haakjes) verwijst naar de server die op de computer is geïnstalleerd,
  • [4]: we kiezen voor Windows-authenticatie. Je moet beheerder van je computer zijn om deze verbinding tot stand te brengen,
  • [5]: je maakt verbinding;
  • [6]: je bent ingelogd;
  • [7]: we willen bepaalde eigenschappen van de server wijzigen;
  • [8]: er wordt gevraagd om twee authenticatiemethoden:
  • Windows-authenticatie, zoals zojuist is gebruikt. Een Windows-gebruiker met de juiste rechten kan dan inloggen,
  • SQL Server-authenticatie. De gebruiker moet deel uitmaken van de gebruikers die zijn geregistreerd in de SGBD;

Zodra dit is gebeurd, kunnen de eigenschappen van de server worden bevestigd;

  • [9]: we bewerken de eigenschappen van de gebruiker „sa” (systeembeheerder);
  • in [10] stelt u een wachtwoord voor hem in. In de rest van dit document is dit sqlserver2012;
  • in [10], geven we hem toestemming om in te loggen;
  • in [11] wordt de verbinding geactiveerd. Hiermee kan de wizard worden bevestigd;
  • in [12] wordt de verbinding met de server verbroken.

Nu maken we opnieuw verbinding met de login sa/sqlserver2012:

  • in [1] maken we opnieuw verbinding;
  • in [2], bij authenticatie SQL Server;
  • in [3] is de gebruiker sa;
  • in [4] is zijn wachtwoord sqlserver2012;
  • in [5] wordt er verbinding gemaakt;
  • in [6] is men ingelogd.

We gaan nu een demonstratiedatabase aanmaken:

  • in [1] maken we een nieuwe aan: BD;
  • in [2], deze krijgt de naam demo;
  • in [3], we bevestigen;
  • in [4] is de database aangemaakt;
  • in [5] wordt een nieuwe tabel aangemaakt in de database demo;
  • in [6] definiëren we een tabel met twee kolommen: ID en NOM;
  • in [7] wordt kolom [ID] ingesteld als primaire sleutel;
  • in [8] wordt de primaire sleutel weergegeven door een sleutel;
  • in [9] wordt de tabel opgeslagen;
  • in [10] wordt er een naam aan gegeven;
  • in [11] moet de database worden vernieuwd om de tabel in de database [demo] te laten verschijnen;
  • in [12] is de tabel [PERSONNES] inderdaad aangemaakt.

We weten nu genoeg over het gebruik van de beheertool van SQL Server.

3.3. De ingebouwde server (localdb)\v11.0

VS Express 2012 wordt geleverd met een ingebouwde SQL-server. Hier wordt ervan uitgegaan dat VS Express 2012 is geïnstalleerd [http://www.microsoft.com/visualstudio/fra/downloads]. We starten VS 2012 [1]:

We starten het beheerprogramma van SQL Server 2012 [2] en loggen in op [3].

  • in [4], logt u in op de server (localdb)\v11.0;
  • in [5], met Windows-authenticatie;
  • in [6] worden na een succesvolle verbinding de databases van de server weergegeven. Net als eerder zou men een nieuwe database kunnen aanmaken.

We zullen deze ingebouwde server in VS 2012 niet gebruiken.

3.4. Een database aanmaken op basis van entiteiten

Met Entity Framework 5 Code First kun je een database aanmaken op basis van entiteiten. Dat gaan we nu bekijken. Met VS Express 2012 maken we een eerste consoleproject in C# aan:

  • in [1], de projectdefinitie;
  • in [2], het aangemaakte project.

Al onze projecten hebben de DLL van Entity Framework 5 nodig. We voegen deze toe:

  • in [1] kun je met de tool NuGet afhankelijkheden downloaden;
  • naar [2], hiermee wordt de Entity Framework-afhankelijkheid gedownload;
  • in [3] is de verwijzing aan het project toegevoegd.

Je kunt meer informatie vinden door de eigenschappen van de toegevoegde verwijzing te bekijken:

  • in [1], de versie van DLL. Versie 5 is vereist;
  • in [2], de locatie in het bestandssysteem: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll waarbij <solution> de map is van de oplossing VS. Alle pakketten die door NuGet worden toegevoegd, komen in de map <solution>/packages terecht;
  • in [3] is een bestand [packages.config] aangemaakt. De inhoud ervan is als volgt:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>

Hierin staan de pakketten vermeld die door NuGet zijn geïmporteerd.

Laten we teruggaan naar het project VS en een map [Models] in het project aanmaken:

  • in [1], een map toevoegen aan het project;
  • in [2], deze krijgt de naam [Models].

We zullen deze gewoonte voortzetten en de definitie van onze entiteiten in de map [Models] plaatsen.

Om onze entiteiten op te bouwen, maken we gebruik van de definitie van de database MySQL 5 die in het project NHibernate wordt gebruikt. Laten we nog even de rol van de entiteiten EF in herinnering brengen:

De entiteiten moeten de tabellen in de database weerspiegelen. De gegevenslaag maakt gebruik van deze entiteiten in plaats van rechtstreeks met de tabellen te werken. Laten we beginnen met de tabel [MEDECINS]:

3.4.1. De entiteit [Medecin]

Deze bevat informatie over de artsen die worden beheerd door de applicatie [RdvMedecins].

  • ID: identificatienummer van de arts – primaire sleutel van de tabel
  • VERSION: identificatienummer van de versie van de rij in de tabel. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de achternaam van de arts
  • PRENOM: zijn of haar voornaam
  • TITRE: zijn/haar aanspreektitel (mevrouw, meisje, meneer)

We zouden kunnen uitgaan van de volgende klasse [Medecin]:


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; }
}
  • regel 3: de klasse [Medecin] is gekoppeld aan de tabel [MEDECINS] in de database. Deze bevindt zich in een schema met de naam "dbo".

We plaatsen deze klasse in een bestand met de naam [Entites.cs] [1]. Daar zullen we al onze entiteiten plaatsen.

Nog steeds in de map [Models] maken we het volgende bestand [Context.cs] aan:


using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // de context
  public class RdvMedecinsContext : DbContext
  {
    // de artsen
    public DbSet<Medecin> Medecins { get; set; }
  }

  // initialisatie van de database
  public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
  {
  }
}
  • regel 8: de klasse [RdvMedecinsContext] vertegenwoordigt de persistentiecontext, c.-à-d. alle entiteiten die door ORM worden beheerd. Deze moet afgeleid zijn van de klasse [System.Data.Entity.DbContext];
  • regel 11: het veld [Medecins] vertegenwoordigt de entiteiten van het type [Medecin] uit de persistentiecontext. Deze is van het type DbSet<Medecin>. Meestal zijn er evenveel [DbSet]-entiteiten als tabellen in de database, één per tabel;
  • regel 15: er wordt een klasse [RdvMedecinsInitializer] gedefinieerd om de aangemaakte database te initialiseren. Deze is hier afgeleid van de klasse [DropCreateDataBaseAlways], die, zoals de naam al aangeeft, de database verwijdert als deze al bestaat en deze vervolgens opnieuw aanmaakt. Dit is handig tijdens de ontwikkelingsfase van de BD. De parameter van de klasse [DropCreateDataBaseAlways] is het type persistentiecontext dat aan de database is gekoppeld. Er kunnen andere bovenliggende klassen dan [DropCreateDataBaseAlways] worden gebruikt voor de initialisatieklasse:
  • [DropCreateDatabaseIfModelChanges]: maakt de database opnieuw aan als de entiteiten zijn gewijzigd,
  • [CreateDatabaseIfNotExists]: maakt de database aan als deze niet bestaat;

Nu moeten we nog een hoofdprogramma maken. Dat wordt het volgende: [CreateDB_01.cs]:


using System;
using System.Data.Entity;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class CreateDB_01
  {
    static void Main(string[] args)
    {
      // de database wordt aangemaakt
      Database.SetInitializer(new RdvMedecinsInitializer());
      using (var context = new RdvMedecinsContext())
      {
        context.Database.Initialize(false);
      }
    }
  }
}
  • regel 12: [System.Data.Entity.DataBase] is een klasse die statische methoden biedt voor het beheer van de database die gekoppeld is aan een persistentiecontext. Met de statische methode [SetInitializer] kan de initialisatieklasse van de database worden gespecificeerd. Dit start de initialisatie niet;
  • regel 13: om met een persistentiecontext te werken, moet deze worden geïnstantieerd. Dat gebeurt hier. Er wordt een using-clausule gebruikt, zodat de context automatisch wordt gesloten bij het verlaten van de clausule. Dus op regel 17 wordt de context gesloten;
  • regel 15: we starten expliciet het genereren van de database die gekoppeld is aan de persistentiecontext [RdvMedecinsContext]. De parameter false geeft aan dat deze bewerking niet moet worden uitgevoerd als deze al voor deze context is uitgevoerd. Hier had men net zo goed true kunnen gebruiken.

Bij het werken met een database worden de verbindingsparameters doorgaans opgeslagen in het bestand [App.config]. We zien dat ze daar op dit moment nog niet in staan:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Ga voor meer informatie over de configuratie van Entity Framework naar 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>

De bovenstaande gegevens zijn in [App.config] opgeslagen toen we de Entity Framework-afhankelijkheid aan de projectverwijzingen hebben toegevoegd.

Laten we het project uitvoeren (Ctrl-F5) nadat we SQL Server Express hebben gestart (dit is belangrijk):

De uitvoering moet zonder fouten worden voltooid. Laten we nu de beheertool van SQL Server openen en de weergave vernieuwen:

We zien dat er een database is aangemaakt met de volledige naam van de klasse [RdvMedecinsContext] en dat deze een tabel [dbo.MEDECINS] (de naam die we eraan hadden gegeven) met kolommen die de namen van de velden van de entiteit [Medecin] overnemen. Als de code correct is uitgevoerd en de bovengenoemde database niet verschijnt, moet u kijken naar de ingebouwde server (localdb)\v11.0 (zie pagina 19). Bij VS 2012 Pro wordt deze server gebruikt als de SQL-server niet actief is op het moment dat de code wordt uitgevoerd. Bij VS 2012 Express is dat niet het geval.

Laten we de structuur van de tabel [MEDECINS] eens bekijken:

  • deze bevat de veldnamen van de entiteit [Medecin];
  • de kolom [Id] is de primaire sleutel. Dit is een conventie van EF: als entiteit E een veld Id of Eid (MedecinId) heeft, dan is deze kolom de primaire sleutel in de bijbehorende tabel;
  • de kolomtypen van de tabel zijn dezelfde als die van de velden van de entiteit;
  • voor de kolommen Titel, Achternaam en Voornaam is het type [nvarchar(max)] gebruikt. We zouden nauwkeuriger kunnen zijn: 5 tekens voor de titel, 30 voor de achternaam en de voornaam;
  • de kolommen Titel, Achternaam en Voornaam kunnen de waarde NULL hebben. Dit gaan we wijzigen.

Laten we eens kijken naar de eigenschappen van de primaire sleutel [Id]:

In [1] zien we dat de primaire sleutel van het type [Identité] is, wat betekent dat de waarde automatisch wordt gegenereerd door de SQL-server. We zullen deze strategie toepassen op alle SGBD-sleutels.

We zullen minder ruimte laten voor de conventies van EF door gebruik te maken van annotaties. De entiteitscode in [Entites.cs] wordt dan als volgt:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin
  {
    // gegevens
    [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; }
  }
}
  • regels 2 en 3: de annotaties zijn te vinden in de naamruimten [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) en [System.ComponentModel.DataAnnotations.Schema] (Column). Andere annotaties zijn te vinden in de naamruimten URL en [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
  • regel 11: [Key] geeft de primaire sleutel aan;
  • regel 12: [Column] bepaalt de naam van de kolom die bij het veld hoort;
  • regel 14: [Required] geeft aan dat het veld verplicht is (SQL NOT NULL);
  • regel 15: [MaxLength] bepaalt de maximale lengte van de tekenreeks, [MinLength] de minimale lengte;

Laten we het project uitvoeren met deze nieuwe definitie van de entiteit [Medecin]. De aangemaakte database ziet er dan als volgt uit:

 
  • de kolommen hebben de naam die we eraan hebben toegekend;
  • de annotatie [Required] is omgezet in SQL, NOT en NULL;
  • de annotatie [MaxLength(N)] is omgezet naar het type SQL nvarchar(N).

In de toepassing NHibernate was de kolom [VERSION] bedoeld om gelijktijdige toegang tot dezelfde rij in een tabel te voorkomen. Het principe is als volgt:

  • een proces P1 leest een rij L uit de tabel [MEDECINS] op tijdstip T1. De rij heeft versie V1;
  • een proces P2 leest dezelfde rij L uit de tabel [MEDECINS] op tijdstip T2. De rij heeft de versie V1 omdat het proces P1 zijn wijziging nog niet heeft gevalideerd;
  • het proces P1 bevestigt zijn wijziging aan rij L. De versie van rij L verandert dan in V2=V1+1;
  • het proces P2 valideert zijn wijziging van regel L. ORM genereert vervolgens een uitzondering omdat het proces P2 een versie V1 van regel L heeft die verschilt van de versie V2 die in de database is aangetroffen.

Dit wordt optimistisch beheer van gelijktijdige toegang genoemd. Bij EF 5 moet een veld dat deze rol vervult een van de twee attributen [Timestamp] of [ConcurrencyCheck] hebben. SQL Server heeft het type [timestamp]. Bij elke invoeging of wijziging van een rij wordt de waarde van een kolom met dit type automatisch gegenereerd door SQL Server. Een dergelijke kolom kan vervolgens worden gebruikt om gelijktijdige toegang te beheren. Om het vorige voorbeeld te herhalen: het proces P2 zal een timestamp aantreffen die verschilt van degene die het heeft gelezen, omdat de wijziging die inmiddels door het proces P1 is aangebracht, deze heeft gewijzigd.

Onze entiteit [Medecin] ontwikkelt zich als volgt:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
  {
    // gegevens
    [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; }
  }
}
  • regels 26-28: de nieuwe kolom met het attribuut [Timestamp] uit regel 27. Het veldtype moet byte[] zijn (regel 28). De veldnaam kan willekeurig zijn. Het attribuut [Required] wordt hier niet toegevoegd, omdat deze waarde niet door de applicatie wordt geleverd, maar door SGBD zelf.

Als het project met deze nieuwe entiteit wordt uitgevoerd, verandert de database als volgt:

Er moet nog één laatste punt worden geregeld. De persistentie-context „weet” dat een entiteit in de database moet worden ingevoegd, omdat de primaire sleutel op dat moment gelijk is aan null. Het is de invoeging in de database die de primaire sleutel een waarde geeft. Hier is het type int dat aan de primaire sleutel [Id] is toegekend, niet geschikt omdat dit type de waarde null niet accepteert. We geven het dan het type int?, dat de waarden *int en de pointer *null accepteert. De entiteit [Medecin] die wordt gebruikt, zal dus als volgt zijn:


public class Medecin
  {
    // gegevens
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    ...

Nu moeten we nog bekijken hoe we het concept van een vreemde sleutel tussen tabellen in een entiteit kunnen weergeven.

3.4.2. De entiteit [Creneau]

De tabel [CRENEAUX] geeft een overzicht van de tijdvakken waarin de RV mogelijk zijn:

  • ID: nummer dat het tijdvak identificeert – primaire sleutel van de tabel
  • VERSION: nummer dat de versie van de rij in de tabel identificeert. Dit nummer wordt met 1 verhoogd telkens wanneer er een wijziging in de rij wordt aangebracht.
  • ID_MEDECIN: identificatienummer van de arts aan wie dit tijdslot toebehoort – vreemde sleutel op de kolom MEDECINS(ID).
  • HDEBUT: starttijd van het tijdvak
  • MDEBUT: minuten begin van het tijdslot
  • HFIN: eindtijd van het tijdvak
  • MFIN: minuten einde tijdslot

De tweede regel van de tabel [CRENEAUX] (zie [1] hierboven) geeft bijvoorbeeld aan dat tijdvak nr. 2 om 8.20 uur begint en om 8.40 uur eindigt en toebehoort aan arts nr. 1 (mevrouw Marie PELISSIER).

Met deze informatie kunnen we de entiteit [Creneau] als volgt definiëren in [Entites.cs]:


[Table("CRENEAUX", Schema = "dbo")]
  public class Creneau
  {
    // gegevens
    [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; }
}

Het enige nieuwe zit in de regels 20-21. Het feit dat de tabel [CRENEAUX] een vreemde sleutel heeft op de tabel [MEDECINS], wordt in de entiteit [Creneau] weergegeven door de aanwezigheid van een verwijzing naar de entiteit [Medecin], regel 21. De naam van het veld doet er niet toe, alleen het type is van belang. De eigenschap moet virtueel worden gedeclareerd met het sleutelwoord virtual. EF moet namelijk alle zogenaamde navigatie-eigenschappen herdefiniëren, d.w.z. die eigenschappen die overeenkomen met een vreemde sleutel en waarmee men van de ene tabel naar de andere kan gaan.

Om de nieuwe entiteit te testen, moeten we enkele wijzigingen aanbrengen in [Context.cs]:


using System.Data.Entity;
using RdvMedecins.Entites;

namespace RdvMedecins.Models
{

  // de context
  public class RdvMedecinsContext : DbContext
  {
    // de entiteiten
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
  }

  // initialisatie van de database
  public class RdvMedecinsInitializer :  DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
  }
}

Regel 12 geeft aan dat de context één entiteit meer moet beheren. Wanneer we het project uitvoeren, krijgen we de volgende nieuwe database:

De tabel [CRENEAUX] is inderdaad aangemaakt en nieuw is de aanwezigheid van een vreemde sleutel [1] en [2]. De naam is gegenereerd op basis van de naam van het overeenkomstige veld in de entiteit (Medecin), met als achtervoegsel „_Id“. Om de eigenschappen van deze vreemde sleutel te bekijken, proberen we deze te wijzigen in [3].

De bovenstaande schermafbeelding laat zien dat [Medecin_Id] de vreemde sleutel is van de tabel [CRENEAUX] en dat deze verwijst naar de primaire sleutel [ID] van de tabel [MEDECINS].

Als we de entiteiten voor een bestaande database aanmaken, zal de kolom met de vreemde sleutel niet noodzakelijkerwijs [Medecin_Id] heten. Voor de andere kolommen hadden we gezien dat de annotatie [Column] dit probleem oploste. Vreemd genoeg is het voor een vreemde sleutel ingewikkelder. Je moet als volgt te werk gaan:


public class Creneau
  {
    // gegevens
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
}
  • regels 5-7: maak een veld aan van het type van de externe sleutel (int). Met behulp van het attribuut [Column] geef je de naam op van de kolom die de externe sleutel zal zijn in de tabel die aan de entiteit is gekoppeld;
  • regel 9: voeg de annotatie [ForeignKey] toe aan het veld van het type [Medecin]. Het argument van deze annotatie is de naam van het veld (niet van de kolom) dat is gekoppeld aan de vreemde-sleutelkolom van de tabel.

Bij het uitvoeren van het project wordt deze keer de volgende tabel aangemaakt:

Hierboven draagt de vreemde-sleutelkolom inderdaad de naam die we eraan hebben gegeven. Merk op dat de velden:


    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }

slechts één kolom hebben opgeleverd, namelijk de kolom [MEDECIN_ID]. Toch is de aanwezigheid van het veld [MedecinId] van belang. Bij het uitlezen van een rij uit de tabel [CRENEAUX] krijgt deze de waarde van de kolom [MEDECIN_ID], d.w.z. de waarde van de vreemde sleutel in de tabel [MEDECINS]. Dit is vaak nuttig.

Het bovenstaande veld [Medecin] weerspiegelt de veel-op-één-relatie die de entiteit [Creneau] koppelt aan de entiteit [Medecin]. Meerdere objecten van het type [Creneau] zijn gekoppeld aan één en hetzelfde object van het type [Medecin]. De omgekeerde relatie, waarbij één object [Medecin] aan meerdere objecten [Creneau] is gekoppeld, kan worden gemodelleerd door middel van een extra veld in de entiteit [Medecin]:


public class Medecin
  {
    // gegevens
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    ...
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

Op regel 8 is het veld [Creneaux] toegevoegd, dat een verzameling is van objecten [Creneau]. Dit veld geeft ons toegang tot alle tijdvakken van de arts.

Wanneer we het project opnieuw uitvoeren, zien we dat de tabel [MEDECINS] ongewijzigd is gebleven:

 

Er is geen enkele kolom toegevoegd. De vreemde-sleutelrelatie tussen de tabel [CRENEAUX] en de tabel [MEDECINS] is voldoende om ervoor te zorgen dat EF de bijbehorende velden kan genereren:


  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; }
    ...
  }

We weten het belangrijkste. We kunnen nu de twee overige entiteiten aanmaken.

3.4.3. De entiteiten [Client] en [Rv]

Met wat we hebben geleerd, kunnen we de entiteiten [Client] en [Rv] schrijven. De entiteit [Client] bevat informatie over de klanten die worden beheerd door de applicatie [RdvMedecins].

  • ID: identificatienummer van de klant – primaire sleutel van de tabel
  • VERSION: identificatienummer van de versie van de rij in de tabel. Dit nummer wordt telkens met 1 verhoogd wanneer er een wijziging in de rij wordt aangebracht.
  • NOM: de naam van de klant
  • PRENOM: de voornaam
  • TITRE: zijn/haar aanspreektitel (mevrouw, meisje, meneer)

De entiteit [Client] zou er als volgt uit kunnen zien:


  [Table("CLIENTS", Schema = "dbo")]
  public class Client
  {
    // gegevens
    [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; }
    // de Rvs van de klant
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

De klasse [Client] is vrijwel identiek aan de klasse [Medecin]. Ze zouden van dezelfde bovenliggende klasse kunnen worden afgeleid. De nieuwigheid zit in regel 21. Deze weerspiegelt het feit dat een klant meerdere afspraken kan hebben en vloeit voort uit de aanwezigheid van een vreemde sleutel van de tabel [RVS] naar de tabel [CLIENTS].

De entiteit [Rv] vertegenwoordigt een afspraak:

  • ID: nummer dat RV op unieke wijze identificeert – primaire sleutel
  • JOUR: dag van RV
  • ID_CRENEAU: tijdvak van RV – externe sleutel op de kolom [ID] van de tabel [CRENEAUX] – bepaalt zowel het tijdvak als de betreffende arts.
  • ID_CLIENT: nummer van de klant voor wie de reservering is gemaakt – externe sleutel op de kolom [ID] van de tabel [CLIENTS]

De entiteit [Rv] zou als volgt kunnen zijn:


[Table("MEDECINS", Schema = "dbo")]
  public class Rv
  {
    // gegevens
    [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; }
}
  • regels 5-7: primaire sleutel;
  • regels 8-10: datum van de afspraak;
  • regels 11-12: de vreemde sleutel van de tabel [RVS] naar de tabel [CLIENTS];
  • regels 13-15: de klant die de afspraak heeft;
  • regels 16-17: de vreemde sleutel van tabel [RVS] naar tabel [CRENEAUX];
  • regels 18-20: het tijdvak van de afspraak;
  • regels 21-23: het veld voor het beheer van gelijktijdige toegang.

In regel 17 zien we een veel-op-één-relatie: aan één tijdslot kunnen meerdere afspraken zijn gekoppeld (niet op dezelfde dag). De omgekeerde relatie kan worden weergegeven in de entiteit [Creneau]:


public class Creneau
  {
    // de Rvs van het tijdvak
    public ICollection<Rv> Rvs { get; set; }
    ...
}

Regel 4: de verzameling afspraken die in dit tijdslot zijn gemaakt.

Wanneer het project wordt uitgevoerd, wordt de volgende database gegenereerd:

 

De tabellen [MEDECINS] en [CRENEAUX] zijn niet gewijzigd. De tabellen [CLIENTS] en [RVS] zien er als volgt uit:

Dit was te verwachten. Er moeten nog enkele details worden geregeld:

  • de naam van de database beheren. Hier is deze gegenereerd door EF;
  • de database vullen met gegevens.

3.4.4. De naam van de database vastleggen

Om de naam van de door EF gegenereerde database vast te leggen, gebruiken we een verbindingsstring die is gedefinieerd in [App.config]. Dit configuratiebestand ziet er nu als volgt uit:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Ga voor meer informatie over de configuratie van Entity Framework naar 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>

  <!-- verbindingsreeks voor de database -->
  <connectionStrings>
    <add name="RdvMedecinsContext"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- de 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>
  • regels 15-19: de verbindingsstring naar de database;
  • regel 16: het attribuut [name] neemt de naam over van de klasse [RdvMedecinsContext] die wordt gebruikt voor de persistentiecontext. Het is belangrijk om dit te onthouden. Deze beperking kan worden omzeild in de constructor van de context:

    // constructor
    public RdvMedecinsContext()
      : base("monContexte")
    {
    }

In dit geval kan name= "monContexte" zijn. Dit is wat we verderop in het document zullen aantreffen.

  • regel 17: de verbindingsstring. [Data Source]: de naam van de server waarop SGBD en [Initial Catalog] staan: de naam van de database, in dit geval dus [rdvmedecins-ef], [User Id]: de eigenaar van de verbinding, [Password]: het wachtwoord. De lezer past deze tekenreeks aan zijn eigen omgeving aan;
  • regels 21-29: definiëren een [DbProviderFactory]. Ik weet niet wat dit is. Afgaande op de naam zou het een klasse kunnen zijn waarmee de laag [ADO.NET] kan worden gegenereerd die EF van SGBD scheidt:

Eigenlijk zijn deze regels overbodig voor SQL Server, maar ik moest ze toevoegen voor de andere SGBD-bestanden. Ik zet ze hier dus ter herinnering neer. Ze staan niet in de weg. Het enige belangrijke punt is de versie op regel 27. Dat is die van de DLL en [System.Data] die in de projectreferenties staan:

Zo. We zijn klaar. We voeren het project uit en krijgen de volgende [rdvmedecins-ef]-basis:

 

Dit wordt onze definitieve database. Nu moeten we er nog gegevens in invoeren.

3.4.5. De database vullen

De initialisatieklasse van de database kan worden gebruikt om gegevens in te voeren:


public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
    // initialisatie van de database
    public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
    {
      protected override void Seed(RdvMedecinsContext context)
      {
        base.Seed(context);
        // de database wordt geïnitialiseerd
        // de klanten
        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);
        }
        // de artsen
        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);
        }
        // de tijdvakken
        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);
        }
        // de afspraken
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
      }

    }
  }
  • regel 6: de initialisatie vindt plaats in de methode [Seed]. Deze methode bestaat in de bovenliggende klasse. Hier wordt ze opnieuw gedefinieerd. Het argument is de persistentiecontext [RdvMedecinsContext] van de applicatie;
  • regel 8: het argument wordt doorgegeven aan de bovenliggende klasse; waarschijnlijk opent deze de doorgegeven persistentiecontext, omdat het openen ervan daarna niet meer nodig is;
  • regels 11-16: aanmaak van 4 klanten;
  • regels 17-20: deze worden toegevoegd aan de persistentiecontext, meer bepaald aan de artsen daarin. Let op de methode [Add] die dit mogelijk maakt. Hierbij moet de definitie van de context in gedachten worden gehouden:

  public class RdvMedecinsContext : DbContext
  {
    // de entiteiten
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
    public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...

Er wordt ook gezegd dat de klanten aan de context zijn gekoppeld, d.w.z. dat ze voortaan door EF worden beheerd. Voorheen waren ze daarvan losgekoppeld. Ze bestonden wel als objecten, maar werden niet door EF beheerd;

  • regels 21-27: aanmaken van 4 artsen;
  • regels 28-31: ze worden in de persistentiecontext geplaatst;
  • regels 33-70: aanmaken van tijdvakken. Regels 34-57 voor de arts medecins[0], regels 58-69 voor de arts medecins[1]. De overige artsen hebben geen tijdvakken;
  • regels 71-74: deze tijdvakken worden in de persistentiecontext geplaatst;
  • regel 76: aanmaken van een afspraak voor de eerste klant met het eerste tijdvak en deze in de persistentie-context plaatsen.

Wanneer het project wordt uitgevoerd, krijgt men de volgende basis:

Hierboven zien we de tabel [CLIENTS] ingevuld.

3.4.6. Wijziging van de entiteiten

Momenteel zijn de klassen [Medecin] en [Client] vrijwel identiek. Als we de velden verwijderen die zijn toegevoegd voor het beheer van de persistentie met EF 5, zijn ze zelfs identiek. We gaan ze laten afleiden van een klasse [Personne]. Deze twee entiteiten worden dan als volgt:


// een persoon
  public abstract class Personne
  {
    // gegevens
    [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; }

    // handtekening
    public override string ToString()
    {
      return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // korte handtekening
    public string ShortIdentity()
    {
      ...
    }

    // hulpprogramma
    private string dump(byte[] timestamp)
    {
      ...
    }

  }

  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin : Personne
  {
    // de spreekuren van de arts
    public ICollection<Creneau> Creneaux { get; set; }
    // handtekening
    public override string ToString()
    {
      return String.Format("Medecin {0}", base.ToString());
    }
  }

[Table("CLIENTS", Schema = "dbo")]
    public class Client : Personne
  {
    // afspraken van de cliënt
    public ICollection<Rv> Rvs { get; set; }
    // handtekening
    public override string ToString()
    {
      return String.Format("Client {0}", base.ToString());
    }
  }

Wanneer we het project uitvoeren, krijgen we dezelfde database. EF 5 heeft de laagste klassen in de overerving elk aan een tabel toegewezen. In feite hanteert EF 5 verschillende strategieën voor het genereren van tabellen om de overerving van entiteiten weer te geven. We zullen deze hier niet toelichten. Zie bijvoorbeeld " Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type", op de URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per].

We zullen voortaan deze versie van de entiteiten gebruiken.

3.4.7. Beperkingen toevoegen aan de database

Er is nog één detail dat we moeten regelen. De tabel [RVS] met afspraken ziet er als volgt uit:

 

Deze tabel moet een uniekheidsbeperking hebben: voor een bepaalde dag mag een tijdslot van een arts slechts één keer voor een afspraak worden gereserveerd. In termen van de tabel betekent dit dat het paar (JOUR,CRENEAU_ID) uniek moet zijn. Ik weet niet of deze beperking rechtstreeks in de code kan worden uitgedrukt, hetzij op de entiteiten, hetzij op de context. Dat is waarschijnlijk wel het geval, maar ik heb het niet gecontroleerd. We zullen een andere aanpak volgen. We gaan een beheerclient van SQL Server gebruiken om deze beperking toe te voegen.

Met „SQL Server Management Studio“ heb ik geen eenvoudige methode gevonden om deze beperking toe te voegen, behalve het uitvoeren van de opdracht SQL die deze aanmaakt:

  • in [1] wordt een query SQL aangemaakt voor de database [rdvmedecins-ef];
  • in [2] wordt de query SQL aangemaakt die de uniekheidsbeperking instelt;
  • in [3] heeft de uitvoering van deze query een nieuwe index aangemaakt in de tabel [RVS].

Er zijn nog andere beheertools voor SQL Server. We gaan hier de tool EMS SQL Manager for SQL Server Freeware [http://www.sqlmanager.net/fr/products/mssql/manager/download] gebruiken. Zodra deze is geïnstalleerd, starten we hem op:

  • in [1] slaan we een database op;
  • in [2] maken we verbinding met de (lokale) server;
  • in [3], met authenticatie via SQL Server;
  • in [4], onder de gebruikersnaam sa;
  • in [5], en het wachtwoord sqlserver2012;
  • in [6] gaan we door naar de volgende stap;
  • in [7], kies je de database [rdvmedecins-ef];
  • in [8] sluit je de wizard af;
  • in [9] verschijnt de database in de databasestructuur. We maken verbinding met [10];
  • in [11] is de verbinding tot stand gebracht.

Met "SQL Manager Lite for SQL Server" kan de uniekheidsbeperking op de tabel [RVS] worden aangemaakt.

  • in [1] zien we de uniekheidsbeperking die we eerder hebben aangemaakt;
  • in [2] verwijderen we deze;
  • in [3] is de index die bij deze uniekheidsbeperking hoort, verdwenen.

We maken de verwijderde voorwaarde opnieuw aan:

  • in [1] maken we een nieuwe index aan voor de tabel [RVS];
  • in [2] geven we deze een naam;
  • in [3] is dit een uniekheidsbeperking;
  • in [4], op de kolommen JOUR en CRENEAU_ID;

Het tabblad DDL geeft ons de code SQL die zal worden uitgevoerd:

  • in [6] wordt de opdracht SQL gecompileerd;
  • in [7], we bevestigen;
  • in [8] is de nieuwe index verschenen.

De interface van "SQL Manager Lite for SQL server" is vergelijkbaar met die van "SQL Server Management Studio". Soortgelijke interfaces zijn te vinden voor de SGBD Oracle, PostgreSQL, Firebird en MySQL. Daarom gaan we vanaf nu verder met deze reeks beheerprogramma’s van SGBD.

Om toegang te krijgen tot de gegevens in een tabel, hoef je er alleen maar op te dubbelklikken:

De informatie over de geselecteerde tabel is beschikbaar in tabbladen. Hierboven zie je het tabblad [Fields] van de tabel [CLIENTS]. Het tabblad [Data] toont de inhoud van de tabel:

Image

3.4.8. De definitieve database

We hebben nu onze definitieve database. We exporteren het bijbehorende script SQL, zodat we de database indien nodig opnieuw kunnen genereren.

  • naar [1], begin van de wizard;
  • naar [2], de server;
  • naar [3], de database die wordt geëxporteerd;
  • in [4], geef de naam op van het bestand waarin het script SQL wordt opgeslagen;
  • in [5], geef de codering op;
  • in [6], geef aan wat u wilt extraheren (tabellen, beperkingen, gegevens);
  • in [7] kunt u het script dat wordt gegenereerd verder verfijnen;
  • bij [8]: sluit de wizard af.

Het script is gegenereerd en in de script-editor geladen. U kunt de gegenereerde code SQL bekijken. We gaan de database op basis van dit script opnieuw opbouwen.

  • in [1] verwijderen we de database;
  • in [2] en [3] wordt deze opnieuw aangemaakt;
  • in [4] wordt men geauthenticeerd;
  • in [5] wordt het script SQL voor het aanmaken van de database uitgevoerd;
  • in [6] slaan we deze op in "SQL Manager";
  • in [7] maakt men verbinding met de zojuist aangemaakte database;
  • in [8] bevat de database momenteel nog geen tabellen;
  • in [9a] wordt een script-editor geopend SQL;
  • in [9b] wordt het eerder aangemaakte script SQL geopend;
  • in [10] voeren we het uit;
  • in [11] zijn de tabellen aangemaakt;
  • in [12] worden ze gevuld;
  • in [14] vinden we de uniekheidsbeperking terug die we hadden aangemaakt voor de tabel [RVS].

We gaan nu met deze bestaande database werken. Mocht deze worden vernietigd of beschadigd raken, dan weten we hoe we deze opnieuw kunnen genereren.

3.5. De database gebruiken met Entity Framework

We gaan:

  • elementen aan de database toevoegen, verwijderen en wijzigen;
  • query's uitvoeren op de database met LINQ to Entities;
  • gelijktijdige toegang tot hetzelfde element in de database beheren;
  • de begrippen Lazy Loading en Eager Loading begrijpen;
  • ontdekken dat het bijwerken van de database door de persistentiecontext plaatsvindt binnen een transactie.

3.5.1. Verwijderen van elementen uit de persistentiecontext

We hebben een gevulde database. We gaan deze leegmaken. We maken een nieuwe klasse [Erase.cs] aan in het huidige project [1]:

De klasse [Erase] ziet er als volgt uit:


using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Erase
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // de huidige database leegmaken
        // de klanten
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        // de artsen
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // de persistentiecontext opslaan
        context.SaveChanges();
      }
    }
  }
}
  • regel 9: bewerkingen op een persistentiecontext vinden altijd plaats in een [using]-clausule. Dit zorgt ervoor dat de context bij het verlaten van de [using] is gesloten;
  • regel 13: de context van de klanten [context.Clients] wordt doorlopen. Alle klanten uit de database worden in de persistentiecontext geplaatst;
  • regel 15: voor elk van hen wordt de bewerking [Remove] uitgevoerd, waardoor ze uit de context worden verwijderd. In feite bevinden ze zich nog steeds in de context, maar in de status "verwijderd";
  • regels 18-21: we doen hetzelfde voor de artsen;
  • regel 23: de persistentiecontext wordt in de database opgeslagen.

Bij het opslaan van de context in de database worden de entiteiten in de context die:

  • een null-primaire sleutel hebben, worden onderworpen aan een bewerking SQL INSERT;
  • de status „verwijderd” hebben, worden onderworpen aan een bewerking SQL DELETE;
  • de status „gewijzigd” hebben, worden verwerkt door de bewerking SQL UPDATE;

Zoals we later zullen zien, vinden deze bewerkingen SQL plaats binnen een transactie. Als een van deze bewerkingen mislukt, wordt alles wat eerder is gedaan ongedaan gemaakt.

Laten we het programma [Erase] instellen als het nieuwe startobject van het project [1] en vervolgens het project uitvoeren.

Laten we de database controleren. We zullen zien dat alle tabellen leeg zijn: [2]. Dat is verrassend, want we hadden alleen gevraagd om het verwijderen van de artsen en de klanten. Door het spel van de vreemde sleutels zijn de andere tabellen in cascade leeggehaald.

De definitie van de vreemde sleutel van de tabel [CRENEAUX] naar de tabel [MEDECINS] is door de provider van EF 5 als volgt gedefinieerd:

  • in [1] selecteert u de tabel [CRENEAUX];
  • in [2] selecteert men het tabblad met de externe sleutels;
  • in [3] bewerkt men de enige externe sleutel;
  • in [4], op het tabblad DDL, de definitie SQL van de vreemde-sleutelbeperking;
  • In [5] zorgt de clausule ON DELETE CASCADE ervoor dat het verwijderen van een arts ook leidt tot het verwijderen van de aan hem gekoppelde tijdvakken.

De foreign key-beperkingen van de tabel [RVS] zijn op dezelfde manier gedefinieerd:

1
2
3
4
5
6
ALTER TABLE [dbo].[RVS]
ADD CONSTRAINT [FK_dbo.RVS_dbo.CLIENTS_CLIENT_ID] FOREIGN KEY ([CLIENT_ID]) 
  REFERENCES [dbo].[CLIENTS] ([ID]) 
  ON UPDATE NO ACTION
  ON DELETE CASCADE
GO
  • regels 1-6: het verwijderen van een klant leidt ook hier tot het verwijderen van de bijbehorende afspraken;
1
2
3
4
5
6
ALTER TABLE [dbo].[RVS]
ADD CONSTRAINT [FK_dbo.RVS_dbo.CRENEAUX_CRENEAU_ID] FOREIGN KEY ([CRENEAU_ID]) 
  REFERENCES [dbo].[CRENEAUX] ([ID]) 
  ON UPDATE NO ACTION
  ON DELETE CASCADE
GO
  • regels 1-6: het verwijderen van een tijdslot verwijdert ook alle afspraken die eraan zijn gekoppeld.

3.5.2. Elementen toevoegen aan de persistentiecontext

Nu we de database hebben leeggemaakt, gaan we deze weer vullen. We voegen het programma [Fill.cs] [1] toe aan het project.

Het programma [Fill.cs] ziet er als volgt uit:


using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Fill
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // de huidige database leegmaken
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // de database wordt gereset
        // de klanten
        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);
        }
        // de artsen
        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);
        }
        // de tijdvakken
        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);
        }
        // de afspraken
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // de context wordt opgeslagen
        context.SaveChanges();
      }
    }
  }
}
  • regel 10: we openen de persistentiecontext;
  • regels 13-20: de rijen uit de tabellen [CLIENTS] en [MEDECINS] worden in de context geplaatst en vervolgens daaruit verwijderd. We hebben zojuist gezien dat dit de database volledig leegmaakte;
  • regels 22-88: er worden elementen aan de persistentiecontext toegevoegd. Ze hebben allemaal een primaire sleutel met de waarde null. Ze worden dus in de database ingevoegd;
  • regel 90: de wijzigingen die in de context zijn aangebracht, worden gesynchroniseerd met de database. Hierop volgt een reeks bewerkingen SQL DELETE, gevolgd door een reeks bewerkingen SQL INSERT;

We maken van het programma [Fill] het nieuwe startobject van het project [1] en voeren dit vervolgens uit.

In [2] zien we dat de tabellen zijn gevuld.

3.5.3. De inhoud van de database weergeven

We gaan nu de inhoud van de database weergeven met behulp van de query’s LINQ to Entity. LINQ (Language INtegrated Query) is in 2007 geïntroduceerd met het .NET 3.5-framework. Het fungeert als een uitbreiding van de talen .NET en c.a.d, waarin het is geïntegreerd, en de syntaxis ervan wordt door de compiler gecontroleerd. Hiermee kunnen verschillende collecties worden doorzocht met een syntaxis die overeenkomsten vertoont met de SQL-taal (Structured Query Language) voor het doorzoeken van databases. Er bestaan verschillende versies van LINQ:

  • LINQ to Object, voor het doorzoeken van collecties in het geheugen;
  • LINQ to XML, voor het doorzoeken van XML;
  • LINQ naar Entity, om databases te doorzoeken;

Om te kunnen functioneren, is LINQ afhankelijk van talrijke uitbreidingen op de .NET-talen. Deze kunnen buiten LINQ worden gebruikt. We zullen ze hier niet toelichten, maar geven alleen twee referenties waar de lezer een uitgebreide beschrijving van LINQ kan vinden:

  • LINQ in Action, Fabrice Marguerie, Steve Eichert, Jim Wooley, uitgegeven door Manning;
  • LINQ pocket reference, Joseph en Ben Albahari, uitgegeven door O’Reilly.

Ik heb het eerste boek gelezen en vond het uitstekend. Het tweede heb ik niet gelezen, maar ik heb wel „C# 3.0 in a nutshell“ van dezelfde auteurs gelezen toen LINQ uitkwam. Ik vond dit boek ver boven het gemiddelde van de boeken die ik gewoonlijk lees. Het lijkt erop dat de andere boeken van deze twee auteurs van hetzelfde niveau zijn. We gaan overigens gebruikmaken van LINQPad, een leermiddel voor LINQ geschreven door Joseph Albahari.

We gaan de entiteiten in de database weergeven. Hiervoor voegen we aan hun klassen twee weergavemethoden toe. Laten we beginnen met de entiteit [Medecin]:


// een arts
  public class Medecin
  {
    // gegevens
    [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; }
    // de tijdvakken van de arts
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // handtekening
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // korte handtekening
    public string ShortIdentity()
    {
      return ToString();
    }

    // hulpprogramma
    private string dump(byte[] timestamp){
      string str = "";
      foreach (byte b in timestamp)
      {
        str += b;
      }
      return str;
    }
  }
  • regels 27-30: de methode ToString van de klasse. Merk op dat deze de verzameling uit regel 21 niet weergeeft;
  • regels 32-37: de methode ShortIdentity die hetzelfde doet.

Om de impact van de twee voorgaande methoden te kunnen beoordelen, moeten we hier de begrippen ‘Lazy Loadingen ‘Eager Loading’ toelichten. We hebben gezien dat een entiteit afhankelijkheden kan hebben van een andere entiteit. Deze afhankelijkheden zijn van twee soorten:

  • van één naar meerdere, zoals hierboven, waar een arts gekoppeld is aan meerdere tijdvakken;
  • van meerdere naar één, zoals in de entiteit [Creneau] hieronder, waar meerdere tijdvakken aan dezelfde arts zijn gekoppeld;

public class Creneau
  {
    // gegevens
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
  }

Wanneer de afhankelijkheden tegelijk met de entiteiten waaraan ze zijn gekoppeld worden geladen, spreken we van ‘Eager Loading’. Anders spreken we van ‘Lazy Loading’: de afhankelijkheden worden pas geladen wanneer er voor het eerst naar wordt verwezen. Standaard maakt EF 5 gebruik van ‘Lazy Loading’: de afhankelijkheden worden niet tegelijk met de entiteit geladen.

Laten we eens kijken naar onze methode [ToString] hierboven:


    // de spreekuren van de arts
    public ICollection<Creneau> Creneaux { get; set; }
    
    // handtekening
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // verkorte handtekening
    public string ShortIdentity()
    {
      return ToString();
}

De methode [ToString] geeft de afhankelijkheid [Creneaux] uit regel 2 niet weer. Als dat wel het geval was geweest, zou dit ertoe hebben geleid dat alle afspraken van de arts vóór de uitvoering van de methode geladen moesten worden. Om dit kostbare laden te vermijden, is de afhankelijkheid niet opgenomen in de handtekening van de entiteit. Over het algemeen nemen we twee handtekeningen op in elke entiteit:

  • een methode ToString die de entiteit en de eventuele één-op-veel-afhankelijkheden weergeeft. Zoals zojuist is uitgelegd, zal dit ervoor zorgen dat de afhankelijkheid wordt geladen;
  • een methode ShortIdentity die geen afhankelijkheid zal verwijzen. Er zullen dus geen afhankelijkheden worden geladen;

De weergavemethoden van de andere entiteiten zijn als volgt:

De entiteit [Client]:


  public class Client
  {
    // gegevens
    ...
    // de afspraken van de cliënt
    public ICollection<Rv> Rvs { get; set; }
    
    // handtekening
    public override string ToString()
    {
      return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // verkorte handtekening
    public string ShortIdentity()
    {
      return ToString();
    }

}
  • regels 9-12: de methode [ToString] geeft de afhankelijkheid van regel 6 niet weer;

De entiteit [Creneau]:


public class Creneau
  {
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    // de afspraken van het tijdslot
    public ICollection<Rv> Rvs { get; set; }
    
    // handtekening
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
    // korte handtekening
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
    }
  }
  • regel 16: de methode [ToString] verwijst naar de afhankelijkheid van regel 9. Dit zorgt ervoor dat deze wordt geladen;
  • regel 11: er wordt niet verwezen naar de afhankelijkheid [Rvs]. Deze wordt niet geladen;
  • regels 21-22: de methode [ShortIdentity] verwijst niet langer naar de verwijzing [Medecin] uit regel 9. Deze wordt dus niet geladen.

De entiteit [Rv]:


public class Rv
  {
    // gegevens
    ...
    [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; }

    // handtekening
    public override string ToString()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
    }
    // korte handtekening
    public string ShortIdentity()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
    }

  }
  • regels 17-20: de methode [ToString] verwijst naar de afhankelijkheden van de regels 9 en 14. Dit zorgt ervoor dat ze worden geladen;
  • regels 17-20: de methode [ShortIdentity] voorkomt dit, waardoor de afhankelijkheden niet worden geladen.

Kortom, we moeten letten op de methoden [ToString] van de entiteiten. Als we hier geen aandacht aan besteden, kan het weergeven van een tabel de helft van de database laden als de tabel veel afhankelijkheden heeft.

Nu dit is uitgelegd, schrijven we de volgende nieuwe code [Dump.cs]:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

namespace RdvMedecins_01
{
  class Dump
  {
    static void Main(string[] args)
    {
      // database-dump
      using (var context = new RdvMedecinsContext())
      {
        // de klanten
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // de artsen
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // de tijdvakken
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // de afspraken
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}

We zullen de regels 17-21 toelichten, die de entiteiten [Client] weergeven. De gegeven uitleg geldt ook voor de andere entiteiten.


        // de klanten
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
}
  • regel 3: het sleutelwoord var is geïntroduceerd met C# 3.0. Hiermee hoeft het precieze type van een variabele niet te worden opgegeven. De compiler leidt dit dan af uit het type van de uitdrukking die aan de variabele wordt toegewezen;
  • regel 3: de uitdrukking die aan de variabele clients is toegewezen, is een LINQ to Entity-query. Hierin zijn sleutelwoorden herkenbaar uit de taal SQL die in LINQ zijn overgenomen. De hier gebruikte syntaxis is als volgt:

from variable in DbSet select variable

Een meer algemene syntaxis van LINQ is


from variable in collection select variable

De verzameling wordt doorlopen en voor elk element daarvan wordt de variabele geëvalueerd. Dit gebeurt pas wanneer de variabele [clients] uit regel 3 wordt doorlopen door de for / each-lus in de regels 4-7. Zolang dit niet gebeurt, is de variabele [clients] slechts een niet-geëvalueerde query;

  • regel 4: de query [clients] wordt doorlopen. Dit dwingt de evaluatie van de query af. De rijen van de tabel [CLIENTS] worden beurtelings in de persistentie-context gebracht;
  • regel 6: de methode [ToString] van de entiteit [Client] wordt gebruikt voor de weergave. Er worden geen afhankelijkheden geladen;

Laten we verdergaan met de volgende regels van de code:

  • regels 24-28: de rijen van de tabel [MEDECINS] worden in de persistentiecontext gebracht en weergegeven. Er worden geen afhankelijkheden geladen;
  • regels 31-35: de regels van de tabel [CRENEAUX] worden in de persistentiecontext gebracht en weergegeven. We hebben gezien dat de methode [ToString] van deze entiteit de afhankelijkheid [Medecin] weergeeft. Deze is echter al geladen. Er vindt dus geen nieuwe lading plaats;
  • regels 38-42: de rijen van de tabel [RVS] worden in de persistentiecontext geladen en weergegeven. We hebben gezien dat de methode [ToString] van deze entiteit de afhankelijkheden [Client] en [Creneau] weergeeft. Deze zijn echter al geladen. Er vindt dus geen nieuwe lading plaats.

Merk op dat de volgorde van weergave niet willekeurig is. Als we eerst de entiteiten [Rv] hadden willen weergeven, zou de methode [ToString] van deze entiteit ervoor hebben gezorgd dat de entiteiten [Client] en [Creneau], die aan deze afspraken zijn gekoppeld, werden geladen. De overige zouden niet zijn geladen. Die zouden pas later in een andere weergave zijn geladen. Dit heeft gevolgen voor de prestaties. De vorige code heeft vier SQL-opdrachten nodig om alle entiteiten weer te geven. Laten we nu aannemen dat we eerst de tabel [RVS] met afspraken gebruiken. Er is een eerste query SQL nodig voor de tabel [RVS]. Vervolgens zal de methode [ToString] van de entiteit [Rv] ervoor zorgen dat de bijbehorende entiteiten [Client] en [Creneau] eventueel worden geladen. Voor elk daarvan is een query SQL nodig. Ervan uitgaande dat er N2 klanten en N3 tijdvakken zijn en dat al deze entiteiten worden vermeld in de tabel [RVS], dan zijn er voor de weergave daarvan 1 + N2 + N3 SQL-query's nodig. De prestaties zijn dus minder goed dan in de onderzochte versie. Om de tabel [RVS] met zijn afhankelijkheden weer te geven, zou een tabeljoin nodig zijn. Dit kan worden gerealiseerd met LINQ. We zullen hier later aan de hand van een voorbeeld op terugkomen. Voorlopig onthouden we dat we aandacht moeten besteden aan de SQL-query’s die ten grondslag liggen aan onze code LINQ.

We configureren het project om deze nieuwe code [1] en [2] uit te voeren en voeren deze vervolgens uit:

De console-uitvoer ziet er als volgt uit:

Clients--------------------------------------
Client[9,Mr,Jules,Martin,000000844]
Client[10,Mme,Christine,German,000000845]
Client[11,Mr,Jules,Jacquard,000000846]
Client[12,Melle,Brigitte,Bistrou,000000847]
Médecins--------------------------------------
Medecin[9,Mme,Marie,Pelissier,000000848]
Medecin[10,Mr,Jacques,Bromard,000000873]
Medecin[11,Mr,Philippe,Jandot,000000886]
Medecin[12,Melle,Justine,Jacquemot,000000887]
Créneaux horaires--------------------------------------
Creneau[73,8,0,8,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000849]
Creneau[74,8,20,8,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000850]
Creneau[75,8,40,9,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000851]
Creneau[76,9,0,9,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000852]
Creneau[77,9,20,9,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000853]
Creneau[78,9,40,10,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000854]
Creneau[79,10,0,10,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000855]
Creneau[80,10,20,10,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000856]
Creneau[81,10,40,11,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000857]
Creneau[82,11,0,11,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000858]
Creneau[83,11,20,11,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000859]
Creneau[84,11,40,12,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000860]
Creneau[85,14,0,14,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000861]
Creneau[86,14,20,14,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000862]
Creneau[87,14,40,15,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000863]
Creneau[88,15,0,15,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000864]
Creneau[89,15,20,15,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000865]
Creneau[90,15,40,16,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000866]
Creneau[91,16,0,16,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000867]
Creneau[92,16,20,16,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000868]
Creneau[93,16,40,17,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000869]
Creneau[94,17,0,17,20, Medecin[9,Mme,Marie,Pelissier,000000848],000000870]
Creneau[95,17,20,17,40, Medecin[9,Mme,Marie,Pelissier,000000848],000000871]
Creneau[96,17,40,18,0, Medecin[9,Mme,Marie,Pelissier,000000848],000000872]
Creneau[97,8,0,8,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000874]
Creneau[98,8,20,8,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000875]
Creneau[99,8,40,9,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000876]
Creneau[100,9,0,9,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000877]
Creneau[101,9,20,9,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000878]
Creneau[102,9,40,10,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000879]
Creneau[103,10,0,10,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000880]
Creneau[104,10,20,10,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000881]
Creneau[105,10,40,11,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000882]
Creneau[106,11,0,11,20, Medecin[10,Mr,Jacques,Bromard,000000873],000000883]
Creneau[107,11,20,11,40, Medecin[10,Mr,Jacques,Bromard,000000873],000000884]
Creneau[108,11,40,12,0, Medecin[10,Mr,Jacques,Bromard,000000873],000000885]
Rendez-vous--------------------------------------
Rv[3,08/10/2012 00:00:00,Client[9,Mr,Jules,Martin,000000844],Creneau[73,8,0,8,20
, Medecin[9,Mme,Marie,Pelissier,000000848],000000849],000000888]
Appuyez sur une touche pour continuer...

3.5.4. LINQ leren met LINQPad

Hierboven hebben we LINQ-query’s naar Entity gebruikt om de inhoud van de databasetabellen weer te geven. Joseph Albahari heeft een programma geschreven om de verschillende vormen van LINQ te leren. We presenteren dit nu.

LINQPad is beschikbaar via de volgende URL [http://www.linqpad.net/]. Zodra het is geïnstalleerd, starten we het op via [1]:

Beginners kunnen aan de slag met de voorbeelden op het tabblad [Samples] [2], waar talrijke voorbeelden te vinden zijn. Laten we het voorbeeld [3] selecteren, dat vervolgens in een ander venster [4] wordt weergegeven. De volledige code van het voorbeeld is als volgt:


// Nu een eenvoudige LINQ-to-objects-query (let op: geen puntkomma):

from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word


// Voel je vrij om dit te bewerken... (niemand kijkt mee!) Je wordt gevraagd om eventuele
// wijzigingen in een apart bestand op te slaan.
//
// Tip:   Je kunt een deel van een query uitvoeren door het te markeren en vervolgens op F5 te drukken.

De regels 3-5 zijn een voorbeeld van een LINQ-query naar Object. De query LINQ volgt de volgende syntaxis:


from variable in collection orderby élément1 select élément2
  • variabele verwijst naar het huidige element van de verzameling. In ons voorbeeld is deze verzameling de lijst met woorden die het resultaat zijn van de opgesplitste tekenreeks;
  • de verzameling is gesorteerd volgens de parameter élément1 van </span>**<span style="color: #000000">orderby**. In ons voorbeeld wordt de verzameling woorden gesorteerd op lengte;
  • het sleutelwoord </span>**<span style="color: #000000">select *geeft aan wat we uit het huidige element </span>*<span style="color: #000000">variable van de verzameling willen halen. In ons voorbeeld is dat het woord.

Laten we deze query LINQ uitvoeren:

  • in [1]: een uitdrukking LINQ wordt uitgevoerd door [F5] of via de uitvoerknop;
  • in [2]: de weergave. De woorden worden weergegeven in volgorde van lengte. Dit eenvoudige voorbeeld toont de kracht van LINQ;
  • in [3] is het mogelijk om andere voorbeelden te downloaden, met name die uit het boek „LINQ in action“ [4];
  • in [5] kiezen we een voorbeeld uit het boek;

string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };

// Groepeer woorden op lengte
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 };

// Elke groep afdrukken
foreach (var group in groups)
{
  Console.WriteLine("Words of length " + group.Length);
  foreach (string word in group.Words)
    Console.WriteLine("  " + word);
}
  • regel 4: een nieuwe zoekopdracht LINQ met nieuwe trefwoorden;
  • regel 5: de opgevraagde verzameling is de woordenlijst van regel 1;
  • regel 6: de verzameling wordt gesorteerd in alfabetische volgorde van de woorden;
  • regel 7: de verzameling wordt gegroepeerd in (trefwoord into) een nieuwe verzameling lengthGroups. lengthGroups.Key vertegenwoordigt de groeperingsfactor (trefwoord by), in dit geval de lengte van de woorden. lengthGroups verzamelt de woorden met dezelfde groeperingsfactor, dus dezelfde lengte;
  • regel 8: de verzameling lengthGroups wordt gesorteerd op aflopende groeperingssleutel, dus hier op aflopende woordlengte;
  • regel 9: uit deze verzameling worden nieuwe objecten (anonieme klassen) gegenereerd met twee velden:
    • Length: de lengte van de woorden,
    • Words: de woorden met deze lengte;

Hier zien we vooral het nut van het sleutelwoord var in regel 4. Omdat we in regel 9 een anonieme klasse hebben gebruikt, kunnen we het type van de variabele groups niet specificeren. De compiler zal de anonieme klasse een interne naam geven en daarmee de variabele groups typen. Vervolgens kan hij bepalen of de variabele groups correct wordt gebruikt

  • regel 12: doorlopen van de query uit regel 4. Pas op dit moment wordt deze geëvalueerd. We herinneren ons dat de uitvoering ervan een verzameling objecten oplevert, zoals gespecificeerd in regel 9;
  • regel 14: de eigenschap Length van het huidige element wordt weergegeven, dus de lengte van de woorden;
  • regels 15-17: elk element van de verzameling van de eigenschap Words wordt weergegeven, dus alle woorden met de eerder weergegeven lengte.

Wanneer we deze query uitvoeren, krijgen we het volgende resultaat in LINQPad:

 

Nu we enkele voorbeelden van [LINQ to Object]-query's hebben gezien, gaan we kijken naar [LINQ to Entity]-query's waarmee we databases kunnen doorzoeken. We gaan eerst verbinding maken met de database SQL Server die we hebben aangemaakt en gevuld:

  • in [1] voegen we een verbinding met een database toe;
  • in [2], de toegangsmethoden tot de gegevensbron. Om toegang te krijgen tot de database SQL Server, gebruiken we [LINQPad Driver];
  • in [3] is het ook mogelijk om een persistentiecontext [DbContext] op te halen die is gedefinieerd in een assembly .exe- of .dll-bestand (optie 3). Helaas wordt Entity Framework 5 op dit moment (8 oktober 2012) nog niet ondersteund;
  • in [4] is het mogelijk om stuurprogramma’s te downloaden voor andere SGBD dan SQL Server;
  • in [5] downloadt u de driver voor de SGBD, MySQL en Oracle;
  • in [6], het gedownloade stuurprogramma;
  • in [7] maken we verbinding met een SQL-server;
  • in [8] staat de database op de (lokale) naamserver;
  • in [9] maken we verbinding met de authenticatie sa / sqlserver2012;
  • in [10], naar de database [rdvmedecins-ef] die we hebben aangemaakt;
  • in [11] kunnen we de verbinding testen;
  • in [12] sluiten we de wizard af;
  • in [13] verschijnt de verbinding in LINQPad.

De entiteiten zijn aangemaakt op basis van de tabel [rdvmedecins-ef]. Dit zijn de volgende:

  • in [1] vertegenwoordigt [CLIENTS] de verzameling entiteiten [Client]. Elke entiteit heeft:
    • de eigenschappen (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • een 1-op-veel-relatie met [CLIENTRVS];
  • waarbij [2] en [CRENEAUXes] de verzameling entiteiten [Creneau] vertegenwoordigen. Elke entiteit heeft:
    • de eigenschappen (ID, HDEBUT, MDEBUT, HFIN, MFIN, MEDECIN_ID, TIMESTAMP),
    • een 1-op-veel-relatie [CRENEAURVS],
  • een relatie van meerdere naar 1: [MEDECIN];
  • in [3] vertegenwoordigt de entiteit [MEDECINS] de verzameling van entiteiten [Medecin]. Elke entiteit heeft:
    • de eigenschappen (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • een 1-op-veel-relatie [MEDECINCRENEAUXes];
  • in [4] vertegenwoordigt de entiteit [RVS] de verzameling van entiteiten [Rv]. Elke entiteit heeft:
    • de eigenschappen (ID, JOUR, CLIET_ID, CRENEAU_ID, TIMESTAMP),
    • een 'veel-op-1'-relatie met [CLIENT],
    • een 'meer-op-1'-relatie [CRENEAU].

Merk op dat de namen van de bovenstaande eigenschappen verschillen van de namen die we tot nu toe hebben gebruikt. Dat maakt niet uit. We willen alleen de basisprincipes van het opvragen van gegevens uit een database leren.

Laten we eens kijken hoe we deze entiteitendatabase kunnen doorzoeken. We willen bijvoorbeeld de lijst met artsen gesorteerd op hun TITRE en NOM:

  • in [1] maken we een nieuwe query aan;
  • in [2], de tekst van de query;
  • in [3], het resultaat van de query;
  • in [4], dezelfde query met lambda-expressies. Een query met lambda-expressies is minder leesbaar dan een tekstquery en je zou er misschien liever zonder doen. Ze zijn echter soms onmisbaar, omdat ze bepaalde dingen mogelijk maken die met tekstquery's niet mogelijk zijn. Een lambda-uitdrukking verwijst naar een functie met één invoerparameter a en één uitvoerparameter b, in de vorm a=>b. De bovenstaande methode OrderBy accepteert een lambda-functie als enige parameter. Deze levert de parameter aan waarmee een verzameling moet worden gesorteerd. Zo is MEDECINS.OrderBy(m=>m.TITRE) de lijst met artsen, gesorteerd op titel. De instructie moet worden gelezen als een pijplijn op een verzameling. De verzameling artsen wordt als invoer doorgegeven aan de methode OrderBy. Deze verwerkt de entiteiten [Medecin] één voor één. In de lambda-uitdrukking m=>m.TITRE staat m voor de invoer van de lambda-functie. Je kunt deze naam naar eigen keuze kiezen. In dit geval is de invoer van de lambda-functie een entiteit [Medecin]. De functie m=>m.TITRE luidt als volgt: als ik m mijn invoer noem (een entiteit [Medecin]), dan is mijn uitvoer m.TITRE, dus de titel van de arts. MEDECINS.OrderBy(m=>m.TITRE) is op zijn beurt een verzameling, namelijk de verzameling van artsen gesorteerd op titel. Deze nieuwe verzameling kan als invoer dienen voor een andere methode, in het voorbeeld de methode ThenBy. Deze werkt volgens hetzelfde principe. Ze dient om extra parameters op te geven voor het sorteren van de verzameling.

Het lezen van de lambda-code die overeenkomt met de tekstcode die we gewoonlijk typen, is een goede manier om deze te leren;

  • in [5], de opdracht SQL die naar de database wordt verzonden. Ook hier zullen we deze code aandachtig doornemen. Hiermee kunnen we de werkelijke kosten van een query LINQ inschatten.

Hieronder geven we enkele voorbeelden van verzoeken van het type LINQ. Telkens tonen we de weergegeven resultaten en de bijbehorende lambda- en SQL-codes. Om deze verzoeken te begrijpen, moeten we de ‘veel-op-één’-relaties in gedachten houden die de entiteiten met elkaar verbinden. Via deze relaties navigeert men van de ene entiteit naar de andere. We noemen dit navigatie-eigenschappen.

// klanten met de titel ‘Mr’, gesorteerd in aflopende volgorde van de namen

Resultaten:

 
LINQ

from client in CLIENTS where client.TITRE=="Mr" 
orderby client.NOM descending  select client
Lambda
CLIENTS
.Where (client => (client.TITRE == "Mr"))
.OrderByDescending (client => client.NOM)
SQL

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Mr'
-- EndRegion
SELECT [t0].[ID], [t0].[TITRE], [t0].[NOM], [t0].[PRENOM],
 [t0].[TIMESTAMP]
FROM [CLIENTS] AS [t0]
WHERE [t0].[TITRE] = @p0
ORDER BY [t0].[NOM] DESC

// alle afspraken met de bijbehorende arts

(Gedeeltelijke) resultaten:

 
LINQ

from creneau in CRENEAUXes 
select new { hd=creneau.HDEBUT, md=creneau.MDEBUT, hf=creneau.HFIN,
 mf=creneau.MFIN, medecin=creneau.MEDECIN}
Lambda
SQL

SELECT [t0].[HDEBUT] AS [hd], [t0].[MDEBUT] AS [md], [t0].[HFIN] AS [hf],
 [t0].[MFIN] AS [mf], [t1].[ID], [t1].[TITRE], [t1].[NOM], [t1].[PRENOM],
 [t1].[TIMESTAMP]
FROM [CRENEAUX] AS [t0]
INNER JOIN [MEDECINS] AS [t1]
 ON [t1].[ID] = [t0].[MEDECIN_ID]

// alle afspraken met de klant en de bijbehorende arts

Resultaten:

 
LINQ

from rv in RVS select new { rv=rv.CLIENT, medecin=rv.CRENEAU.MEDECIN}
Lambda
SQL

SELECT [t1].[ID], [t1].[TITRE], [t1].[NOM], [t1].[PRENOM], [t1].[TIMESTAMP],
 [t3].[ID] AS [ID2], [t3].[TITRE] AS [TITRE2], [t3].[NOM] AS [NOM2],
 [t3].[PRENOM] AS [PRENOM2], [t3].[TIMESTAMP] AS [TIMESTAMP2]
FROM [RVS] AS [t0]
INNER JOIN [CLIENTS] AS [t1] ON [t1].[ID] = [t0].[CLIENT_ID]
INNER JOIN [CRENEAUX] AS [t2] ON [t2].[ID] = [t0].[CRENEAU_ID]
INNER JOIN [MEDECINS] AS [t3] ON [t3].[ID] = [t2].[MEDECIN_ID]

// artsen zonder afspraak

Resultaten:

 
LINQ
 
Lambda
SQL

SELECT [t0].[ID], [t0].[TITRE], [t0].[NOM], [t0].[PRENOM], [t0].[TIMESTAMP]
FROM [MEDECINS] AS [t0]
WHERE NOT (EXISTS(
    SELECT NULL AS [EMPTY]
    FROM [RVS] AS [t1]
    INNER JOIN [CRENEAUX] AS [t2] ON [t2].[ID] = [t1].[CRENEAU_ID]
    INNER JOIN [MEDECINS] AS [t3] ON [t3].[ID] = [t2].[MEDECIN_ID]
    WHERE [t3].[ID] = [t0].[ID]
    ))

Er is geen query LINQ voor deze aanvraag. Je moet lambda-expressies gebruiken. Deze luidt als volgt: ik neem de verzameling artsen (MEDECINS) en behoud (Where) alleen de artsen (m) waarbij ik in de verzameling afspraken (RVS) geen afspraak (rv) met deze arts (m) kan vinden.

// tijdvakken van mevrouw Pélissier

(Gedeeltelijke) resultaten:

 
LINQ

from creneau in CRENEAUXes where creneau.MEDECIN.NOM=="Pelissier"
 select creneau
Lambda
SQL

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Pelissier'
-- EndRegion
SELECT [t0].[ID], [t0].[HDEBUT], [t0].[MDEBUT], [t0].[HFIN], [t0].[MFIN],
 [t0].[MEDECIN_ID], [t0].[TIMESTAMP]
FROM [CRENEAUX] AS [t0]
INNER JOIN [MEDECINS] AS [t1] ON [t1].[ID] = [t0].[MEDECIN_ID]
WHERE [t1].[NOM] = @p0

// aantal afspraken van mevrouw Pélissier op 08/10/2012

Resultaten:

 
LINQ

(from rv in RVS where rv.CRENEAU.MEDECIN.NOM=="Pelissier"
 && rv.JOUR==new DateTime(2012,10,08)  select rv).Count()
Lambda
 
SQL

-- Region Parameters
DECLARE @p0 NVarChar(1000) = 'Pelissier'
DECLARE @p1 DateTime = '2012-10-08 00:00:00.000'
-- EndRegion
SELECT COUNT(*) AS [value]
FROM [RVS] AS [t0]
INNER JOIN [CRENEAUX] AS [t1] ON [t1].[ID] = [t0].[CRENEAU_ID]
INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]
WHERE ([t2].[NOM] = @p0) AND ([t0].[JOUR] = @p1)

// lijst van klanten die op 08/10/2012 een afspraak hebben gemaakt met mevrouw Pélissier

Resultaten:

 
LINQ

from rv in RVS where (rv.JOUR==new DateTime(2012,10,08)
 && rv.CRENEAU.MEDECIN.NOM=="Pelissier") select rv.CLIENT
Lambda
SQL

-- Region Parameters
DECLARE @p0 DateTime = '2012-10-08 00:00:00.000'
DECLARE @p1 NVarChar(1000) = 'Pelissier'
-- EndRegion
SELECT [t3].[ID], [t3].[TITRE], [t3].[NOM], [t3].[PRENOM], [t3].[TIMESTAMP]
FROM [RVS] AS [t0]
INNER JOIN [CRENEAUX] AS [t1] ON [t1].[ID] = [t0].[CRENEAU_ID]
INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]
INNER JOIN [CLIENTS] AS [t3] ON [t3].[ID] = [t0].[CLIENT_ID]
WHERE ([t0].[JOUR] = @p0) AND ([t2].[NOM] = @p1)

// aantal tijdvakken per arts

Resultaten:

 
LINQ

from creneau in CRENEAUXes 
group creneau by creneau.MEDECIN into creneauxMedecin 
select new { nom=creneauxMedecin.Key.NOM,
 prenom=creneauxMedecin.Key.PRENOM,
 nbRv=creneauxMedecin.Count()}
Lambda
SQL

SELECT [t2].[NOM] AS [nom], [t2].[PRENOM] AS [prenom], [t1].[value] AS [nbRv]
FROM (
    SELECT COUNT(*) AS [value], [t0].[MEDECIN_ID]
    FROM [CRENEAUX] AS [t0]
    GROUP BY [t0].[MEDECIN_ID]
    ) AS [t1]
INNER JOIN [MEDECINS] AS [t2] ON [t2].[ID] = [t1].[MEDECIN_ID]

3.5.5. Een entiteit wijzigen die aan de persistentiecontext is gekoppeld

We hebben de volgende bewerkingen op de persistentiecontext bekeken:

  • een element aan de context toevoegen ([dbContext].[DbSet].Add);
  • een element uit de context verwijderen ([dbContext].[DbSet].Remove);
  • een context opvragen met LINQ-query's.

Als je de context met de database wilt synchroniseren, schrijf je [dbContext].SaveChanges().

De code [ModifyAttachedEntity] illustreert het wijzigen van een entiteit die aan de context is gekoppeld:


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;
      // 1e context
      using (var context = new RdvMedecinsContext())
      {
        // de huidige database leegmaken
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // Een klant toevoegen
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // opvolging
        Console.WriteLine("client1--avant");
        Console.WriteLine(client1);
        // context opslaan
        context.SaveChanges();
        // opvolging
        Console.WriteLine("client1--après");
        Console.WriteLine(client1);
      }
      // 2e context
      using (var context = new RdvMedecinsContext())
      {
        // klant1 wordt opgehaald in klant2
        client2 = context.Clients.Find(client1.Id);
        // vervolg
        Console.WriteLine("client2");
        Console.WriteLine(client2);
        // klant2 wordt gewijzigd
        client2.Nom = "yy";
        // context opslaan
        context.SaveChanges();
      }
      // 3e context
      using (var context = new RdvMedecinsContext())
      {
        // klant2 wordt opgehaald in klant3
        client3 = context.Clients.Find(client2.Id);
        // vervolg
        Console.WriteLine("client3");
        Console.WriteLine(client3);
      }
    }
  }
}
  • regel 15: de applicatiecontext wordt geopend;
  • regels 18-25: de context wordt leeggemaakt. Om precies te zijn: alle entiteiten worden vanuit de database in de context geladen en krijgen vervolgens de status "verwijderd". Merk op dat de database in dit stadium nog niet is gewijzigd. Zolang de context niet met de database is gesynchroniseerd, verandert deze niet. We herinneren ons dat het verwijderen van de entiteiten [Medecin] en [Client] voldoende is om de database leeg te maken door middel van cascade-verwijderingen;
  • regels 27-28: er wordt een nieuwe klant aan de database toegevoegd;
  • regels 30-31: deze wordt weergegeven voordat hij in de database wordt opgeslagen;
  • regel 33: de context wordt gesynchroniseerd met de database. De entiteiten die zijn gemarkeerd als „verwijderd” zullen worden verwerkt door de bewerking SQL DELETE, de entiteit wordt toegevoegd via een bewerking SQL INSERT;
  • regels 35-36: de klant wordt weergegeven na de synchronisatie met de database;

Het resultaat op de console is als volgt:

1
2
3
4
client1--avant
Client[,xx,xx,xx,]
client1--après
Client[16,xx,xx,xx,000000132209]

Let op de volgende punten:

  • vóór de synchronisatie met de database heeft de klant noch een primaire sleutel, noch timestamp,
  • na de synchronisatie heeft hij deze wel. We herinneren er hier aan dat de primaire sleutel is geconfigureerd om door de SQL-server te worden gegenereerd. Ook genereert deze SGBD automatisch de tijdstempel;
  • regel 37: de persistentiecontext wordt gesloten. De entiteiten die deze bevatte, worden “losgekoppeld”. Ze bestaan als objecten, maar niet als entiteiten die aan een persistentiecontext zijn gekoppeld;
  • regel 39: er wordt een nieuwe, lege context gestart;
  • regel 42: de client wordt rechtstreeks uit de database opgehaald via zijn primaire sleutel. Hij wordt vervolgens in de context geplaatst. Als hij niet wordt gevonden, retourneert de methode Find de pointer null;
  • regels 48-49: deze wordt weergegeven;

Dit levert het volgende resultaat op:

client2
Client[16,xx,xx,xx,000000132209]
  • regel 47: we wijzigen het;
  • regel 49: de context wordt gesynchroniseerd met de database. EF zal detecteren dat bepaalde elementen van de context zijn gewijzigd sinds ze erin zijn geplaatst. Voor deze elementen zal het de opdrachten SQL en UPDATE in de database genereren. In dit geval bestaat de synchronisatie dus uit één enkele opdracht: UPDATE;
  • regel 50: de tweede context wordt gesloten. De entiteit client2 die aan de context was gekoppeld, wordt nu daarvan losgekoppeld;
  • regel 52: er wordt een derde, lege context geopend;
  • regel 55: de enige client van de database wordt er opnieuw naartoe gebracht. We willen zien of de wijziging die in de vorige context op de client is aangebracht, is doorgevoerd in de database;
  • regels 57-58: de klant wordt weergegeven. Dit levert het volgende resultaat op:
client3
Client[16,xx,xx,yy,000000132210]

De naam van de klant is inderdaad in de database gewijzigd. Het is interessant om op te merken dat zijn timestamp is bijgewerkt.

  • regel 59: de context wordt gesloten. Terloops zij opgemerkt dat, in tegenstelling tot de twee voorgaande keren, het niet nodig was om de context vooraf te synchroniseren met de database (SaveChanges), omdat de context niet was gewijzigd.

3.5.6. Beheer van losgekoppelde entiteiten

Laten we terugkeren naar de gelaagde architectuur van een applicatie zoals die uit de casestudy:

De laag [DAO] gebruikt ORM en EF5 om toegang te krijgen tot de gegevens. We hebben de basisbouwstenen van deze laag. Elke methode opent een persistentiecontext, voert daarin de nodige bewerkingen uit (invoegen, wijzigen, verwijderen, opvragen) en sluit deze vervolgens weer af. De entiteiten die door de laag [DAO] worden beheerd, worden doorgegeven aan de weblaag ASP.NET. In deze laag bevinden ze zich buiten de persistentiecontext en zijn ze dus losgekoppeld. In de weblaag kan een gebruiker deze entiteiten wijzigen (toevoegen, wijzigen, verwijderen). Wanneer ze terugkeren naar de laag [DAO], zijn ze nog steeds losgekoppeld. De laag [DAO] moet echter de door de gebruiker aangebrachte wijzigingen doorvoeren in de database. Deze laag zal dus moeten werken met losgekoppelde entiteiten. Laten we de drie mogelijke gevallen bekijken:

Een losstaande entiteit toevoegen

Dit is het normale geval bij een toevoeging. Het volstaat om de losgekoppelde entiteit toe te voegen (Add) aan de context, waarbij ervoor gezorgd moet worden dat deze een primaire sleutel heeft die gelijk is aan null.

Een losstaande entiteit wijzigen

Je kunt de volgende code gebruiken:

[DbContext].Entry(entité-détachée).State=EntityState.Modified ;
  • de methode [DbContext].Entry(losstaande entiteit) plaatst de entiteit in de context;
  • de status van deze entiteit wordt ingesteld op „gewijzigd”, zodat deze het voorwerp wordt van een opdracht SQL UPDATE.

Een losstaande entiteit verwijderen

Je kunt de volgende code gebruiken:

Entity e=[DbContext].[DbSet].Find(clé primaire de l'entité détachée) ;
[DbContext].[DbSet].Remove(e) ;
  • regel 1: de entiteit met dezelfde primaire sleutel als de losgekoppelde entiteit wordt in de context geplaatst;
  • regel 2: deze wordt verwijderd:

Merk op dat hiervoor in principe een SELECT gevolgd door een DELETE nodig is, terwijl normaal gesproken alleen de DELETE volstaat. Je kunt ook het voorbeeld van het wijzigen van een losstaande entiteit volgen en het volgende schrijven:

[DbContext].Entry(entité-détachée).State=EntityState.Deleted ;

Aangezien ik geen logbestanden heb kunnen genereren voor de SQL-bewerkingen die op de database zijn uitgevoerd, weet ik niet of de ene methode aan te raden is boven de andere.

Hier is een voorbeeld:

De programmacode van [ModifyDetachedEntities] is als volgt:


using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class ModifyDetachedEntities
  {
    static void Main(string[] args)
    {
      Client client1;

      // de huidige database wordt leeggemaakt
      Erase();
      // er wordt een klant toegevoegd
      using (var context = new RdvMedecinsContext())
      {
        // klant aanmaken
        client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
        // de klant wordt aan de context toegevoegd
        context.Clients.Add(client1);
        // de context opslaan
        context.SaveChanges();
      }
      // database weergeven
      Dump("1-----------------------------");
      // klant1 staat niet in de context – deze wordt aangepast
      client1.Nom = "y";
      // nieuwe context
      using (var context = new RdvMedecinsContext())
      {
        // hier hebben we een lege context
        // we plaatsen klant1 in de context in een gewijzigde status
        context.Entry(client1).State = EntityState.Modified;
        // de context wordt opgeslagen
        context.SaveChanges();
      }
      // basisweergave
      Dump("2-----------------------------");
      // entiteit buiten de context verwijderen
      using (var context = new RdvMedecinsContext())
      {
        // hier hebben we een nieuwe lege context
        // we plaatsen client1 in de context in de status 'verwijderd'
        context.Entry(client1).State = EntityState.Deleted;
        // we slaan de context op
        context.SaveChanges();
      }
      // database weergeven
      Dump("3-----------------------------");
    }

    static void Erase()
    {
      // de database leegmaken
      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);
        }
        // de context wordt opgeslagen
        context.SaveChanges();
      }
    }

    static void Dump(string str)
    {
      Console.WriteLine(str);
      // de database weergeven
      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);
        }
      }
    }
  }
}
  • regel 15: de database wordt gewist;
  • regels 17-25: er wordt een klant aan de database toegevoegd;
  • regel 27: de inhoud van de database wordt weergegeven;
1-----------------------------
Client[20,x,x,x,0000011209]
  • na regel 25 bestaat de persistentiecontext niet meer. Er zijn dus geen gekoppelde entiteiten meer. De entiteit client1 is overgegaan in de status "losgekoppeld";
  • regel 29: de naam van de losgekoppelde entiteit wordt gewijzigd;
  • regel 31: er wordt een nieuwe, lege context geopend;
  • regel 35: de losgekoppelde entiteit client1 wordt in de context geplaatst in de status "gewijzigd";
  • regel 37: de context wordt gesynchroniseerd met de database;
  • regel 38: deze wordt gesloten;
  • regel 40: de database wordt weergegeven;
2-----------------------------
Client[20,x,x,y,0000011210]

De naam van de klant is inderdaad in de database gewijzigd. Merk op dat de timestamp is bijgewerkt;

  • regel 42: een nieuwe lege context wordt geopend;
  • regel 46: de losgekoppelde entiteit client1 wordt in de context geplaatst in de status "verwijderd";
  • regel 48: de context wordt gesynchroniseerd met de database;
  • regel 49: deze wordt gesloten;
  • regel 51: de database wordt weergegeven;
3-----------------------------

De entiteit is inderdaad uit de database verwijderd.

Nu bekijken we de twee manieren waarop de afhankelijkheden van een entiteit worden geladen: Lazy en Eager Loading.

3.5.7. Lazy en Eager Loading

Laten we het schema van de ‘veel-op-één’-afhankelijkheden van een van onze vier entiteiten nog eens bekijken:

Hierboven heeft de entiteit [Creneau] een navigatie-eigenschap [Creneau.Medecin] naar de entiteit [Medecin]. Dit noemen we een afhankelijkheid. We hebben gezien dat er ook één-op-veel-afhankelijkheden bestaan. Het principe dat hier wordt uitgelegd, is ook op deze van toepassing.

Standaard staat EF 5 in de modus ‘Lazy Loading’: wanneer het een entiteit vanuit de database naar de persistentiecontext haalt, haalt het de afhankelijkheden ervan niet mee. Deze worden pas opgehaald wanneer ze voor het eerst worden gebruikt. Dit is een logische maatregel. Als dit niet het geval was, zou het ophalen van de afspraken in de context, op basis van de bovenstaande afhankelijkheden, het volgende met zich meebrengen:

  • de entiteiten [Creneau] die aan de afspraken zijn gekoppeld;
  • de entiteiten [Medecin] die aan deze tijdvakken zijn gekoppeld;
  • de entiteiten [Clients] die aan de afspraken zijn gekoppeld.

Soms is het echter nodig om zowel een entiteit als de bijbehorende afhankelijkheden op te halen. We zullen beide manieren van laden illustreren.

De code van [LazyEagerLoading] is als volgt:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;

namespace RdvMedecins_01
{
  class LazyEagerLoading
  {
    // de entiteiten
    static Medecin[] medecins;
    static Client[] clients;
    static Creneau[] creneaux;

    static void Main(string[] args)
    {
      // de database initialiseren      
      InitBase();
      Console.WriteLine("Initialisation terminée");
      // eager loading
      Creneau creneau;
      int idCreneau = (int)creneaux[0].Id;
      using (var context = new RdvMedecinsContext())
      {
        // slot nr. 0
        creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // weergave van afhankelijkheden
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
      }
      // lazy loading - standaardmodus
      using (var context = new RdvMedecinsContext())
      {
        // slot nr. 0
        creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // weergave van afhankelijkheden
      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()
    {
      // database initialiseren
      using (var context = new RdvMedecinsContext())
      {
        // de huidige database wordt geleegd
        ...
        // de database wordt geïnitialiseerd
        // de klanten
        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" }
     };
...
        // de afspraken
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // de persistentiecontext wordt opgeslagen
        context.SaveChanges();
      }
    }
  }
}
  • regel 18: we gaan uit van een bekende basis, die tot nu toe is gebruikt. Na deze bewerking worden de tabellen in de regels 11-13 gevuld met losse entiteiten;
  • regels 21-22: we richten ons op het eerste tijdslot en de bijbehorende arts;
  • regel 23: nieuwe context;
  • regel 26: we plaatsen het tijdslot in de context met zijn afhankelijkheid (eager loading). Omdat dit niet de standaardmodus is, moet deze afhankelijkheid expliciet worden opgevraagd. Dit kan met de methode Include. De parameter hiervan is de naam van de afhankelijkheid in de entiteit die in de context wordt gebracht. De query die de entiteit in de context ophaalt, maakt gebruik van lambda-expressies. Met de methode Single kan een voorwaarde worden gespecificeerd om één enkele entiteit op te halen. Hier zoeken we in de database naar de entiteit [Creneau], die de primaire sleutel van tijdslot nr. 0 heeft;
  • regel 27: de opgehaalde entiteit wordt weergegeven. Laten we de twee schrijfmethoden die in de entiteiten worden gebruikt nog eens op een rijtje zetten:

// handtekening
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
 
   // korte handtekening
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
    }
  • regels 2-5: de methode [ToString] geeft de afhankelijkheid [Medecin] weer. Als deze nog niet in de context aanwezig is, wordt er in de database naar gezocht om deze toe te voegen;
  • regels 8-11: de methode [ShortIdentity] geeft de afhankelijkheid [Medecin] niet weer. Deze wordt dus niet in de database opgezocht als deze niet in de context aanwezig is;

Op dit moment ziet de console er als volgt uit:

Initialisation terminée
Creneau[181,8,0,8,20, 21, 00000195150]
  • regel 28: de context wordt gesloten;
  • regels 30-37: er wordt geprobeerd de afhankelijkheid [Medecin] van de entiteit te schrijven. Ter herinnering: bij Lazy Loading wordt een afhankelijkheid bij het eerste gebruik geladen als deze niet aanwezig is. Hier is deze normaal gesproken wel aanwezig. De weergave is als volgt:
Médecin=Medecin[21,Mme,Marie,Pelissier,00000195149]
  • regels 39-44: in het kader van een nieuwe context wordt slot nr. 0 opnieuw in de database opgezocht en in de context geladen. Hier wordt de afhankelijkheid [Medecin] niet expliciet opgevraagd. Deze wordt dus niet geladen (Lazy Loading);
  • regel 43: de korte identificatie van het tijdslot wordt als volgt weergegeven:
Creneau[181,8,0,8,20, 21, 00000195150]

Hier is het belangrijk om ShortIdentity te gebruiken in plaats van ToString om de entiteit weer te geven. Als we ToString gebruiken, wordt de afhankelijkheid [Medecin] weergegeven en daarvoor wordt deze in de database opgezocht. Dat willen we echter niet.

  • regel 44: de context wordt gesloten;
  • regels 46-53: er wordt geprobeerd de afhankelijkheid van de entiteit weer te geven. Het is belangrijk om dit buiten de context te doen, anders wordt deze in de database opgezocht en gevonden. Hier bevinden we ons buiten de context. De entiteit [Creneau] is losgekoppeld en de bijbehorende entiteit [Medecin] ontbreekt (Lazy Loading). Wat gaat er gebeuren? De weergave op het scherm is als volgt:
L'erreur 2 suivante s'est produite : System.ObjectDisposedException: L'instance ObjectContext a été supprimée et ne peut plus être utilisée pour les opérations qui requièrent une connexion.
   à System.Data.Objects.ObjectContext.EnsureConnection()
   à System.Data.Objects.ObjectQuery`1.GetResults(Nullable`1 forMergeOption)
   à System.Data.Objects.ObjectQuery`1.Execute(MergeOption mergeOption)
   à System.Data.Objects.DataClasses.EntityReference`1.Load(MergeOption mergeOption)
   à System.Data.Objects.DataClasses.RelatedEnd.Load()
   à System.Data.Objects.DataClasses.RelatedEnd.DeferredLoad()
   à System.Data.Objects.Internal.LazyLoadBehavior.LoadProperty[TItem](TItem propertyValue, String relationshipName, String targetRoleName, Boolean mustBeNull,Object wrapperObject)
   à System.Data.Objects.Internal.LazyLoadBehavior.<>c__DisplayClass7`2.<GetInterceptorDelegate>b__2(TProxy proxy, TItem item)
   à System.Data.Entity.DynamicProxies.Creneau_AF14A89855AD9B7E5ABA4A877B4989B2F8B3F7ECA154E3FEC02BA722002773E4.get_Medecin()
   à RdvMedecins_01.LazyEagerLoading.Main(String[] args) dans d:\data\istia-1213\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\LazyEagerLoading.cs:regel 48

EF heeft vastgesteld dat de afhankelijkheid [Medecin] ontbreekt. Het probeerde deze te laden, maar omdat de context gesloten was, was deze bewerking niet meer mogelijk. We onthouden deze uitzondering [System.ObjectDisposedException], omdat deze kenmerkend is voor het laden van een afhankelijkheid buiten een geopende context.

Laten we nu eens kijken naar de concurrente toegang tot entiteiten.

3.5.8. Concurrentie bij toegang tot entiteiten

Laten we terugkeren naar de definitie van de entiteit [Client]:


public class Client
  {
    // gegevens
    [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; }
    // de Rvs van de klant
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // handtekening
    ...
  }

We zullen ons richten op het veld [Timestamp] op regel 23. We weten dat de waarde ervan wordt gegenereerd door SGBD. We hebben ook gezegd dat de annotatie [Timestamp] op regel 22 ervoor zorgde dat EF 5 het geannoteerde veld gebruikte om concurrerende toegang tot entiteiten te beheren. Laten we nog eens herhalen wat het beheer van concurrerende toegang inhoudt:

  • een proces P1 leest een rij L uit de tabel [MEDECINS] op tijdstip T1. De rij heeft de timestamp TS1;
  • een proces P2 leest dezelfde rij L uit de tabel [MEDECINS] op tijdstip T2. De rij heeft de waarden timestamp en TS1 omdat het proces P1 zijn wijziging nog niet heeft gevalideerd;
  • het proces P1 valideert zijn wijziging van regel L. De status van regel L verandert dan van timestamp naar TS2;
  • het proces P2 valideert zijn wijziging van regel L. HetORM genereert vervolgens een uitzondering omdat het proces P2 een timestamp TS1 van regel L heeft die verschilt van de timestamp TS2 in de database.

Dit wordt optimistisch beheer van gelijktijdige toegang genoemd. Met EF 5 moet een veld dat deze rol vervult een van de twee attributen [Timestamp] of [ConcurrencyCheck] hebben. SQL server heeft het type [timestamp]. Bij elke invoeging of wijziging van een rij wordt de waarde van een kolom met dit type automatisch gegenereerd door de SQL-server. Een dergelijke kolom kan vervolgens worden gebruikt om gelijktijdige toegang te beheren.

We zullen deze gelijktijdige toegang illustreren met twee threads die tegelijkertijd dezelfde entiteit [Client] in de database wijzigen. Het project verloopt als volgt:

De programmacode van [AccèsConcurrents] is als volgt:


using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // object uitgewisseld met de threads
  class Data
  {
    public int Duree { get; set; }
    public string Nom { get; set; }
    public Client Client { get; set; }
  }

  // testprogramma
  class AccèsConcurrents
  {

    static void Main(string[] args)
    {
      Client client1;
      using (var context = new RdvMedecinsContext())
      {
        // hoofdthread
        Thread.CurrentThread.Name = "main";
        // de huidige database wordt leeggemaakt
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // er wordt een client toegevoegd
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // opvolging
        Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
        // back-up
        context.SaveChanges();
        // opvolging
        Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
      }
      // we gaan klant1 wijzigen met twee threads
      // thread 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 wachten tot de 2 threads zijn voltooid
      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);
      // we geven de wijziging weer – er kan er maar één gelukt zijn
      using (var context = new RdvMedecinsContext())
      {
        // we halen client1 op in 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)
    {
 ...
}
  • regel 26: er wordt een lege context gestart;
  • regel 29: de huidige thread krijgt een naam om deze te onderscheiden van de twee threads die later zullen worden aangemaakt;
  • regels 31-38: de entiteiten [Medecin] en [Client] worden in de status "verwijderd" gezet;
  • regels 40-41: er wordt een klant aan de context toegevoegd;
  • regels 43-44: deze wordt weergegeven vóór de synchronisatie van de context;
  • regel 46: synchronisatie van de context met de database: entiteiten met de status "verwijderd" worden uit de database verwijderd. De entiteit [Client] die in de context is geplaatst, wordt in de database ingevoegd. Dit zal het enige element in de database zijn;
  • regels 47-49: de client wordt weergegeven na synchronisatie van de context. Op dit moment zien de schermweergaven er als volgt uit:
1
2
3
4
main client1--avant sauvegarde du contexte
Client[,xx,xx,xx,]
main client1--après sauvegarde du contexte
Client[33,xx,xx,xx,000001126209]

Merk op dat de klant na synchronisatie van de context een primaire sleutel en een timestamp heeft;

  • regel 50: de context wordt gesloten;
  • regel 53: er wordt een thread t1 gekoppeld aan de methode [Modifie] uit regel 84. Dit betekent dat wanneer deze wordt gestart, hij de methode [Modifie] zal uitvoeren;
  • regel 54: de thread t1 krijgt een naam;
  • regel 55: de thread t1 wordt gestart. Er worden parameters aan doorgegeven in de vorm van een structuur [Data] die is gedefinieerd in de regels 12-17:
    • Duur: de thread stopt Durée seconden voordat de uitvoering is voltooid,
    • Klant: een verwijzing naar de klant die in de database moet worden bijgewerkt,
    • Naam: de naam die aan deze klant moet worden gegeven;
  • regels 57-59: hetzelfde geldt voor een tweede thread. Uiteindelijk zullen twee threads proberen de naam van dezelfde klant in de database te wijzigen;
  • regels 60-63: nadat de twee threads zijn gestart, wacht de hoofdthread tot ze zijn voltooid;
  • regel 62: wachten op het einde van thread t1;
  • regel 63: wachten op het einde van thread t2;
  • regel 64: we weten niet in welke volgorde de twee threads zullen eindigen. Wat zeker is, is dat ze in regel 64 zijn voltooid;
  • regels 66-72: in een nieuwe context wordt de klant uit de database opgehaald om te zien in welke status hij zich bevindt.

Laten we nu eens kijken wat de twee threads t1 en t2 doen. Ze voeren de volgende methode [Modifie] uit:


static void Modifie(object infos)
    {
      // de parameter wordt opgehaald
      Data data = (Data)infos;
      try
      {
        using (var context = new RdvMedecinsContext())
        {
          Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
          // we halen client1 op in 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());
          // we wijzigen client2
          client2.Nom = data.Nom;
          // even wachten
          Thread.Sleep(data.Duree);
          // de wijzigingen worden opgeslagen
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        // uitzondering
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
      }
      // einde van de thread
      Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
    }
  • regel 4: de parameters van de thread (Duur, Naam, Klant) worden opgehaald;
  • regel 7: nieuwe context;
  • regel 11: de klant wordt in de context geplaatst;
  • regels 12-13: controle om de status van de klant te bekijken;
  • regel 15: de naam wordt gewijzigd;
  • regel 17: de thread stopt Duree milliseconden. Dit heeft een interessant effect. De thread geeft de processor vrij die hem uitvoerde, waardoor er ruimte ontstaat voor een andere thread. In ons voorbeeld hebben we drie threads: main, t1 en t2. De thread main staat stil en wacht tot de threads t1 en t2 zijn voltooid. Ervan uitgaande dat thread t1 als eerste de processor in bezit heeft, draagt hij deze nu over aan thread t2. Dit heeft tot gevolg dat thread t2 precies hetzelfde leest als thread t1: dezelfde klant met dezelfde timestamp;
  • regel 19: de context wordt gesynchroniseerd met de database. Laten we opnieuw aannemen dat thread t1 als eerste weer actief wordt. Hij zal de klant met de naam „yy“ opslaan. Hij kan dit doen omdat hij dezelfde timestamp heeft als in de database. Door deze update zal de SGBD de timestamp wijzigen. Wanneer thread t2 op zijn beurt ontwaakt, zal hij een klant hebben met een timestamp die verschilt van degene die nu in de database staat. Zijn update zal worden geweigerd.

De schermweergaven zijn als volgt:

main client1--vóór het opslaan van de context
Client[,xx,xx,xx,]
main client1--na het opslaan van de context
Client[33,xx,xx,xx,000001126209]
Thread main -- begin wachten op einde van beide threads
Début Thread t1
Début Thread t2
Thread t2 client2
Thread t2 Client[33,xx,xx,xx,000001126209]
Thread t1 client2
Thread t1 Client[33,xx,xx,xx,000001126209]
Fin Thread t2
Thread t1 System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: Une instruction de mise à jour, d'insertion ou de suppression dans le magasin a affecté un nombre inattendu de lignes (0). Des entités ont peut-être é modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager. ---> System.Data.OptimisticConcurrencyException: Een instructie voor het bijwerken, invoegen of verwijderen in de opslag heeft een onverwacht aantal rijen (0) beïnvloed. Mogelijk zijn entiteiten gewijzigd of verwijderd sinds hun karakter
gement. Actualisez les entrées ObjectStateManager.
   à System.Data.Mapping.Update.Internal.UpdateTranslator.ValidateRowsAffected(I
nt64 rowsAffected, UpdateCommand source)
   ...
   à RdvMedecins_01.AccèsConcurrents.Modifie(Object infos) dans d:\data\istia-12
13\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\AccèsConcurrents
.cs:ligne 102
Fin Thread t1
Thread main -- einde wachtrij, einde van beide threads
Thread main client2
Thread main Client[33,xx,xx,zz,000001126210]
  • regel 4: de client in de database;
  • regel 9: de klant zoals deze door thread t2 wordt gelezen;
  • regel 11: de klant zoals deze wordt gelezen door thread t1. Beide threads hebben dus hetzelfde gelezen;
  • regel 12: thread t2 eindigt als eerste. Deze heeft dus de update kunnen uitvoeren. De naam moet zijn veranderd in "zz";
  • regel 13: thread t1 genereert een uitzondering van het type [System.Data.OptimisticConcurrencyException]. EF heeft vastgesteld dat hij niet de juiste timestamp had;
  • regel 21: de thread t1 wordt op zijn beurt beëindigd;
  • regel 22: de hoofdthread is klaar met wachten;
  • regel 24: de hoofdthread geeft de klant in de database weer. Het is inderdaad thread t2 die gewonnen heeft. De naam is "zz". Merk op dat de timestamp is veranderd.

Laten we nu een ander aspect bekijken: de transactie die de synchronisatie van de persistentiecontext met de database omvat.

3.5.9. Synchronisatie binnen een transactie

De tabel [CRENEAUX] heeft een uniekheidsbeperking die we handmatig hebben toegevoegd (zie paragraaf 2.2.4, pagina 12):

ALTER TABLE RV ADD CONSTRAINT UNQ1_RV UNIQUE (JOUR, ID_CRENEAU);

We gaan als volgt te werk: we voegen tegelijkertijd twee afspraken toe voor dezelfde arts, op dezelfde dag en in hetzelfde tijdslot. We zullen zien wat er gebeurt.

Het project ontwikkelt zich als volgt:

De programmacode [SynchronisationTransaction] is als volgt:


using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{

  // testprogramma
  class SynchronisationTransaction
  {

    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // de huidige database wordt leeggemaakt
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        context.SaveChanges();
      }

      // er wordt een klant aangemaakt
      Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // een arts aanmaken
      Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // er wordt een tijdvak aangemaakt voor deze arts
      Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
      // er worden twee afspraken aangemaakt voor deze arts en deze klant, op dezelfde dag, in hetzelfde tijdslot
      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 plaatsen al deze gegevens in de persistentiecontext
        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);
          // we slaan de context op – er moet een uitzondering optreden
          // omdat de onderliggende BD een uniekheidsbeperking heeft die voorkomt
          // dat er twee RDV op dezelfde dag en in hetzelfde tijdslot voorkomen
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("Erreur : {0}", e);
      }
      // als het opslaan plaatsvindt binnen een transactie, dan mag er niets in de database zijn ingevoerd
      // vanwege de vorige uitzondering – we controleren

      using (var context = new RdvMedecinsContext())
      {
        // de klanten
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // de artsen
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // de tijdvakken
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // de afspraken
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}
  • regels 15-27: er wordt een persistentiecontext gebruikt om de database te legen;
  • regel 30: aanmaken van een object [Client];
  • regel 32: aanmaken van een object [Medecin];
  • regel 34: aanmaken van een object [Creneau];
  • regel 36: aanmaken van een object [Rv];
  • regel 37: aanmaken van een tweede object [Rv], identiek aan het vorige;
  • regel 41: een nieuwe context openen;
  • regels 43-47: de eerder aangemaakte objecten worden aan de nieuwe context gekoppeld. Merk hierbij op dat we, rekening houdend met de afhankelijkheden, het aantal bewerkingen Add hadden kunnen minimaliseren. Maar EF zal de opdrachten SQL en INSERT die naar de database moeten worden verzonden, optimaliseren;
  • regel 51: de context wordt gesynchroniseerd met de database. Zoals de opmerking aangeeft, moet het invoegen van een van de twee afspraken mislukken vanwege de uniekheidsbeperking op de tabel [RVS]. Maar meer nog: als de synchronisatie plaatsvindt binnen een transactie, moet alles ongedaan worden gemaakt. Er mag dus geen invoeging plaatsvinden. De database moet leeg blijven;
  • regel 53: de context wordt gesloten;
  • regels 61-90: weergave van de inhoud van de database. Deze moet leeg zijn.

De schermweergave is als volgt:

Erreur : System.Data.Entity.Infrastructure.DbUpdateException: Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultezl'exception interne. ---> System.Data.UpdateException: Er is een fout opgetreden bij het bijwerken van de gegevens. Raadpleeg de interne uitzondering voor meer informatie. ---> System.Data.SqlClient.SqlException: Overtreding van de beperking UNIQUE KEY „RVS_uq”. Kan geen dubbele sleutel invoegen in het object „dbo.RVS”. Dubbele sleutelwaarde: (18 okt 2012 12:00AM, 34).
L'instruction a é arrêtée.
   à System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   à System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)...
    --- Einde van de interne uitzonderingsstapel ---
   ...
   à System.Data.Entity.DbContext.SaveChanges()
   à RdvMedecins_01.SynchronisationTransaction.Main(String[] args) dans d:\data\istia-1213\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-SqlServer-01\SynchronisationTransaction.cs:regel 59
Clients--------------------------------------
Médecins--------------------------------------
Créneaux horaires--------------------------------------
Rendez-vous--------------------------------------
  • regel 1: uitzondering vanwege schending van de uniekheidsbeperking op de tabel [RVS];
  • regels 9-12: de database is inderdaad leeg. De synchronisatie van de context met de database heeft dus plaatsgevonden binnen een transactie.

Er zijn ongetwijfeld nog andere aspecten te onderzoeken in EF 5. Maar we weten nu genoeg om terug te keren naar onze studie van een meerlaagse architectuur. Aan het begin van dit document vindt de lezer verwijzingen naar artikelen en boeken waarmee hij zijn kennis van EF 5 kan verdiepen.

3.6. Onderzoek naar een meerlaagse architectuur op basis van EF 5

We keren terug naar onze casestudie die in paragraaf 2 is beschreven. Het betreft een webapplicatie ASP.NET die als volgt is gestructureerd:

We beginnen met het opbouwen van de [DAO]-laag voor gegevenstoegang. Deze laag zal gebaseerd zijn op EF5.

3.6.1. Het nieuwe project

We maken een nieuw consoleproject VS 2012 [RdvMedecins-SqlServer-02] aan in de huidige oplossing [1]:

We voegen vier mappen toe met de naam [2], waarin we onze codes gaan indelen. De map [Entites] is een kopie van de map [Entites] uit het vorige project. Na deze kopie verschijnen er fouten omdat we niet over de juiste verwijzingen beschikken. We moeten een verwijzing naar Entity Framework 5 toevoegen. Hiervoor volgen we de methode die wordt uitgelegd in paragraaf 3.4, pagina 21. De lijst met verwijzingen ziet er als volgt uit: [3]:

In dit stadium zou het project geen compilatiefouten meer moeten vertonen. Uit het vorige project kopiëren we ook het bestand [App.config], dat de verbinding met de database configureert:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Ga voor meer informatie over de configuratie van Entity Framework naar 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>

  <!-- verbindingsstring voor de database -->
  <connectionStrings>
    <add name="monContexte"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- de 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. De klasse Exception

We gaan een projectspecifieke uitzonderingsklasse gebruiken. Deze wordt gegenereerd door de laag [DAO]:

De laag [DAO] zal alle uitzonderingen die naar deze laag worden doorgegeven, opvangen en deze in een uitzondering van het type [RdvMedecinsException] inkapselen. Deze uitzondering ziet er als volgt uit:


using System;

namespace RdvMedecins.Exceptions
{
  public class RdvMedecinsException : Exception
  {

    // eigenschappen
    public int Code { get; set; }

    // constructors
    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;
    }

    // identiteit
    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);
      }
    }
  }
}
  • regel 5: de klasse is afgeleid van de klasse [Exception];
  • regel 9: de klasse voegt een foutcode toe aan de basisklasse;
  • regels 12-32: de verschillende constructors bevatten het veld [Code].

Het project ontwikkelt zich als volgt:

3.6.3. De laag [DAO]

De laag [DAO] biedt een interface naar de laag [ASP.NET]. Om deze te identificeren, moet u de webpagina's van de applicatie bekijken:

  • in [1] hierboven is de vervolgkeuzelijst gevuld met de lijst van artsen. De laag [DAO] zal deze lijst leveren;
  • in [2] levert de laag [DAO];
  • de lijst met afspraken van een arts voor die dag,
  • de lijst met beschikbare tijdvakken van een arts,
  • aanvullende informatie over de geselecteerde arts;
  • in [3] wordt de vervolgkeuzelijst met klanten geleverd door de laag [DAO];
  • in [4] bevestigt de gebruiker een afspraak. De laag [DAO] moet deze aan de database kunnen toevoegen. Deze laag moet ook aanvullende informatie over de geselecteerde klant kunnen verstrekken;
  • in [5] verwijdert de gebruiker een afspraak. De laag [DAO] moet dit mogelijk maken.

Met deze informatie zou de interface [IDao] van de laag [DAO] er als volgt uit kunnen zien:


using System;
using System.Collections.Generic;
using RdvMedecins.Entites;

namespace RdvMedecins.Dao
{
  public interface IDao
  {
    // klantenlijst
    List<Client> GetAllClients();
    // lijst met artsen
    List<Medecin> GetAllMedecins();
    // lijst met tijdvakken van een arts
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // lijst met RV van een bepaalde arts op een bepaalde dag
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // een RV toevoegen
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // een RV verwijderen
    void SupprimerRv(int idRv);
    // een entiteit T opzoeken via de primaire sleutel
    T Find<T>(int id) where T : class;
  }
}

De methoden in de regels 10-20 vloeien voort uit de zojuist uitgevoerde analyse. De methode in regel 22 is bedoeld om rekening te houden met het feit dat er met Lazy Loading wordt gewerkt. Als er in de laag [ASP.NET] een afhankelijkheid van een entiteit nodig is, wordt deze met deze methode uit de database opgehaald.

De implementatie [Dao] van deze interface ziet er als volgt uit:


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
  {

    //lijst met klanten
    public List<Client> GetAllClients()
    {
      // lijst met klanten
      List<Client> clients = null;
      try
      {
        // persistentiecontext openen
        using (var context = new RdvMedecinsContext())
        {
          // lijst met klanten
          clients = context.Clients.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(1, "GetAllClients", ex);
      }
      // het resultaat wordt weergegeven
      return clients;
    }

    // lijst met artsen
    public List<Medecin> GetAllMedecins()
    {
      // lijst met artsen
      List<Medecin> medecins = null;
      try
      {
        // persistentiecontext openen
        using (var context = new RdvMedecinsContext())
        {
          // lijst met artsen
          medecins = context.Medecins.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(2, "GetAllMedecins", ex);
      }
      // het resultaat wordt weergegeven
      return medecins;
    }

    // lijst met beschikbare tijdvakken van een bepaalde arts
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
   ...
    }

    // lijst met RV van een arts voor een bepaalde dag
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
 ...
    }

    // een RV toevoegen
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
 ...
    }

    // een RV verwijderen
    public void SupprimerRv(int idRv)
    {
...
    }

    // een klant zoeken
    public Client FindClient(int id)
    {
...
    }

    // een tijdvak zoeken
    public Creneau FindCreneau(int id)
    {
 ...
    }

    // een arts zoeken
    public Medecin FindMedecin(int id)
    {
....
    }

    // een afspraak zoeken
    public Rv FindRv(int id){
...
    }

  }
}

Laten we de methode [GetAllClients] toelichten, die de lijst met alle klanten moet retourneren:

  • regels 18-31: het zoeken naar klanten gebeurt in een try/catch-blok. Dit geldt ook voor alle volgende methoden;
  • regel 21: een nieuwe context wordt geopend;
  • regel 24: de entiteiten [Client] worden in de context geladen en in een lijst geplaatst.

De methode [GetAllMedecins], die de lijst met alle artsen moet retourneren, is vergelijkbaar (regels 37-57).

De methode [GetCreneauxMedecin] is als volgt:


// overzicht van de beschikbare tijdvakken van een bepaalde arts
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
      // lijst met beschikbare tijdvakken
      try
      {
        // context voor persistentie openen
        using (var context = new RdvMedecinsContext())
        {
          // de arts met zijn beschikbare tijdvakken ophalen
          Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
          // lijst met tijdvakken van de arts
          return medecin.Creneaux.ToList<Creneau>();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
      }
    }
  • regel 9: een nieuwe persistentiecontext openen;
  • regel 11: zoeken naar de arts waarvan de primaire sleutel bekend is. Er wordt gevraagd om de afhankelijkheid [Creneaux], een verzameling van de beschikbare tijdvakken van de arts, hierin op te nemen. Als de arts niet bestaat, genereert de methode Single een uitzondering;
  • regel 13: de lijst met beschikbare tijdvakken wordt geretourneerd.

De methode [GetRvMedecinJour] moet de lijst met afspraken van een arts voor een bepaalde dag retourneren. De code zou er als volgt uit kunnen zien:


// lijst met RV van een arts voor een bepaalde dag
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
      // lijst met afspraken
      List<Rv> rvs = null;

      try
      {
        // context voor persistentie openen
        using (var context = new RdvMedecinsContext())
        {
          // de arts wordt opgehaald
          Medecin medecin = context.Medecins.Find(idMedecin);
          if (medecin == null)
          {
            throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
          }
          // lijst met afspraken
          rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
      }
      // het resultaat wordt teruggestuurd
      return rvs;
    }
  • regel 13: de arts waarvan de primaire sleutel bekend is, wordt in de context opgehaald;
  • regels 14-17: als deze niet bestaat, wordt er een uitzondering gegenereerd;
  • regel 19: de query LINQ wordt uitgevoerd om de afspraken voor deze arts op te halen;

De methode [AjouterRv] moet een afspraak aan de database toevoegen en de primaire sleutel van het ingevoegde element retourneren. De code zou er als volgt uit kunnen zien:


// een RV toevoegen
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
      // nummer van de toegevoegde afspraak
      int idRv;
      try
      {
        // context voor persistentie openen
        using (var context = new RdvMedecinsContext())
        {
          // het tijdvak wordt opgehaald
          Creneau creneau = context.Creneaux.Find(idCreneau);
          if (creneau == null)
          {
            throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
          }
          // de klant wordt opgehaald
          Client client = context.Clients.Find(idClient);
          if (client == null)
          {
            throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
          }
          // tijdvak aangemaakt
          Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
          // toevoeging aan de context
          context.Rvs.Add(rv);
          // context opslaan
          context.SaveChanges();
          // de primaire sleutel van de toegevoegde afspraak ophalen
          idRv = (int)rv.Id;
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(7, "AjouterRv", ex);
      }
      // resultaat
      return idRv;
    }
  • regel 12: we zoeken het tijdvak van de afspraak in de database;
  • regels 13-16: als deze niet wordt gevonden, wordt er een uitzondering gegenereerd;
  • regel 18: de klant van de afspraak wordt in de database opgezocht;
  • regels 19-22: als deze niet wordt gevonden, wordt er een uitzondering gegenereerd;
  • regel 24: er wordt een object [Rv] aangemaakt met de benodigde informatie;
  • regel 26: dit wordt toegevoegd aan de persistentiecontext;
  • regel 28: we synchroniseren de persistentiecontext met de database. De afspraak wordt dan in de database opgeslagen;
  • regel 30: we weten dat na synchronisatie met de database de primaire sleutels van de ingevoegde elementen beschikbaar zijn. We halen die van de toegevoegde afspraak op;
  • regel 31: de persistentiecontext wordt gesloten.

De methode [SupprimerRv] moet een afspraak verwijderen waarvan de primaire sleutel wordt doorgegeven.


// een RV verwijderen
    public void SupprimerRv(int idRv)
    {
      try
      {
        // persistentiecontext openen
        using (var context = new RdvMedecinsContext())
        {
          // de Rv wordt opgehaald
          Rv rv = context.Rvs.Find(idRv);
          if (rv == null)
          {
            throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
          }
          // Rv verwijderen
          context.Rvs.Remove(rv);
          // context opslaan
          context.SaveChanges();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(8, "SupprimerRv", ex);
      }
    }
  • regel 7: nieuwe persistentiecontext;
  • regel 10: de afspraak die moet worden verwijderd, wordt in de context geplaatst;
  • regels 11-15: als deze niet bestaat, wordt er een uitzondering gegenereerd;
  • regel 16: de afspraak wordt uit de context verwijderd;
  • regel 18: de context wordt gesynchroniseerd met de database;
  • regel 19: de context wordt gesloten.

Met de methode [Find<T>] kan in de database een entiteit van het type T worden opgezocht aan de hand van de primaire sleutel. De code zou er als volgt uit kunnen zien:


public T Find<T>(int id)  where T : class
    {
      try
      {
        // persistentiecontext openen
        using (var context = new RdvMedecinsContext())
        {
          return context.Set<T>().Find(id);
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(20, "Find<T>", ex);
      }
    }
  • regel 8: met de methode Set<T> haalt men een DbSet<T> op, waarop men de gebruikelijke methoden kan toepassen.

Het project ontwikkelt zich als volgt:

3.6.4. Testen van de [DAO]-laag

We gaan een testprogramma maken voor de laag [DAO]. De architectuur van de test zal als volgt zijn:

Een consoleprogramma vraagt [Spring.net] om de laag [DAO] te instantiëren. Zodra dit is gebeurd, test het de verschillende functionaliteiten van de interface van de laag [DAO]. In plaats van een consoleprogramma zou het beter zijn geweest om een testprogramma van het type NUnit te schrijven. Een testprogramma voor de laag [DAO] zou er als volgt uit kunnen zien:


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
      {
        // instantiëren van de laag [DAO] via Spring
        dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

        // klanten weergeven
        List<Client> clients = dao.GetAllClients();
        DisplayClients("Liste des clients :", clients);

        // weergave van artsen
        List<Medecin> medecins = dao.GetAllMedecins();
        DisplayMedecins("Liste des médecins :", medecins);

        // lijst met tijdvakken van arts nr. 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);

        // lijst met afspraken van een arts voor een bepaalde dag
        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)));

        // een RV toevoegen aan arts nr. 1 in tijdvak nr. 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)));

        // een afspraak toevoegen aan een reeds bezet tijdvak – dit moet een uitzondering veroorzaken
        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));
        }

        // een afspraak verwijderen
        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));
      }
      //pauze 
      Console.ReadLine();
    }

    // hulpprogramma's – geeft lijsten weer
    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)
    {
...
    }
  }
}
  • regel 14: de verwijzing naar de laag [DAO]. Om de test onafhankelijk te maken van de daadwerkelijke implementatie ervan, is deze verwijzing van het type van de interface [IDao] en niet van het type van de klasse [Dao];
  • regel 18: de laag [DAO] wordt door Spring geïnstantieerd. We komen later terug op de configuratie die hiervoor nodig is. We casten de door Spring geretourneerde objectreferentie naar een referentie van het type van de interface [IDao];
  • regels 21-22: geven de klanten weer;
  • regels 25-26: geven de artsen weer;
  • regels 29-30: geven de lijst met tijdvakken van arts nr. 0 weer;
  • regel 33: geeft de afspraken van arts nr. 0 weer op 23/11/2013. Er zouden er geen moeten zijn;
  • regel 37: voegt een afspraak toe voor arts nr. 0 op 23/11/2013;
  • regel 39: toont de afspraken van arts nr. 0 op 23/11/2013. Er moet er één zijn;
  • regel 46: dezelfde afspraak wordt een tweede keer toegevoegd. Er moet een uitzondering optreden;
  • regel 57: de enige toegevoegde afspraak wordt verwijderd;
  • regel 58: toont de afspraken van arts nr. 0 op 23/11/2013. Er mogen er geen zijn.

3.6.5. Configuratie van Spring.net

In het bovenstaande testprogramma zijn we snel voorbijgegaan aan de instructie die de laag [DAO] instantiëert:


dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

De klasse [ContextRegistry] is een Spring-klasse in de naamruimte [Spring.Context.Support]. Om Spring te kunnen gebruiken, moeten we de bijbehorende DLL toevoegen aan de projectreferenties. We gaan als volgt te werk:

  • in [1] zoeken we naar pakketten met de tool [NuGet];
  • in [2] zoeken we online naar pakketten;
  • in [3] voer je het trefwoord spring in het zoekveld in;
  • in [4] worden de pakketten weergegeven waarvan de beschrijving dit trefwoord bevat. Hier is [Spring.Core] wat we zoeken. We installeren het.

De projectreferenties veranderen als volgt:

Het pakket [Spring.Core] was afhankelijk van het pakket [Common.Logging]. Dit is ook gedownload. In dit stadium zou het project geen fouten meer moeten vertonen.

Dat betekent echter nog niet dat het zal werken. We moeten eerst Spring configureren in het bestand [App.config]. Dit is het meest delicate deel van het project. Het nieuwe bestand [App.config] ziet er als volgt uit:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Ga voor meer informatie over de configuratie van Entity Framework naar 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>
    <!-- algemene logboekregistratie-->
    <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>
  <!-- verbindingsstrings -->
  <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-configuratie -->
  <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>
  <!-- configuratie 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>

Laten we beginnen met alles te verwijderen wat al bekend is: Entity Framework, verbindingsstrings, ProviderFactory. Het bestand ziet er dan als volgt uit:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Ga voor meer informatie over de Entity Framework-configuratie naar 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>
    <!-- algemene logboekregistratie-->
    <sectionGroup name="common">
      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
    </sectionGroup>
  </configSections>
...
  <!-- Spring-configuratie -->
  <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>
  <!-- configuratie 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>
  • regels 3-15: definiëren configuratiesecties;
  • regel 8: definieert de klasse die de sectie <spring><context> van het bestand XML (regels 19-21) zal beheren;
  • regel 9: definieert de klasse die de sectie <spring><objects> van het bestand XML (regels 22-24) zal beheren;
  • regel 13: definieert de klasse die de sectie <common><logging> van het bestand XML (regels 27-36) zal beheren;
  • regels 7-14: zijn stabiel. Hoeven in een ander project niet te worden gewijzigd;
  • regels 18-25: Spring-configuratie. Is stabiel, behalve de regels 22-24 die de objecten definiëren die Spring zal instantiëren;
  • regel 23: definitie van een object. Het attribuut id is vrij te kiezen. Dit is de identificatiecode van het object. Het attribuut type geeft de klasse aan die moet worden geïnstantieerd in de vorm „volledige naam van de klasse, Assembly die de klasse bevat”. De klasse hier is degene die de laag [DAO] implementeert: [RdvMedecins.Dao.Dao]. Om de bijbehorende assembly te achterhalen, moet je de projecteigenschappen bekijken:

In [1] staat de naam van de assembly die moet worden opgegeven;

  • regels 27-36: de configuratie van "Common Logging" is stabiel. Het kan nodig zijn om het informatieniveau aan te passen, regel 32. Na de debugfase kan het niveau worden gewijzigd naar INFO.

Al met al blijkt het Spring-configuratiebestand, dat op het eerste gezicht complex lijkt, eenvoudig te zijn. Het enige wat u hoeft te wijzigen, zijn:

  • de regels 22-24, die de te instantiëren objecten definiëren;
  • regel 32: het logniveau.

In het testprogramma is de instructie die de laag [DAO] instantiëert als volgt:


dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

[ContextRegistry] is een Spring-klasse die gebruikmaakt van de Spring-configuratie die is vastgelegd in een bestand [Web.config] of [App.config]. In dit geval maakt de klasse gebruik van de volgende sectie uit het bestand [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() maakt gebruik van de context van de regels 2-4. Regel 3 betekent dat de Spring-objecten zijn gedefinieerd in de sectie [spring/objects] van het configuratiebestand. Deze sectie bestaat uit de regels 5-7;
  • ContextRegistry.GetContext().GetObject("rdvmedecinsDao") maakt gebruik van de sectie op de regels 5-7. Het retourneert een verwijzing naar het object met het attribuut id= "rdvmedecinsDao". Dit is het object dat in regel 6 is gedefinieerd. Spring zal vervolgens de klasse instantiëren die is gedefinieerd door het attribuut type, met behulp van de constructor zonder parameters. Deze constructor moet dus bestaan. Zodra dit is gebeurd, wordt de verwijzing naar het aangemaakte object teruggegeven aan de aanroepende code. Als het object een tweede keer in de code wordt opgevraagd, geeft Spring gewoon een verwijzing naar het eerste aangemaakte object terug. Dit is het ontwerppatroon (Design Pattern) dat ‘singleton’ wordt genoemd.

Het aanmaken van het object kan complexer zijn. Men kan een constructor met parameters gebruiken of de initialisatie van bepaalde velden van het object specificeren zodra dit is aangemaakt. Voor meer informatie over dit onderwerp kun je het artikel "Spring-tutorial IOC voor .NET" lezen, op de pagina URL [http://tahe.developpez.com/dotnet/springioc/].

Zodra dit is gebeurd, kunnen we de applicatie uitvoeren. De schermresultaten zijn als volgt:

Liste des clients :
Client[35,Mr,Jules,Martin,00000118981]
Client[36,Mme,Christine,German,00000118982]
Client[37,Mr,Jules,Jacquard,00000118983]
Client[38,Melle,Brigitte,Bistrou,00000118984]
Liste des médecins :
Medecin[26,Mme,Marie,Pelissier,00000118985]
Medecin[27,Mr,Jacques,Bromard,000001189110]
Medecin[28,Mr,Philippe,Jandot,000001189123]
Medecin[29,Melle,Justine,Jacquemot,000001189124]
Liste des créneaux horaires du médecin Medecin[26,Mme,Marie,Pelissier,00000118985]
Creneau[218,8,0,8,20, 26, 00000118986]
Creneau[219,8,20,8,40, 26, 00000118987]
Creneau[220,8,40,9,0, 26, 00000118988]
Creneau[221,9,0,9,20, 26, 00000118989]
Creneau[222,9,20,9,40, 26, 00000118990]
Creneau[223,9,40,10,0, 26, 00000118991]
Creneau[224,10,0,10,20, 26, 00000118992]
Creneau[225,10,20,10,40, 26, 00000118993]
Creneau[226,10,40,11,0, 26, 00000118994]
Creneau[227,11,0,11,20, 26, 00000118995]
Creneau[228,11,20,11,40, 26, 00000118996]
Creneau[229,11,40,12,0, 26, 00000118997]
Creneau[230,14,0,14,20, 26, 00000118998]
Creneau[231,14,20,14,40, 26, 00000118999]
Creneau[232,14,40,15,0, 26, 000001189100]
Creneau[233,15,0,15,20, 26, 000001189101]
Creneau[234,15,20,15,40, 26, 000001189102]
Creneau[235,15,40,16,0, 26, 000001189103]
Creneau[236,16,0,16,20, 26, 000001189104]
Creneau[237,16,20,16,40, 26, 000001189105]
Creneau[238,16,40,17,0, 26, 000001189106]
Creneau[239,17,0,17,20, 26, 000001189107]
Creneau[240,17,20,17,40, 26, 000001189108]
Creneau[241,17,40,18,0, 26, 000001189109]
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :
Ajout d'un RV au médecin Medecin[26,Mme,Marie,Pelissier,00000118985] avec client  Client[35,Mr,Jules,Martin,00000118981] le 23/11/2013
Rdv ajouté
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :
Rv[28,23/11/2013 00:00:00,35,218,00000289145]
Ajout d'un RV dans un créneau déjà occupé
L'erreur suivante s'est produite : RdvMedecinsException[7,AjouterRv,Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultez l'exception interne.]
Suppression du RV n° 28
Liste des RV du médecin Medecin[26,Mme,Marie,Pelissier,00000118985], le 23/11/2013 :

De resultaten komen overeen met wat we hadden verwacht. We gaan er nu vanuit dat onze laag [DAO] geldig is. De tutorial zou hier kunnen eindigen. Tot nu toe hebben we het volgende laten zien:

  • de basisprincipes van Entity Framework 5;
  • een [DAO]-laag die gebruikmaakt van deze ORM.

Laten we even terugkijken naar onze casestudy die aan het begin van dit document is beschreven. We gaan uit van een bestaande applicatie met de volgende architectuur:

die we willen omzetten in deze:

waarbij EF5 NHibernate heeft vervangen. We hebben zojuist de laag [DAO2] gebouwd. Deze heeft in feite niet dezelfde interface als de laag [DAO1], waarvan de interface beperkter was:


  public interface IDao
  {
    // klantenlijst
    List<Client> GetAllClients();
    // lijst met artsen
    List<Medecin> GetAllMedecins();
    // lijst met tijdvakken van een arts
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // lijst met RV van een bepaalde arts, op een bepaalde dag
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // een RV toevoegen
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // een RV verwijderen
    void SupprimerRv(int idRv);
  }

De laag [DAO2] heeft aan deze interface de volgende methode toegevoegd:


// een T-entiteit opzoeken via de primaire sleutel
T Find<T>(int id) where T : class;

Deze methode is toegevoegd omdat de ORM EF 5 standaard in de modus ‘Lazy Loading’ werkt. De entiteiten komen in de laag [ASP.NET] binnen zonder hun afhankelijkheden. Met de bovenstaande methode kunnen we ze ophalen als dat nodig is, en in sommige gevallen is dat inderdaad nodig. NHibernate werkt standaard ook in de modus ‘Lazy Loading’, maar ik had deze in de modus ‘Eager Loading’ gebruikt. De entiteiten kwamen in de laag [ASP.NET] binnen met hun afhankelijkheden.

We gaan de porting van de applicatie ASP.NET / NHibernate naar de applicatie ASP.NET / EF 5 afronden. Maar aangezien dit niet langer betrekking heeft op EF5, zullen we geen commentaar geven op de webcode. We zullen alleen uitleggen hoe je de webapplicatie kunt opzetten en testen. Deze is beschikbaar op de website van deze tutorial.

3.6.6. Genereren van DLL uit de laag [DAO]

In de volgende architectuur:

zal de laag [ASP.NET] de lagen rechts ervan tot zijn beschikking hebben in de vorm van DLL. We bouwen dus de DLL van de laag [DAO].

  • in [1] selecteren we het testprogramma en in [2] nemen we het niet op in de DLL die zal worden gegenereerd;
  • in [3], in de projecteigenschappen, geef je aan dat de te maken assembly een DLL is;
  • in [4], in het menu van VS, wordt aangegeven dat er een assembly van het type [Release] wordt gegenereerd, die minder informatie bevat dan een assembly van het type [Debug];
  • in [5] wordt de assembly van het project opnieuw gegenereerd. De DLL wordt gegenereerd;
  • in [6] worden alle bestanden van het project weergegeven;
  • met [7] wordt de DLL van het project van de laag [DAO] gegenereerd. Dit is het bestand dat het webproject ASP.NET zal gebruiken;
  • in [8] verversen we de weergave van het project;
  • in [9] worden de DLL-bestanden uit de map [Release] verzameld in een externe map [lib], [10]. Daar haalt het webproject zijn verwijzingen vandaan.

3.6.7. De laag [ASP.NET]

Hier zullen we de porting van de applicatie [ASP.NET / NHibernate] naar de applicatie [ASP.NET / EF 5] toelichten. We gaan werken met Visual Studio Express 2012 voor het web, dat gratis beschikbaar is op URL [http://www.microsoft.com/visualstudio/fra/downloads].

We gaan verderbouwen op het bestaande webproject dat is gemaakt met VS 2010.

  • in [1] openen we het bestaande project:
  • in [2] heeft het geladen project de volgende verwijzingen: [3]:
  • [NHibernate] is de DLL van het framework NHibernate,
  • [Spring.Core] is de DLL van het framework Spring.net,
  • [log4net] is de DLL van het log4net-logframework. Dit framework wordt gebruikt door Spring.net,
  • [MySql.Data] is de driver ADO.NET van de SGBD MySQL,
  • [rdvmedecins] is de DLL van de laag [DAO] die is opgebouwd met NHibernate;
  • in [4] wijzigen we de naam van het project en in [5] verwijderen we de eerdere verwijzingen;
  • in [6] voegen we verwijzingen naar het project toe;
  • in [7] gebruiken we in de wizard de optie [Parcourir];
  • in [8] selecteren we alle DLL-bestanden van project nr. 2 die eerder in de map [lib] zijn geplaatst;
  • in [9], een overzicht dat we valideren;
  • in [10], het webproject met de nieuwe referenties.

Nu dit is gebeurd, ziet het project er als volgt uit:

  • in [1] is de code voor het beheer van de webpagina's verdeeld over de twee bestanden [Global.asax] en [Default.aspx]. Hulpprogrammacode is in de map [Entites] geplaatst. Ten slotte wordt de applicatie geconfigureerd via het bestand [Web.config];
  • in [2] genereren we de assembly van het project;
  • in [3] treden er fouten op.

Laten we de fouten eens bekijken, bijvoorbeeld de volgende:

Image

en de uitleg daarbij:

Image

Het type van [medecin.Id] is int?, terwijl de methode [GetCreneauxMedecin] van het type int is. Er is dus een cast nodig. Deze fout komt in de hele code steeds weer voor, omdat de entiteiten van het project ASP.NET / NHibernate primaire sleutels van het type int hadden, terwijl die van het project ASP.NET / EF van het type int? zijn. We corrigeren alle fouten van dit type en genereren het project opnieuw. Dan zijn er geen fouten meer.

Er is nog één detail dat we moeten regelen voordat we het project uitvoeren: het instantiëren van de laag [DAO] door het Spring-framework. Dit gebeurt in [Global.asax]:


protected void Application_Start(object sender, EventArgs e)
    {
      // bepaalde gegevens uit de database in de cache opslaan
      try
      {
        // instantie van de laag [dao]
        Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
        ...
      }
      catch (Exception ex)
      {...
      }
    }

In het testprogramma van de laag [DAO] werd de laag [DAO] als volgt geïnstantieerd:


dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

Beide methoden zijn identiek. We herinneren ons dat deze instantiëring van de laag [DAO] gebaseerd was op een configuratie die in [App.config] was gemaakt. Vervolgens vervangen we de huidige inhoud [Web.config] van het webproject door die van [App.config] uit het project van de laag [DAO], zodat we dezelfde configuratie krijgen.

We zijn klaar voor een eerste uitvoering. De startpagina wordt weergegeven: [1]:

  • in [2] voeren we een afspraakdag in en bevestigen we deze;
  • in [3] treedt er een fout op.

Als we de foutmelding bekijken die op de pagina wordt weergegeven, zien we dat de gemelde uitzondering die van Lazy Loading is: er is geprobeerd een afhankelijkheid van een object te laden terwijl de persistentiecontext die dit object beheert, is gesloten. Het object bevindt zich nu in een "losgekoppelde" toestand. Deze fout is te wijten aan het feit dat NHibernate in de Eager Loading-modus werd gebruikt, terwijl EF standaard in Lazy Loading werkt. In de hierboven rood gemarkeerde regel:

  • rdv staat voor een object [Rv] dat zonder zijn afhankelijkheden is geladen;
  • om rdv.Creneau.Id te evalueren, probeert de applicatie de afhankelijkheid rdv.Creneau te laden. Maar aangezien we ons niet meer in de context bevinden, is dit niet mogelijk, vandaar de uitzondering.

De oplossing is hier eenvoudig. Op regel 108 wordt een item aangemaakt in een woordenboek met als sleutel de primaire sleutel van het tijdvak van een afspraak. Het blijkt echter dat de entiteit [Rv] de primaire sleutel van het bijbehorende tijdvak bevat. We schrijven dus:


        dicoRvPris[(int)rdv.CreneauId] = rdv;

We proberen de uitvoering opnieuw. Deze keer is de foutmelding als volgt:

De fout is vergelijkbaar. Op regel 132 wordt geprobeerd de afhankelijkheid [Client] van een object [Rv] te laden in de laag ASP.NET, dus buiten de context. Het object [Client] moet uit de database worden opgehaald. Om dit probleem op te lossen, is de interface [IDao] uitgebreid met de volgende methode:


    // een entiteit T opzoeken via de primaire sleutel
    T Find<T>(int id) where T : class;

Hiermee kunnen de afhankelijkheden worden opgehaald. De foutieve regel hierboven wordt dus als volgt herschreven:


        Client client = Global.Dao.Find<Client>(agenda.Creneaux[i].Rdv.ClientId);

Opnieuw valt het voordeel op dat entiteiten hun vreemde sleutels meenemen. Hier geeft de entiteit [Rv] ons toegang tot de vreemde sleutel van de bijbehorende afhankelijkheid [Creneau]. Nu deze twee correcties zijn aangebracht, werkt de applicatie. De lezer wordt uitgenodigd om de applicatie [RdvMedecins-SqlServer-03] te testen, die te vinden is in de downloadgedeelte met voorbeelden op de website van dit artikel.

3.7. Conclusion

We hebben de porting van een applicatie ASP.NET / NHibernate succesvol afgerond:

naar een applicatie ASP.NET / EF 5:

Hoewel deze architectuur ons in staat had moeten stellen de laag [ASP.NET] intact te houden, moesten we deze om twee redenen aanpassen:

  • de entiteiten waren niet precies hetzelfde. Het type van de primaire sleutels van de entiteiten NHibernate was int, terwijl dat van EF 5 int? was. Dit bracht ons ertoe om cast in de webcode op te nemen;
  • de manier waarop de entiteiten werden geladen, was niet hetzelfde voor beide ORM: ‘Eager Loading’ voor NHibernate, ‘Lazy Loading’ voor EF 5. Dit heeft ertoe geleid dat we de interface van de laag [DAO] hebben uitgebreid met een generieke methode waarmee een entiteit op basis van de primaire sleutel kan worden opgehaald.

De porting bleek echter vrij eenvoudig, wat – voor zover dat nog nodig was – de gelaagde architectuur en het injecteren van afhankelijkheden met Spring of een ander framework voor het injecteren van afhankelijkheden opnieuw rechtvaardigt.

We gaan nu de impact meten van een wijziging in SGBD op de vorige architectuur. We gaan alle vorige projecten overzetten naar vier andere SGBD:

  • Oracle Database Express Edition 11g Release 2;
  • MySQL 5.5.28;
  • PostgreSQL 9.2.1;
  • Firebird 2.1.

De codes zullen niet meer veranderen. Alleen de volgende elementen zullen veranderen:

  • de definitie in de entiteiten van het veld dat wordt gebruikt om de toegang tot een entiteit te regelen;
  • de configuratiebestanden [App.config] of [Web.config];

We zullen alleen ingaan op de elementen die veranderen.