Skip to content

3. Fallstudie mit SQL Server Express 2012

3.1. Einleitung

Die meisten online verfügbaren Beispiele für Entity Framework sind Beispiele, die SQL Server verwenden. Das ist ganz normal. Es ist wahrscheinlich das am weitesten verbreitete DBMS in der .NET-Unternehmenswelt. Wir werden diesem Trend folgen. Die Beispiele werden dann auf alle in Abschnitt 1.2 genannten Datenbanken ausgeweitet.

3.2. Installation der Tools

Wir werden die Installation der Tools nicht beschreiben. Dies würde eine große Anzahl von Screenshots erfordern, die schnell veralten würden. Dies ist eine Aufgabe (zugegebenermaßen nicht immer einfach), die wir dem Leser überlassen.

Wir müssen die folgenden Tools installieren:

  • das DBMS SQL Server Express 2012: [http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]. Laden Sie die Version „With Tools“ herunter, die ein Verwaltungstool für das DBMS enthält:
 

Sobald das DBMS installiert ist, starten wir es:

  • [1]: Starten Sie über das Startmenü den „SQL Server Configuration Manager“;
  • [2]: Starten Sie in diesem Manager den Server;
  • [3]: Er läuft nun.

Starten Sie nun das SQL Server-Verwaltungstool:

  • [1]: Starten Sie über das Startmenü „SQL Server Management Studio“;
  • [2]: das Verwaltungstool.

Wir stellen eine Verbindung zum Server her:

  • Öffnen Sie in [1] den Objekt-Explorer;
  • Geben Sie in [2] die Verbindungsparameter ein:
  • [3]: Der (lokale) Server (beachten Sie die erforderlichen Klammern) bezieht sich auf den auf dem Rechner installierten Server,
  • [4]: Wählen Sie „Windows-Authentifizierung“. Sie müssen Administrator auf Ihrem Computer sein, damit diese Verbindung erfolgreich hergestellt werden kann,
  • [6]: Sie sind verbunden;
  • [7]: Sie möchten bestimmte Servereigenschaften ändern;
  • [8]: Wir fordern zwei Authentifizierungsmodi an:
  • Windows-Authentifizierung, wie gerade verwendet. Ein Windows-Benutzer mit den entsprechenden Berechtigungen kann sich dann anmelden,
  • SQL-Server-Authentifizierung. Der Benutzer muss einer der im Datenbankmanagementsystem registrierten Benutzer sein;

Sobald dies erledigt ist, können wir die Servereigenschaften überprüfen;

  • [9]: Bearbeiten Sie die Eigenschaften des Benutzers „sa“ (Systemadministrator);
  • Legen Sie in [10] ein Passwort für den Benutzer fest. Im weiteren Verlauf dieses Dokuments lautet das Passwort „sqlserver2012“;
  • Erteilen Sie unter [10] die Berechtigung zum Herstellen einer Verbindung;
  • In [11] ist die Verbindung aktiviert. Der Assistent kann nun bestätigt werden;
  • Melden Sie sich in [12] vom Server ab.

Nun stellen wir die Verbindung erneut her, indem wir uns mit dem Login „sa/sqlserver2012“ anmelden:

  • In [1] stellen wir die Verbindung wieder her;
  • in [2] während der SQL Server-Authentifizierung;
  • In [3] ist der Benutzer „sa“;
  • in [4] lautet sein Passwort „sqlserver2012“;
  • in 5 melden wir uns an;
  • in [6] sind wir angemeldet.

Wir erstellen nun eine Demo-Datenbank:

  • Erstellen Sie in [1] eine neue Datenbank;
  • Geben Sie ihr in [2] den Namen „demo“;
  • Klicken Sie in [3] auf „Validieren“;
  • in [4] wird die Datenbank erstellt;
  • in 5 erstellen Sie eine neue Tabelle in der Datenbank „demo“;
  • In [6] definieren wir eine Tabelle mit zwei Spalten, ID und NAME;
  • in [7] legen wir die Spalte [ID] als Primärschlüssel fest;
  • in [8] wird der Primärschlüssel durch einen Schlüssel dargestellt;
  • in [9] wird die Tabelle gespeichert;
  • In [10] geben wir ihr einen Namen;
  • In [11] müssen Sie die Datenbank aktualisieren, damit die Tabelle in der Datenbank [demo] erscheint;
  • in [12] wurde die Tabelle [PERSONNES] erfolgreich erstellt.

Wir wissen nun genug über die Verwendung von SQL Server Management Studio.

3.3. Der integrierte Server (localdb)\v11.0

VS Express 2012 enthält einen eingebetteten SQL Server. Wir gehen hier davon aus, dass VS Express 2012 installiert wurde [http://www.microsoft.com/visualstudio/fra/downloads]. Starten Sie VS 2012 [1]:

Starten Sie das SQL Server 2012 Management Studio [2] und melden Sie sich an [3].

  • Stellen Sie unter [4] eine Verbindung zum Server (localdb)\v11.0 her;
  • Verwenden Sie in 5 die Windows-Authentifizierung;
  • In [6] werden bei erfolgreicher Verbindung die Datenbanken des Servers angezeigt. Wie zuvor können Sie eine neue Datenbank erstellen.

Wir werden diesen eingebetteten Server in VS 2012 nicht verwenden.

3.4. Erstellen der Datenbank aus Entitäten

Mit Entity Framework 5 Code First können Sie eine Datenbank aus Entitäten erstellen. Das werden wir nun untersuchen. Mit VS Express 2012 erstellen wir ein erstes Konsolenprojekt in C#:

  • in [1], die Projektdefinition;
  • in [2] das erstellte Projekt.

Alle unsere Projekte benötigen „ “, die Entity Framework 5-DLL. Wir fügen sie hinzu:

  • in [1] können Sie mit dem NuGet-Tool Abhängigkeiten herunterladen;
  • in [2] laden wir die Entity-Framework-Abhängigkeit herunter;
  • in [3] wurde die Referenz zum Projekt hinzugefügt.

Weitere Informationen erhalten Sie, wenn Sie die Eigenschaften der hinzugefügten Referenz anzeigen:

  • in [1] die DLL-Version. Sie benötigen Version 5;
  • in [2] den Speicherort im Dateisystem: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll, wobei <solution> der VS-Lösungsordner ist. Alle von NuGet hinzugefügten Pakete werden in den Ordner <solution>/packages verschoben;
  • in [3] wurde eine [packages.config]-Datei erstellt. Ihr Inhalt lautet wie folgt:

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

Hier sind die von NuGet importierten Pakete aufgeführt.

Kehren wir zum VS-Projekt zurück und erstellen wir einen Ordner [Models] im Projekt:

  • in [1], indem ein Ordner zum Projekt hinzugefügt wird;
  • in [2] erhält er den Namen [Models].

Wir werden diese Vorgehensweise beibehalten und unsere Entitätsdefinitionen im Ordner [Models] ablegen.

Um unsere Entitäten zu erstellen, verwenden wir die im NHibernate-Projekt verwendete MySQL-5-Datenbankdefinition. Lassen Sie uns die Rolle von EF-Entitäten noch einmal betrachten:

Entitäten müssen die Datenbanktabellen widerspiegeln. Die Datenzugriffsebene verwendet diese Entitäten, anstatt direkt mit den Tabellen zu arbeiten. Beginnen wir mit der Tabelle [DOCTORS]:

3.4.1. Die Entität [Medecin]

Sie enthält Informationen zu den Ärzten, die von der Anwendung [RdvMedecins] verwaltet werden.

  • ID: die ID-Nummer des Arztes – der Primärschlüssel der Tabelle
  • VERSION: Nummer, die die Version der Zeile in der Tabelle identifiziert. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • LAST_NAME: der Nachname des Arztes
  • VORNAME: der Vorname des Arztes
  • TITLE: Anrede (Frau, Frau, Herr)

Wir könnten mit der folgenden Klasse [Doctor] beginnen:


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; }
}
  • Zeile 3: Die Klasse [Medecin] ist mit der Tabelle [MEDECINS] in der Datenbank verknüpft. Diese Tabelle befindet sich in einem Schema namens „dbo“.

Wir speichern diese Klasse in einer Datei namens [Entities.cs] [1]. Hier werden wir alle unsere Entitäten ablegen.

Noch immer im Ordner [Models] erstellen wir die folgende Datei [Context.cs]:


using System.Data.Entity;
using RdvMedecins.Entites;
 
namespace RdvMedecins.Models
{
 
  // the context
  public class RdvMedecinsContext : DbContext
  {
    // the doctors
    public DbSet<Medecin> Medecins { get; set; }
  }
 
  // database initialization
  public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
  {
  }
}
  • Zeile 8: Die Klasse [RdvMedecinsContext] repräsentiert den Persistenzkontext, d. h. die Menge der vom ORM verwalteten Entitäten. Sie muss von der Klasse [System.Data.Entity.DbContext] abgeleitet sein;
  • Zeile 11: Das Feld [Medecins] repräsentiert die [Medecin]-Entitäten im Persistenzkontext. Es ist vom Typ DbSet<Medecin>. Im Allgemeinen gibt es so viele [DbSet]s wie Tabellen in der Datenbank, nämlich eines pro Tabelle;
  • Zeile 15: Wir definieren eine [RdvMedecinsInitializer]-Klasse, um die erstellte Datenbank zu initialisieren. Diese leitet sich von der Klasse [DropCreateDataBaseAlways] ab, die, wie der Name schon sagt, die Datenbank löscht, falls sie bereits existiert, und sie anschließend neu erstellt. Dies ist während der Datenbankentwicklungsphase nützlich. Der Parameter der Klasse [DropCreateDataBaseAlways] ist der Typ des Persistenzkontexts, der mit der Datenbank verknüpft ist. Neben [DropCreateDataBaseAlways] können auch andere übergeordnete Klassen für die Initialisierungsklasse verwendet werden:
  • [DropCreateDatabaseIfModelChanges]: Erstellt die Datenbank neu, wenn sich die Entitäten geändert haben,
  • [CreateDatabaseIfNotExists]: Erstellt die Datenbank, falls sie nicht existiert;

Wir müssen noch ein Hauptprogramm erstellen. Es wird wie folgt aussehen [CreateDB_01.cs]:


using System;
using System.Data.Entity;
using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
  class CreateDB_01
  {
    static void Main(string[] args)
    {
      // we create the
      Database.SetInitializer(new RdvMedecinsInitializer());
      using (var context = new RdvMedecinsContext())
      {
        context.Database.Initialize(false);
      }
    }
  }
}
  • Zeile 12: [System.Data.Entity.DataBase] ist eine Klasse, die statische Methoden zur Verwaltung der mit einem Persistenzkontext verbundenen Datenbank bereitstellt. Mit der statischen Methode [SetInitializer] können Sie die Datenbankinitialisierungsklasse angeben. Dies löst keine Initialisierung aus;
  • Zeile 13: Um mit einem Persistenzkontext zu arbeiten, müssen Sie ihn instanziieren. Dies geschieht hier. Es wird eine using-Anweisung verwendet, damit der Kontext automatisch geschlossen wird, wenn die Anweisung endet. Daher wird in Zeile 17 der Kontext geschlossen;
  • Zeile 15: Wir lösen explizit die Generierung der mit dem Persistenzkontext [RdvMedecinsContext] verbundenen Datenbank aus. Der Parameter „false“ gibt an, dass dieser Vorgang nicht ausgeführt werden soll, wenn er für diesen Kontext bereits erfolgt ist. Hier hätten wir ihn genauso gut auf „true“ setzen können.

Bei der Arbeit mit einer Datenbank werden Verbindungsparameter in der Regel in der Datei [App.config] gespeichert. Beachten Sie, dass sie dort derzeit noch nicht vorhanden sind:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>
</configuration>

Die oben genannten Elemente wurden der Datei [App.config] hinzugefügt, als die Entity Framework-Abhängigkeit zu den Projektreferenzen hinzugefügt wurde.

Führen wir das Projekt aus (Strg-F5), nachdem wir SQL Server Express gestartet haben (dies ist wichtig):

Die Ausführung sollte ohne Fehler abgeschlossen werden. Öffnen wir nun das SQL Server Management Studio und aktualisieren die Ansicht:

Wir sehen, dass eine Datenbank mit dem vollständigen Namen der Klasse [RdvMedecinsContext] erstellt wurde und dass sie eine Tabelle namens [dbo.MEDECINS] (den Namen, den wir ihr gegeben haben) mit Spalten enthält, die den Feldnamen der Entität [Medecin] entsprechen. Wenn der Code erfolgreich ausgeführt wurde, die oben genannte Datenbank jedoch nicht angezeigt wird, überprüfen Sie den eingebetteten Server (localdb)\v11.0 (siehe Seite 19). Bei VS 2012 Pro wird dieser Server verwendet, wenn SQL Server bei der Ausführung des Codes nicht aktiv ist. Bei VS 2012 Express ist dies nicht der Fall.

Sehen wir uns die Struktur der Tabelle [MEDECINS] an:

  • Sie verwendet die Feldnamen der Entität [Medecin];
  • die Spalte [Id] ist der Primärschlüssel. Dies ist eine EF-Konvention: Wenn die Entität E ein Id- oder Eid-Feld (MedecinId) hat, dann ist diese Spalte der Primärschlüssel in der zugehörigen Tabelle;
  • die Spaltentypen in der Tabelle entsprechen denen der Entitätsfelder;
  • Für die Spalten „Title“, „Last Name“ und „First Name“ wurde der Typ [nvarchar(max)] verwendet. Wir könnten genauer sein: 5 Zeichen für den Titel, 30 für den Nachnamen und den Vornamen;
  • Die Spalten „Titel“, „Nachname“ und „Vorname“ können einen NULL-Wert annehmen. Das werden wir ändern.

Sehen wir uns die Eigenschaften des Primärschlüssels [Id] an:

In [1] sehen wir, dass der Primärschlüssel vom Typ [Identity] ist, was bedeutet, dass sein Wert automatisch von SQL Server generiert wird. Wir werden diese Strategie für alle DBMS anwenden.

Durch die Verwendung von Annotationen werden wir uns weniger auf EF-Konventionen verlassen. Der Entity-Code in [Entities.cs] sieht nun wie folgt aus:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
 
namespace RdvMedecins.Entites
{
  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public int Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Required]
    [Column("VERSION")]
    public int Version { get; set; }
  }
}
  • Zeilen 2 und 3: Die Annotationen befinden sich im Namespace [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) und im Namespace [System.ComponentModel.DataAnnotations.Schema] (Column). Weitere Annotationen finden Sie unter der URL [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
  • Zeile 11: [Key] bezeichnet den Primärschlüssel;
  • Zeile 12: [Column] legt den dem Feld entsprechenden Spaltennamen fest;
  • Zeile 14: [Required] gibt an, dass das Feld erforderlich ist (SQL NOT NULL);
  • Zeile 15: [MaxLength] legt die maximale Länge der Zeichenfolge fest, [MinLength] ihre Mindestlänge;

Führen wir das Projekt mit dieser neuen Definition der Entität [Medecin] aus. Die resultierende Datenbank sieht wie folgt aus:

 
  • die Spalten tragen die Namen, die wir ihnen zugewiesen haben;
  • die Anmerkung [Required] wurde in SQL NOT NULL übersetzt;
  • Die Annotation [MaxLength(N)] wurde dem SQL-Typ nvarchar(N) zugeordnet.

In der NHibernate-Anwendung diente die Spalte [VERSION] dazu, den gleichzeitigen Zugriff auf dieselbe Zeile in einer Tabelle zu verhindern. Das Prinzip lautet wie folgt:

  • Ein Prozess P1 liest zum Zeitpunkt T1 eine Zeile L aus der Tabelle [DOCTORS]. Die Zeile hat die Version V1;
  • Ein Prozess P2 liest zum Zeitpunkt T2 dieselbe Zeile L aus der Tabelle [DOCTORS]. Die Zeile hat die Version V1, da Prozess P1 seine Änderung noch nicht festgeschrieben hat;
  • Prozess P1 bestätigt seine Änderung an Zeile L. Die Version von Zeile L ändert sich daraufhin zu V2 = V1 + 1;
  • Prozess P2 bestätigt seine Änderung an Zeile L. Das ORM löst daraufhin eine Ausnahme aus, da Prozess P2 eine Version V1 von Zeile L hat, die sich von der in der Datenbank gefundenen Version V2 unterscheidet.

Dies wird als optimistisches Parallelitätsmanagement bezeichnet. Bei EF 5 muss ein Feld, das diese Rolle übernimmt, eines von zwei Attributen aufweisen: [Timestamp] oder [ConcurrencyCheck]. SQL Server verfügt über einen [timestamp]-Typ. Der Wert einer Spalte dieses Typs wird von SQL Server automatisch generiert, sobald eine Zeile eingefügt oder geändert wird. Eine solche Spalte kann dann zur Verwaltung des parallelen Zugriffs verwendet werden. Um auf das vorherige Beispiel zurückzukommen: Prozess P2 findet einen Zeitstempel, der sich von dem unterscheidet, den er gelesen hat, da die von Prozess P1 vorgenommene Änderung ihn in der Zwischenzeit verändert hat.

Unsere Entität [Doctor] entwickelt sich wie folgt:


using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
 
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public int Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
  }
}
  • Zeilen 26–28: die neue Spalte mit dem Attribut [Timestamp] aus Zeile 27. Der Feldtyp muss byte[] sein (Zeile 28). Der Feldname kann beliebig sein. Wir setzen das Attribut [Required] nicht, da die Anwendung diesen Wert nicht bereitstellt; vielmehr übernimmt dies das DBMS selbst.

Wenn wir das Projekt mit dieser neuen Entität ausführen, entwickelt sich die Datenbank wie folgt:

Es gibt noch einen letzten Punkt zu klären. Der Persistenzkontext „weiß“, dass eine Entität in die Datenbank eingefügt werden muss, da ihr Primärschlüssel zu diesem Zeitpunkt null ist. Erst durch das Einfügen in die Datenbank wird dem Primärschlüssel ein Wert zugewiesen. Hier ist der dem Primärschlüssel [Id] zugewiesene Typ int nicht geeignet, da dieser Typ den Wert null nicht akzeptiert. Wir weisen ihm daher den Typ int? zu, der int-Werte sowie den Null-Zeiger akzeptiert. Die verwendete Entität [Medecin] sieht daher wie folgt aus:


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

Wir müssen noch sehen, wie das Konzept eines Fremdschlüssels zwischen Tabellen in einer Entität dargestellt wird.

3.4.2. Die Entität [Creneau]

Die Tabelle [CRENEAUX] listet die Zeitfenster auf, in denen Termine möglich sind:

  • ID: Die ID-Nummer des Zeitfensters – der Primärschlüssel der Tabelle
  • VERSION: Nummer, die die Version der Zeile in der Tabelle identifiziert. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • ID_MEDECIN: ID-Nummer zur Identifizierung des Arztes, dem dieser Terminblock gehört – Fremdschlüssel auf die Spalte MEDECINS(ID).
  • START_TIME: Startzeit des Zeitfensters
  • MSTART: Startminute des Zeitfensters
  • HFIN: Endzeit des Zeitfensters
  • MFIN: Endminute des Zeitfensters

Die zweite Zeile der Tabelle [SLOTS] (siehe [1] oben) gibt beispielsweise an, dass Zeitfenster Nr. 2 um 8:20 Uhr beginnt und um 8:40 Uhr endet und der Ärztin Nr. 1 (Frau Marie PELISSIER) zugeordnet ist.

Mit diesen Informationen können wir die Entität [Creneau] in [Entites.cs] wie folgt definieren:


[Table("CRENEAUX", Schema = "dbo")]
  public class Creneau
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [Column("HDEBUT")]
    public int Hdebut { get; set; }
    [Required]
    [Column("MDEBUT")]
    public int Mdebut { get; set; }
    [Required]
    [Column("HFIN")]
    public int Hfin { get; set; }
    [Required]
    [Column("MFIN")]
    public int Mfin { get; set; }
    [Required]
    public virtual Medecin Medecin { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

Die einzige Änderung befindet sich in den Zeilen 20–21. Die Tatsache, dass die Tabelle [CRENEAUX] einen Fremdschlüssel auf die Tabelle [MEDECINS] hat, spiegelt sich in der Entität [Creneau] durch das Vorhandensein einer Referenz auf die Entität [Medecin] in Zeile 21 wider. Der Feldname ist irrelevant; nur der Typ ist von Bedeutung. Die Eigenschaft muss mit dem Schlüsselwort virtual als virtuell deklariert werden. Der Grund dafür ist, dass EF alle sogenannten Navigationseigenschaften neu definieren muss – also diejenigen, die einem Fremdschlüssel entsprechen und die Navigation zwischen Tabellen ermöglichen.

Um die neue Entität zu testen, müssen wir einige Änderungen in [Context.cs] vornehmen:


using System.Data.Entity;
using RdvMedecins.Entites;
 
namespace RdvMedecins.Models
{
 
  // the context
  public class RdvMedecinsContext : DbContext
  {
    // entities
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
  }
 
  // database initialization
  public class RdvMedecinsInitializer :  DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
  }
}

Zeile 12 spiegelt wider, dass der Kontext eine weitere Entität zu verwalten hat. Wenn wir das Projekt ausführen, erhalten wir die folgende neue Datenbank:

Die Tabelle [CRENEAUX] wurde tatsächlich erstellt, und die neue Funktion besteht im Vorhandensein der Fremdschlüssel [1] und [2]. Ihre Namen wurden aus den Namen der entsprechenden Felder in der Entität (Medecin) generiert, denen das Suffix „_Id“ angehängt wurde. Um die Eigenschaften dieses Fremdschlüssels anzuzeigen, versuchen wir, ihn zu ändern [3].

Der obige Screenshot zeigt, dass [Medecin_Id] ein Fremdschlüssel in der Tabelle [CRENEAUX] ist und dass er auf den Primärschlüssel [ID] in der Tabelle [MEDECINS] verweist.

Wenn wir die Entitäten für eine bestehende Datenbank erstellen, wird die Fremdschlüsselspalte nicht unbedingt [Medecin_Id] heißen. Bei anderen Spalten haben wir gesehen, dass die Annotation [Column] dieses Problem gelöst hat. Seltsamerweise ist es bei einem Fremdschlüssel komplizierter. Wir müssen wie folgt vorgehen:


public class Creneau
  {
    // data
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
}
  • Zeilen 5–7: Wir erstellen ein Feld vom Typ Fremdschlüssel (int). Mit dem Attribut [Column] geben wir den Namen der Spalte an, die in der mit der Entität verknüpften Tabelle als Fremdschlüssel dienen soll;
  • Zeile 9: Wir fügen die Annotation [ForeignKey] zum Feld vom Typ [Medecin] hinzu. Das Argument dieser Annotation ist der Name des Feldes (nicht der Spalte), das mit der Fremdschlüsselspalte in der Tabelle verknüpft ist.

Wenn das Projekt nun ausgeführt wird, wird die folgende Tabelle erstellt:

Oben trägt die Fremdschlüsselspalte tatsächlich den Namen, den wir ihr gegeben haben. Beachten Sie, dass die Felder:


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

haben zu einer einzigen Spalte geführt, der Spalte [MEDECIN_ID]. Dennoch ist das Vorhandensein des Feldes [MedecinId] wichtig. Beim Auslesen einer Zeile aus der Tabelle [SLOTS] erhält sie den Wert der Spalte [DOCTOR_ID], d. h. den Wert des Fremdschlüssels in der Tabelle [DOCTORS]. Dies ist oft nützlich.

Das obige Feld [Medecin] spiegelt die Viele-zu-Eins-Beziehung wider, die die Entität [Creneau] mit der Entität [Medecin] verbindet. Mehrere [Slot]-Objekte sind mit demselben [Doctor] verknüpft. Die umgekehrte Beziehung – bei der ein einzelnes [Doctor]-Objekt mit mehreren [Slot]-Objekten assoziiert ist – kann mithilfe eines zusätzlichen Feldes in der Entität [Doctor] modelliert werden:


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

In Zeile 8 haben wir das Feld [Slots] hinzugefügt, das eine Sammlung von [Slot]-Objekten ist. Dieses Feld ermöglicht uns den Zugriff auf alle verfügbaren Zeitfenster des Arztes.

Wenn wir das Projekt erneut ausführen, sehen wir, dass sich die Tabelle [DOCTORS] nicht geändert hat:

 

Es wurden keine Spalten hinzugefügt. Die Fremdschlüsselbeziehung zwischen der Tabelle [CRENEAUX] und der Tabelle [MEDECINS] reicht aus, damit EF die zugehörigen Felder generieren kann:


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

Wir kennen die Grundlagen. Zum Abschluss können wir die beiden anderen Entitäten erstellen.

3.4.3. Die Entitäten [Client] und [Appointment]

Mit dem, was wir gelernt haben, können wir die Entitäten [Client] und [Appointment] schreiben. Die Entität [Client] enthält Informationen über die Kunden, die von der Anwendung [DoctorAppointments] verwaltet werden.

  • ID: die ID-Nummer des Kunden – der Primärschlüssel der Tabelle
  • VERSION: Nummer, die die Version der Zeile in der Tabelle identifiziert. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • LAST_NAME: der Nachname des Kunden
  • VORNAME: der Vorname des Kunden
  • TITLE: Anrede (Frau, Frau, Herr)

Die Entität [Client] könnte wie folgt aussehen:


  [Table("CLIENTS", Schema = "dbo")]
  public class Client
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // customer rvs
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

Die Klasse [Client] ist fast identisch mit der Klasse [Doctor]. Sie könnten von derselben übergeordneten Klasse abgeleitet sein. Das neue Element befindet sich in Zeile 21. Es spiegelt die Tatsache wider, dass ein Kunde mehrere Termine haben kann, und leitet sich aus dem Vorhandensein eines Fremdschlüssels von der Tabelle [RVS] zur Tabelle [CLIENTS] ab.

Die Entität [Rv] repräsentiert einen Termin:

  • ID: Nummer zur eindeutigen Identifizierung des Termins – Primärschlüssel
  • DAY: Tag des Termins
  • SLOT_ID: Terminzeitfenster – Fremdschlüssel auf die Spalte [ID] der Tabelle [SLOTS] – bestimmt sowohl das Zeitfenster als auch den beteiligten Arzt.
  • CLIENT_ID: ID des Kunden, für den der Termin vereinbart wurde – Fremdschlüssel auf der Spalte [ID] der Tabelle [CLIENTS]

Die Entität [Rv] könnte wie folgt aussehen:


[Table("MEDECINS", Schema = "dbo")]
  public class Rv
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [Column("JOUR")]
    public DateTime Jour { get; set; }
    [Column("CLIENT_ID")]
    public int ClientId { get; set; }
    [ForeignKey("ClientId")]
    [Required]
    public virtual Client Client { get; set; }
    [Column("CRENEAU_ID")]
    public int CreneauId { get; set; }
    [ForeignKey("CreneauId")]
    [Required]
    public virtual Creneau Creneau { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}
  • Zeilen 5–7: Primärschlüssel;
  • Zeilen 8–10: Termin;
  • Zeilen 11–12: der Fremdschlüssel von der Tabelle [RVS] zur Tabelle [CLIENTS];
  • Zeilen 13–15: der Kunde mit dem Termin;
  • Zeilen 16–17: der Fremdschlüssel von der Tabelle [RVS] zur Tabelle [CRENEAUX];
  • Zeilen 18–20: das Terminfenster;
  • Zeilen 21–23: das Feld für die parallele Zugriffskontrolle.

In Zeile 17 sehen wir eine Viele-zu-Eins-Beziehung: Ein einzelner Zeitfenster kann mehreren Terminen entsprechen (nicht am selben Tag). Die umgekehrte Beziehung lässt sich in der Entität [Creneau] widerspiegeln:


public class Creneau
  {
    // niche Rvs
    public ICollection<Rv> Rvs { get; set; }
    ...
}

Zeile 4: die Sammlung der für diesen Zeitblock geplanten Termine.

Wenn das Projekt ausgeführt wird, sieht die generierte Datenbank wie folgt aus:

 

Die Tabellen [DOCTORS] und [SLOTS] haben sich nicht verändert. Die Tabellen [CLIENTS] und [APPs] sehen wie folgt aus:

Das war zu erwarten. Wir müssen noch ein paar Details klären:

  • den Datenbanknamen verwalten. Hier wurde er von EF generiert;
  • die Datenbank mit Daten füllen.

3.4.4. Festlegen des Datenbanknamens

Um den Namen der von EF generierten Datenbank festzulegen, verwenden wir eine in [App.config] definierte Verbindungszeichenfolge. Diese Konfigurationsdatei ändert sich wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>
 
  <!-- connection chain on base -->
  <connectionStrings>
    <add name="RdvMedecinsContext"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- the factory provider -->
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider"
       invariant="System.Data.SqlClient"
       description=".Net Framework Data Provider for SqlServer"
       type="System.Data.SqlClient.SqlClientFactory, System.Data,
     Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    />
    </DbProviderFactories>
  </system.data>
 
</configuration>
  • Zeilen 15–19: die Datenbankverbindungszeichenfolge;
  • Zeile 16: Das Attribut [name] verwendet den Namen der Klasse [RdvMedecinsContext], die für den Persistenzkontext verwendet wird. Es ist wichtig, dies zu beachten. Diese Einschränkung kann im Kontextkonstruktor umgangen werden:

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

In diesem Fall können wir name= „myContext“ verwenden. Dies wird im weiteren Verlauf des Dokuments verwendet.

  • Zeile 17: die Verbindungszeichenfolge. [Data Source]: der Name des Servers, auf dem das DBMS gehostet wird; [Initial Catalog]: der Name der Datenbank, in diesem Fall [rdvmedecins-ef]; [User Id]: der Eigentümer der Verbindung; [Password]: das Passwort des Eigentümers. Der Leser sollte diese Zeichenfolge an seine Umgebung anpassen;
  • Zeilen 21–29: Definieren Sie eine [DbProviderFactory]. Ich weiß nicht, was das ist. Dem Namen nach zu urteilen, könnte es sich um eine Klasse handeln, die zur Generierung der [ADO.NET]-Schicht dient, die EF vom DBMS trennt:

Eigentlich sind diese Zeilen für SQL Server überflüssig, aber ich musste sie für andere DBMS hinzufügen. Daher füge ich sie hier als Referenz ein. Sie verursachen keine Probleme. Der einzige wichtige Punkt ist die Version in Zeile 27. Es handelt sich um die Version der [System.Data]-DLL, die in den Projektreferenzen aufgeführt ist:

So, das war's. Wir sind bereit. Wir führen das Projekt aus und erhalten die folgende Datenbank [rdvmedecins-ef]:

 

Das wird unsere endgültige Datenbank sein. Jetzt müssen wir sie nur noch mit Daten füllen.

3.4.5. Befüllen der Datenbank

Die Datenbankinitialisierungsklasse kann verwendet werden, um Daten in die Datenbank einzufügen:


public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
    // database initialization
    public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
    {
      protected override void Seed(RdvMedecinsContext context)
      {
        base.Seed(context);
        // initialize the base
        // our customers
        Client[] clients ={
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
        foreach (Client client in clients)
        {
          context.Clients.Add(client);
        }
        // the doctors
        Medecin[] medecins ={
        new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
        new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
        new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
        new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
     };
        foreach (Medecin medecin in medecins)
        {
          context.Medecins.Add(medecin);
        }
        // time slots
        Creneau[] creneaux ={
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
      };
        foreach (Creneau creneau in creneaux)
        {
          context.Creneaux.Add(creneau);
        }
        // dates
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
      }
 
    }
  }
  • Zeile 6: Die Initialisierung erfolgt in der Methode [Seed]. Diese Methode existiert in der übergeordneten Klasse. Sie wird hier neu definiert. Das Argument ist der Persistenzkontext der Anwendung [RdvMedecinsContext];
  • Zeile 8: Das Argument wird an die übergeordnete Klasse übergeben; es ist wahrscheinlich, dass die übergeordnete Klasse den an sie übergebenen Persistenzkontext öffnet, da dieser Kontext später nicht mehr geöffnet werden muss;
  • Zeilen 11–16: Erstellung von 4 Clients;
  • Zeilen 17–20: Diese werden dem Persistenzkontext hinzugefügt, genauer gesagt dessen Ärzten. Beachten Sie die Methode [Add], die dies ermöglicht. Erinnern Sie sich an die Definition des Kontexts:

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

Es wird auch gesagt, dass die Clients an den Kontext angehängt wurden, d. h., sie werden nun von EF verwaltet. Zuvor waren sie abgekoppelt. Sie existierten als Objekte, wurden aber nicht von EF verwaltet;

  • Zeilen 21–27: Erstellung von 4 Ärzten;
  • Zeilen 28–31: Hinzufügen zum Persistenzkontext;
  • Zeilen 33–70: Erstellung von Zeitfenstern. Zeilen 34–57 für doctor medecins[0], Zeilen 58–69 für doctor medecins[1]. Die anderen Ärzte haben keine Zeitfenster;
  • Zeilen 71–74: Diese Zeitfenster werden in den Persistenzkontext aufgenommen;
  • Zeile 76: Erstellung eines Termins für den ersten Kunden mit dem ersten Zeitfenster und Aufnahme in den Persistenzkontext.

Wenn das Projekt ausgeführt wird, ergibt sich folgende Datenbank:

Oben sehen wir die gefüllte Tabelle [CLIENTS].

3.4.6. Entitäten ändern

Derzeit sind die Klassen [Doctor] und [Client] nahezu identisch. Wenn wir die für die Persistenzverwaltung mit EF 5 hinzugefügten Felder entfernen, sind sie sogar identisch. Wir lassen sie von einer Klasse [Person] ableiten. Diese beiden Entitäten sehen dann wie folgt aus:


// a person
  public abstract class Personne
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
 
    // signature
    public override string ToString()
    {
      return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // short signature
    public string ShortIdentity()
    {
      ...
    }
 
    // utility
    private string dump(byte[] timestamp)
    {
      ...
    }
 
  }
 
  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin : Personne
  {
    // the doctor's time slots
    public ICollection<Creneau> Creneaux { get; set; }
    // signature
    public override string ToString()
    {
      return String.Format("Medecin {0}", base.ToString());
    }
  }
 
[Table("CLIENTS", Schema = "dbo")]
    public class Client : Personne
  {
    // customer rvs
    public ICollection<Rv> Rvs { get; set; }
    // signature
    public override string ToString()
    {
      return String.Format("Client {0}", base.ToString());
    }
  }

Wenn das Projekt ausgeführt wird, wird dieselbe Datenbank generiert. EF 5 hat jede der untersten Klassen in der Vererbungshierarchie einer separaten Tabelle zugeordnet. Tatsächlich verfügt EF 5 über verschiedene Strategien zur Tabellengenerierung, um die Entitätsvererbung in „ “ darzustellen. Wir werden hier nicht näher darauf eingehen. Sie können beispielsweise den Artikel „ Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type“ unter der URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per] lesen.

Wir werden nun diese Version der Entitäten verwenden.

3.4.7. Hinzufügen von Einschränkungen zur Datenbank

Es gibt noch ein weiteres Detail zu beachten. Die [RVS]-Tabelle für Termine sieht wie folgt aus:

 

Diese Tabelle muss eine Eindeutigkeitsbeschränkung aufweisen: Für einen bestimmten Tag kann ein Zeitfenster eines Arztes nur einmal für einen Termin gebucht werden. In Bezug auf die Tabelle bedeutet dies, dass das Paar (DAY, SLOT_ID) eindeutig sein muss. Ich weiß nicht, ob diese Beschränkung direkt im Code ausgedrückt werden kann, sei es auf den Entitäten oder im Kontext. Es ist wahrscheinlich, aber ich habe es nicht überprüft. Wir werden einen anderen Ansatz wählen. Wir werden einen SQL Server-Verwaltungsclient verwenden, um diese Einschränkung hinzuzufügen.

Mit „SQL Server Management Studio“ habe ich keine einfachere Möglichkeit gefunden, diese Einschränkung hinzuzufügen, als die SQL-Anweisung auszuführen, die sie erstellt:

  • In [1] erstellen wir eine SQL-Abfrage für die Datenbank [rdvmedecins-ef];
  • in [2] die SQL-Abfrage, die die Eindeutigkeitsbeschränkung erstellt;
  • In [3] wurde durch die Ausführung dieser Abfrage ein neuer Index in der Tabelle [RVS] erstellt.

Es gibt noch weitere SQL Server-Verwaltungstools. Hier verwenden wir das Freeware-Tool „EMS SQL Manager for SQL Server“ [http://www.sqlmanager.net/fr/products/mssql/manager/download]. Nach der Installation starten wir es:

  • In [1] registrieren wir eine Datenbank;
  • in [2] stellen wir eine Verbindung zum (lokalen) Server her;
  • in [3] verwenden wir die SQL Server-Authentifizierung;
  • in [4] unter dem Benutzernamen sa;
  • in 5 und dem Passwort sqlserver2012;
  • in [6] fahren wir mit dem nächsten Schritt fort;
  • in [7] wählen Sie die Datenbank [rdvmedecins-ef] aus;
  • Beenden Sie in [8] den Assistenten;
  • in [9] erscheint die Datenbank in der Datenbankansicht. Stellen Sie eine Verbindung her [10];
  • in [11] sind Sie verbunden.

Mit „SQL Manager Lite for SQL Server“ können Sie die Eindeutigkeitsbeschränkung für die Tabelle [RVS] erstellen.

  • In [1] sehen Sie die zuvor erstellte Eindeutigkeitsbeschränkung;
  • Löschen Sie diese in [2];
  • In [3] ist der Index, der dieser Eindeutigkeitsbeschränkung entspricht, verschwunden.

Wir erstellen die gelöschte Einschränkung neu:

  • In [1] erstellen wir einen neuen Index für die Tabelle [RVS];
  • in [2] geben wir ihm einen Namen;
  • in [3] handelt es sich um eine Eindeutigkeitsbeschränkung;
  • in [4] für die Spalten DAY und SLOT_ID;

Die Registerkarte „DDL“ enthält den auszuführenden SQL-Code:

  • In [6] kompilieren wir die SQL-Anweisung;
  • in [7] bestätigen wir;
  • in [8] ist der neue Index erschienen.

Die Benutzeroberfläche von „SQL Manager Lite for SQL Server“ ähnelt der von „SQL Server Management Studio“. Ähnliche Benutzeroberflächen sind für Oracle-, PostgreSQL-, Firebird- und MySQL-Datenbanken verfügbar. Wir werden daher mit dieser Familie von Datenbankverwaltungstools fortfahren.

Um auf Informationen zu einer Tabelle zuzugreifen, doppelklicken Sie einfach darauf:

Informationen zur ausgewählten Tabelle werden in Registerkarten angezeigt. Oben sehen wir die Registerkarte [Felder] für die Tabelle [CLIENTS]. Die Registerkarte [Daten] zeigt den Inhalt der Tabelle an:

Image

3.4.8. Die endgültige Datenbank

Wir haben nun unsere endgültige Datenbank. Wir exportieren ihr SQL-Skript, damit wir sie bei Bedarf neu erstellen können.

  • in [1], Start des Assistenten;
  • in [2], der Server;
  • in [3] die zu exportierende Datenbank;
  • Geben Sie in [4] den Namen der Datei an, in der das SQL-Skript gespeichert werden soll;
  • Geben Sie in 5 die Kodierung an;
  • Geben Sie in [6] an, was Sie extrahieren möchten (Tabellen, Einschränkungen, Daten);
  • in [7] können Sie das zu generierende Skript anpassen;
  • in [8] schließen Sie den Assistenten ab.

Das Skript wurde generiert und in den Skript-Editor geladen. Sie können den generierten SQL-Code anzeigen. Wir werden die Datenbank mit diesem Skript neu erstellen.

  • Löschen Sie in [1] die Datenbank;
  • In [2] und [3] erstellen wir sie neu;
  • In [4] melden Sie sich an;
  • führen Sie in 5 das SQL-Skript aus, um die Datenbank anzulegen;
  • in [6] speichern wir es im „SQL Manager“;
  • In [7] stellen wir eine Verbindung zu der soeben erstellten Datenbank her;
  • In [8] enthält die Datenbank derzeit keine Tabellen;
  • Öffnen Sie in [9a] einen SQL-Skript-Editor;
  • Öffnen Sie in [9b] das zuvor erstellte SQL-Skript;
  • Führen Sie es in [10] aus;
  • In [11] wurden die Tabellen erstellt;
  • in [12] werden sie gefüllt;
  • in [14] sehen wir die eindeutige Einschränkung, die wir für die Tabelle [RVS] erstellt haben.

Wir werden nun mit dieser bestehenden Datenbank arbeiten. Sollte sie zerstört oder beschädigt werden, wissen wir, wie wir sie neu erstellen können.

3.5. Arbeiten mit der Datenbank unter Verwendung von Entity Framework

Wir werden:

  • Datenbankelemente hinzufügen, löschen und ändern;
  • die Datenbank mit LINQ to Entities abfragen;
  • den gleichzeitigen Zugriff auf dasselbe Datenbankelement verwalten;
  • die Konzepte von Lazy Loading und Eager Loading verstehen;
  • feststellen, dass Datenbankaktualisierungen über den Persistenzkontext innerhalb einer Transaktion erfolgen.

3.5.1. Elemente aus dem Persistenzkontext löschen

Wir haben eine gefüllte Datenbank. Wir werden sie leeren. Wir erstellen eine neue Klasse [Erase.cs] im aktuellen Projekt [1]:

Die Klasse [Erase] sieht wie folgt aus:


using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
  class Erase
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // empty the current base
        // our customers
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        // the doctors
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // save the persistence context
        context.SaveChanges();
      }
    }
  }
}
  • Zeile 9: Operationen auf einem Persistenzkontext werden immer innerhalb eines [using]-Blocks ausgeführt. Dadurch wird sichergestellt, dass der Kontext geschlossen wird, sobald der [using]-Block endet;
  • Zeile 13: Wir durchlaufen den Client-Kontext [context.Clients]. Alle Clients in der Datenbank werden in den Persistenzkontext aufgenommen;
  • Zeile 15: Für jeden von ihnen führen wir die [Remove]-Operation durch, die sie aus dem Kontext entfernt. Tatsächlich befinden sie sich weiterhin im Kontext, jedoch im Status „entfernt“;
  • Zeilen 18–21: Wir machen dasselbe für die Ärzte;
  • Zeile 23: Wir speichern den Persistenzkontext in der Datenbank.

Beim Speichern des Kontexts in der Datenbank werden Entitäten im Kontext, die:

  • einen Null-Primärschlüssel haben, werden einer SQL-INSERT-Operation unterzogen;
  • sich im Status „gelöscht“ befinden, werden einer SQL-DELETE-Operation unterzogen;
  • sich im Status „modifiziert“ befinden, werden einer SQL-UPDATE-Operation unterzogen;

Wie wir später sehen werden, werden diese SQL-Operationen innerhalb einer Transaktion ausgeführt. Wenn eine davon fehlschlägt, wird alles, was zuvor getan wurde, rückgängig gemacht.

Machen wir das Programm [Erase] zum neuen Ausgangspunkt für Projekt [1] und führen wir das Projekt anschließend aus.

Schauen wir uns die Datenbank an. Wir werden sehen, dass alle Tabellen leer sind [2]. Das ist überraschend, da wir lediglich darum gebeten haben, die Ärzte und Kunden zu löschen. Durch den Mechanismus der Fremdschlüssel wurden die anderen Tabellen kaskadierend geleert.

Die Fremdschlüsseldefinition von der Tabelle [CRENEAUX] zur Tabelle [MEDECINS] wurde vom EF 5-Anbieter wie folgt definiert:

  • Wählen Sie in [1] die Tabelle [CRENEAUX] aus;
  • Wählen Sie in [2] die Registerkarte „Fremdschlüssel“ aus;
  • Bearbeiten Sie in [3] den einzelnen Fremdschlüssel;
  • in [4] auf der Registerkarte „DDL“ die SQL-Definition der Fremdschlüsselbeschränkung;
  • in 5 stellt die ON DELETE CASCADE-Klausel sicher, dass das Löschen eines Arztes zum Löschen der mit ihm verbundenen Zeitfenster führt.

Die Fremdschlüsselbeschränkungen für die Tabelle [RVS] werden auf ähnliche Weise definiert:

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
  • Zeilen 1–6: Beim Löschen eines Kunden werden auch die damit verbundenen Termine gelöscht;
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
  • Zeilen 1–6: Beim Löschen eines Zeitfensters werden auch alle damit verbundenen Termine gelöscht.

3.5.2. Hinzufügen von Elementen zum Persistenzkontext

Nachdem wir die Datenbank geleert haben, füllen wir sie nun wieder auf. Wir fügen das Programm [Fill.cs] [1] zum Projekt hinzu.

Das Programm [Fill.cs] sieht wie folgt aus:


using RdvMedecins.Entites;
using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
  class Fill
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // empty the current base
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // reset it
        // our customers
        Client[] clients ={
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
        foreach (Client client in clients)
        {
          context.Clients.Add(client);
        }
        // the doctors
        Medecin[] medecins ={
        new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
        new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
        new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
        new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
     };
        foreach (Medecin medecin in medecins)
        {
          context.Medecins.Add(medecin);
        }
        // time slots
        Creneau[] creneaux ={
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
        new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
        new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
        new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
      };
        foreach (Creneau creneau in creneaux)
        {
          context.Creneaux.Add(creneau);
        }
        // dates
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // save the persistence context
        context.SaveChanges();
      }
    }
  }
}
  • Zeile 10: Wir öffnen den Persistenzkontext;
  • Zeilen 13–20: Zeilen aus den Tabellen [CLIENTS] und [DOCTORS] werden dem Kontext hinzugefügt und anschließend wieder daraus entfernt. Wir haben gerade gesehen, dass dies die Datenbank vollständig geleert hat;
  • Zeilen 22–88: Elemente werden dem Persistenzkontext hinzugefügt. Alle haben einen Null-Primärschlüssel. Sie werden daher in die Datenbank eingefügt;
  • Zeile 90: Die am Kontext vorgenommenen Änderungen werden mit der Datenbank synchronisiert. In der Datenbank wird eine Reihe von SQL-DELETE-Operationen durchgeführt, gefolgt von einer Reihe von SQL-INSERT-Operationen;

Wir machen das Programm [Fill] zum neuen Startobjekt für Projekt [1] und führen es anschließend aus.

In [2] sehen wir, dass die Tabellen gefüllt wurden.

3.5.3. Anzeigen des Datenbankinhalts

Wir werden nun den Inhalt der Datenbank mithilfe von LINQ to Entities-Abfragen anzeigen. LINQ (Language-Integrated Query) wurde 2007 mit dem .NET Framework 3.5 eingeführt. Es fungiert als Erweiterung der .NET-Sprachen, d. h., es ist in die Sprache integriert und seine Syntax wird vom Compiler validiert. Es ermöglicht Ihnen, verschiedene Sammlungen mit einer Syntax abzufragen, die der von SQL (Structured Query Language) für Datenbankabfragen ähnelt. Es gibt verschiedene Versionen von LINQ:

  • LINQ to Objects, für Abfragen in In-Memory-Sammlungen;
  • LINQ to XML, für die Abfrage von XML;
  • LINQ to Entity, für die Abfrage von Datenbanken;

LINQ stützt sich auf zahlreiche Erweiterungen der .NET-Sprachen. Diese können auch außerhalb von LINQ verwendet werden. Wir werden sie hier nicht vorstellen, sondern lediglich zwei Referenzen angeben, in denen der Leser eine ausführliche Beschreibung von LINQ finden kann:

  • LINQ in Action, von Fabrice Marguerie, Steve Eichert und Jim Wooley, erschienen bei Manning;
  • LINQ Pocket Reference, von Joseph und Ben Albahari, erschienen bei O’Reilly.

Ich habe das erste Buch gelesen und fand es ausgezeichnet. Das zweite habe ich noch nicht gelesen, aber ich habe „C# 3.0 in a Nutshell“ von denselben Autoren gelesen, als LINQ veröffentlicht wurde. Ich fand, dass dieses Buch weit über dem Durchschnitt der Bücher lag, die ich normalerweise lese. Es scheint, dass die anderen Bücher dieser beiden Autoren vom gleichen Kaliber sind. Wir werden auch LINQPad verwenden, ein von Joseph Albahari geschriebenes LINQ-Lernwerkzeug.

Wir werden die Entitäten in der Datenbank anzeigen. Dazu fügen wir ihren Klassen zwei Anzeigemethoden hinzu. Beginnen wir mit der Entität [Doctor]:


// a doctor
  public class Medecin
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // the doctor's time slots
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
 
    // signature
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // short signature
    public string ShortIdentity()
    {
      return ToString();
    }
 
    // utility
    private string dump(byte[] timestamp){
      string str = "";
      foreach (byte b in timestamp)
      {
        str += b;
      }
      return str;
    }
  }
  • Zeilen 27–30: die ToString-Methode der Klasse. Beachten Sie, dass sie die Sammlung aus Zeile 21 nicht anzeigt;
  • Zeilen 32–37: die Methode ShortIdentity, die dasselbe tut.

An dieser Stelle müssen wir die Konzepte des Lazy und Eager Loading erläutern, um die Auswirkungen der beiden vorangegangenen Methoden zu bewerten. Wir haben gesehen, dass eine Entität Abhängigkeiten zu einer anderen Entität haben kann. Diese Abhängigkeiten sind zweierlei Art:

  • Eins-zu-Viele, wie oben, wo ein Arzt mit mehreren Zeitfenstern verknüpft ist;
  • Viele-zu-Eins, wie bei der Entität [Slot] unten, bei der ein oder mehrere Zeitfenster mit demselben Arzt verknüpft sind;

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

Wenn Abhängigkeiten gleichzeitig mit den Entitäten geladen werden, an die sie angehängt sind, spricht man von Eager Loading. Andernfalls spricht man von Lazy Loading: Abhängigkeiten werden erst geladen, wenn zum ersten Mal auf sie verwiesen wird. Standardmäßig verwendet EF 5 Lazy Loading: Abhängigkeiten werden nicht gleichzeitig mit der Entität geladen.

Schauen wir uns unsere [ToString]-Methode oben an:


    // the doctor's time slots
    public ICollection<Creneau> Creneaux { get; set; }
 
    // signature
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // short signature
    public string ShortIdentity()
    {
      return ToString();
}

Die [ToString]-Methode zeigt die [Slots]-Abhängigkeit in Zeile 2 nicht an. Wäre dies der Fall, hätte dies das Laden aller Slots des Arztes vor der Ausführung erzwungen. Um dieses aufwendige Laden zu vermeiden, wurde die Abhängigkeit nicht in die Signatur der Entität aufgenommen. Im Allgemeinen nehmen wir zwei Signaturen in jede Entität auf:

  • eine ToString-Methode, die die Entität und alle Eins-zu-Viele-Abhängigkeiten anzeigt. Wie gerade erläutert, löst dies das Laden der Abhängigkeit aus;
  • eine ShortIdentity-Methode, die keine Abhängigkeiten referenziert. Daher werden keine Abhängigkeiten geladen;

Die Anzeigemethoden für die anderen Entitäten lauten wie folgt:

Die Entität [Client]:


  public class Client
  {
    // data
    ...
    // customer rvs
    public ICollection<Rv> Rvs { get; set; }
 
    // signature
    public override string ToString()
    {
      return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // short signature
    public string ShortIdentity()
    {
      return ToString();
    }
 
}
  • Zeilen 9–12: Die [ToString]-Methode zeigt die Abhängigkeit in Zeile 6 nicht an;

Die Entität [Creneau]:


public class Creneau
  {
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    // niche Rvs
    public ICollection<Rv> Rvs { get; set; }
 
    // signature
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
    // short signature
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
    }
  }
  • Zeile 16: Die [ToString]-Methode verweist auf die Abhängigkeit in Zeile 9. Dadurch wird sie zwangsläufig geladen;
  • Zeile 11: Die Abhängigkeit [Rvs] wird nicht referenziert. Sie wird nicht geladen;
  • Zeilen 21–22: Die Methode [ShortIdentity] verweist nicht mehr auf die Referenz [Medecin] aus Zeile 9. Daher wird sie nicht geladen.

Die Entität [Rv]:


public class Rv
  {
    // data
    ...
    [Column("CLIENT_ID")]
    public int ClientId { get; set; }
    [ForeignKey("ClientId")]
    [Required]
    public virtual Client Client { get; set; }
    [Column("CRENEAU_ID")]
    public int CreneauId { get; set; }
    [ForeignKey("CreneauId")]
    [Required]
    public virtual Creneau Creneau { get; set; }
 
    // signature
    public override string ToString()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
    }
    // short signature
    public string ShortIdentity()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
    }
 
  }
  • Zeilen 17–20: Die Methode [ToString] verweist auf die Abhängigkeiten in den Zeilen 9 und 14. Dadurch werden diese zwangsläufig geladen;
  • Zeilen 17–20: Die Methode [ShortIdentity] verhindert dies, sodass die Abhängigkeiten nicht geladen werden.

Zusammenfassend lässt sich sagen, dass wir auf die [ToString]-Methoden von Entitäten achten müssen. Wenn wir dies nicht beachten, kann die Anzeige einer Tabelle die Hälfte der Datenbank laden, wenn die Tabelle viele Abhängigkeiten hat.

Vor diesem Hintergrund schreiben wir den folgenden neuen Code [Dump.cs]:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
 
namespace RdvMedecins_01
{
  class Dump
  {
    static void Main(string[] args)
    {
      // base dump
      using (var context = new RdvMedecinsContext())
      {
        // our customers
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // the doctors
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // time slots
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // dates
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}

Wir werden die Zeilen 17–21 erläutern, in denen die [Client]-Entitäten angezeigt werden. Die Erläuterung gilt auch für die anderen Entitäten.


        // our customers
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
}
  • Zeile 3: Das Schlüsselwort „var“ wurde mit C# 3.0 eingeführt. Es ermöglicht es Ihnen, die genaue Typangabe einer Variablen zu vermeiden. Der Compiler leitet den Typ dann aus dem Typ des Ausdrucks ab, der der Variablen zugewiesen wird;
  • Zeile 3: Der der Variablen „clients“ zugewiesene Ausdruck ist eine LINQ-to-Entities-Abfrage. Er enthält SQL-Schlüsselwörter, die in LINQ portiert wurden. Die hier verwendete Syntax lautet wie folgt:

from variable in DbSet select variable

Eine allgemeinere LINQ-Syntax lautet


from variable in collection select variable

Die Sammlung wird durchlaufen, und für jedes Element darin wird die Variable ausgewertet. Dies geschieht erst, wenn die Variable [clients] aus Zeile 3 durch die for/each-Schleife in den Zeilen 4–7 durchlaufen wird. Bis dies geschieht, ist die Variable [clients] lediglich eine nicht ausgewertete Abfrage;

  • Zeile 4: Die Abfrage [clients] wird durchlaufen. Dies erzwingt die Auswertung der Abfrage. Die Zeilen der Tabelle [CLIENTS] werden nacheinander in den Persistenzkontext übernommen;
  • Zeile 6: Die [ToString]-Methode der [Client]-Entität wird zur Anzeige verwendet. Es werden keine Abhängigkeiten geladen;

Kommen wir nun zu den folgenden Codezeilen:

  • Zeilen 24–28: Die Zeilen der Tabelle [DOCTORS] werden in den Persistenzkontext übernommen und angezeigt. Es werden keine Abhängigkeiten geladen;
  • Zeilen 31–35: Die Zeilen der Tabelle [SLOTS] werden in den Persistenzkontext geladen und angezeigt. Wir haben gesehen, dass die [ToString]-Methode dieser Entität die Abhängigkeit [Doctor] anzeigt. Diese ist jedoch bereits geladen. Daher erfolgt kein erneutes Laden;
  • Zeilen 38–42: Die Zeilen der Tabelle [RVS] werden in den Persistenzkontext geladen und angezeigt. Wir haben gesehen, dass die [ToString]-Methode dieser Entität die Abhängigkeiten [Client] und [Slot] anzeigt. Diese sind jedoch bereits geladen. Daher findet kein neues Laden statt.

Beachten Sie, dass die Anzeigereihenfolge nicht neutral ist. Hätten wir die [Rv]-Entitäten zuerst anzeigen wollen, hätte deren [ToString]-Methode das Laden der mit diesen Terminen verknüpften [Client]- und [Creneau]-Entitäten ausgelöst. Die anderen wären nicht geladen worden. Sie wären später in einer anderen Ansicht geladen worden. Dies hat Auswirkungen auf die Leistung. Der vorherige Code benötigt vier SQL-Anweisungen, um alle Entitäten anzuzeigen. Nehmen wir nun an, wir fragen zunächst die [RVS]-Tabelle der Termine ab. Für die [RVS]-Tabelle ist eine erste SQL-Abfrage erforderlich. Als Nächstes löst die [ToString]-Methode der [Rv]-Entität das mögliche Laden der zugehörigen [Client]- und [Slot]-Entitäten aus. Für jede ist eine SQL-Abfrage erforderlich. Angenommen, es gibt N2 Kunden und N3 Zeitfenster und alle diese Entitäten werden in der [RVS]-Tabelle referenziert, dann erfordert die Anzeige 1+N2+N3 SQL-Abfragen. Daher ist die Leistung geringer als in der von uns untersuchten Version. Um die [RVS]-Tabelle mit ihren Abhängigkeiten anzuzeigen, wäre ein Tabellen-Join erforderlich. Dies lässt sich mit LINQ erreichen. Wir werden darauf anhand eines Beispiels zurückkommen. Merken wir uns vorerst, dass wir auf die SQL-Abfragen achten müssen, die unserem LINQ-Code zugrunde liegen.

Wir konfigurieren das Projekt so, dass dieser neue Code [1] und [2] ausgeführt wird, und führen ihn dann aus:

Die Konsolenausgabe lautet wie folgt:

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 lernen mit LINQPad

Oben haben wir LINQ-to-Entity-Abfragen verwendet, um den Inhalt der Datenbanktabellen anzuzeigen. Joseph Albahari hat ein Programm geschrieben, das Ihnen hilft, die verschiedenen Formen von LINQ zu erlernen. Wir stellen es Ihnen nun vor.

LINQPad ist unter der folgenden URL verfügbar [http://www.linqpad.net/]. Nach der Installation starten wir es [1]:

LINQ-Einsteiger können mit den Beispielen auf der Registerkarte [Samples] [2] beginnen, die eine große Auswahl an Beispielen bietet. Wählen wir das Beispiel [3] aus, das sich dann in einem neuen Fenster öffnet [4]. Der vollständige Code für das Beispiel lautet wie folgt:


// Now for a simple LINQ-to-objects query expression (notice no semicolon):
 
from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word
 
 
// Feel free to edit this... (no-one's watching!) You'll be prompted to save any
// changes to a separate file.
//
// Tip:  You can execute part of a query by highlighting it, and then pressing F5.

Die Zeilen 3–5 sind ein Beispiel für eine LINQ to Objects-Abfrage. Die LINQ-Abfrage folgt der folgenden Syntax:


from variable in collection orderby élément1 select élément2
  • variable bezieht sich auf das aktuelle Element in der Sammlung. In unserem Beispiel ist diese Sammlung die Liste der Wörter, die aus der geteilten Zeichenkette resultiert;
  • Die Sammlung wird gemäß dem Parameter element1 von orderby sortiert. In unserem Beispiel wird die Sammlung von Wörtern nach Länge sortiert;
  • Das Schlüsselwort select gibt an, was wir aus der aktuellen Variablen element in der Sammlung extrahieren möchten. In unserem Beispiel ist dies das Wort.

Führen wir diese LINQ-Abfrage aus:

  • in [1]: Ein LINQ-Ausdruck wird durch Drücken von [F5] oder über die Schaltfläche „Ausführen“ ausgeführt;
  • in [2]: die Anzeige. Die Wörter werden in der Reihenfolge ihrer Länge angezeigt. Dieses einfache Beispiel veranschaulicht die Leistungsfähigkeit von LINQ;
  • in [3] können Sie weitere Beispiele herunterladen, darunter auch solche aus dem Buch „LINQ in Action“ [4];
  • in 5 wählen wir ein Beispiel aus dem Buch aus;

string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
 
// Group words by length
var groups =
  from word in words
  orderby word ascending
  group word by word.Length into lengthGroups
  orderby lengthGroups.Key descending
  select new { Length = lengthGroups.Key, Words = lengthGroups };
 
// Print each group out
foreach (var group in groups)
{
  Console.WriteLine("Words of length " + group.Length);
  foreach (string word in group.Words)
    Console.WriteLine("  " + word);
}
  • Zeile 4: eine neue LINQ-Abfrage mit neuen Schlüsselwörtern;
  • Zeile 5: Die abgefragte Sammlung ist das Array mit den Wörtern aus Zeile 1;
  • Zeile 6: Die Sammlung wird alphabetisch nach Wörtern sortiert;
  • Zeile 7: Die Sammlung wird nach (Schlüsselwort) in eine neue Sammlung namens lengthGroups gruppiert. lengthGroups.Key steht für den Gruppierungsfaktor (Schlüsselwort nach), hier die Länge der Wörter. lengthGroups fasst Wörter mit demselben Gruppierungsfaktor, d. h. derselben Länge, zusammen;
  • Zeile 8: Die Sammlung lengthGroups wird nach Gruppierungsschlüssel in absteigender Reihenfolge sortiert, hier also nach abnehmender Wortlänge;
  • Zeile 9: Aus dieser Sammlung werden neue Objekte (anonyme Klassen) mit zwei Feldern erstellt:
    • Length: die Länge der Wörter,
    • Words: die Wörter dieser Länge;

Hier wird besonders der Vorteil des Schlüsselworts var in Zeile 4 deutlich. Da wir in Zeile 9 eine anonyme Klasse verwendet haben, können wir den Typ der Variablen groups nicht angeben. Der Compiler weist der anonymen Klasse jedoch einen internen Namen zu und verwendet diesen, um die Variable groups zu typisieren. Er kann dann feststellen, ob die Variable groups korrekt verwendet wird

  • Zeile 12: Iteration über die Abfrage aus Zeile 4. Erst an dieser Stelle wird sie ausgewertet. Erinnern Sie sich daran, dass ihre Ausführung eine Sammlung von Objekten erzeugt, die in Zeile 9 angegeben ist;
  • Zeile 14: Wir zeigen die Eigenschaft „Length“ des aktuellen Elements an, d. h. die Länge der Wörter;
  • Zeilen 15–17: Wir zeigen jedes Element der Sammlung „Words“ an, d. h. die Menge der Wörter mit der zuvor angezeigten Länge.

Wenn wir diese Abfrage ausführen, erhalten wir in LINQPad das folgende Ergebnis:

 

Nachdem wir nun einige Beispiele für [LINQ to Object]-Abfragen gesehen haben, wollen wir uns [LINQ to Entity]-Abfragen ansehen, mit denen wir Datenbanken abfragen können. Zunächst stellen wir eine Verbindung zu der von uns erstellten und gefüllten SQL Server-Datenbank her:

  • In [1] fügen wir eine Verbindung zu einer Datenbank hinzu;
  • in [2] die Art des Zugriffs auf die Datenquelle. Um auf die SQL Server-Datenbank zuzugreifen, verwenden wir den [LINQPad Driver];
  • unter [3] ist es auch möglich, einen in einer .exe- oder .dll-Assembly definierten Persistenzkontext [DbContext] abzurufen (Option 3). Leider wird Entity Framework 5 zum heutigen Zeitpunkt (8. Oktober 2012) nicht unterstützt;
  • unter [4] können Treiber für andere DBMS als SQL Server heruntergeladen werden;
  • In 5 laden wir den Treiber für die DBMS MySQL und Oracle herunter;
  • in [6] den heruntergeladenen Treiber;
  • in [7] stellen wir eine Verbindung zu einer SQL-Server-Datenbank her;
  • in [8] befindet sich die Datenbank auf dem lokalen Server;
  • in [9] stellen wir die Verbindung mit den Anmeldedaten sa / sqlserver2012 her;
  • in [10] mit der von uns erstellten Datenbank [rdvmedecins-ef];
  • In [11] können Sie die Verbindung testen;
  • In [12] schließen Sie den Assistenten ab;
  • In [13] wird die Verbindung in LINQPad angezeigt.

Die Entitäten wurden aus der Tabelle [rdvmedecins-ef] erstellt. Es handelt sich um folgende:

  • In [1] steht [CLIENTS] für die Menge der [Client]-Entitäten. Jede Entität hat:
    • die Eigenschaften (ID, TITLE, LAST_NAME, FIRST_NAME, TIMESTAMP),
    • eine 1:n-Beziehung [CLIENTRVS];
  • In [2] repräsentiert [CRENEAUXes] die Menge der [Creneau]-Entitäten. Jede Entität hat:
    • die Eigenschaften (ID, START_TIME, MIN_TIME, END_TIME, MAX_TIME, DOCTOR_ID, TIMESTAMP),
    • eine 1:n-Beziehung [CRENEAURVS],
  • eine Viele-zu-Eins-Beziehung [DOCTOR];
  • In [3] repräsentiert die Entität [MEDECINS] die Menge der [Medecin]-Entitäten. Jede Entität hat:
    • die Eigenschaften (ID, TITLE, LAST_NAME, FIRST_NAME, TIMESTAMP),
    • eine 1:n-Beziehung [DOCTOR-SLOTS];
  • in [4] repräsentiert die Entität [RVS] die Menge der [Rv]-Entitäten. Jede Entität hat:
    • die Eigenschaften (ID, DAY, CLIENT_ID, SLOT_ID, TIMESTAMP),
    • eine Viele-zu-Eins-Beziehung [CLIENT],
    • eine Viele-zu-Eins-Beziehung [SLOT].

Beachte, dass sich die Namen der oben genannten Eigenschaften von den bisher verwendeten Namen unterscheiden. Das spielt jedoch keine Rolle. Wir wollen lediglich die Grundprinzipien der Datenbankabfrage erlernen.

Schauen wir uns an, wie wir diese Entitätsdatenbank abfragen können. Wir möchten beispielsweise eine Liste von Ärzten, sortiert nach ihrem TITLE und LAST_NAME:

  • in [1] erstellen wir eine neue Abfrage;
  • in [2] den Abfragetext;
  • in [3] das Ergebnis der Abfrage;
  • in [4] dieselbe Abfrage unter Verwendung von Lambda-Ausdrücken. Eine Abfrage mit Lambda-Ausdrücken ist weniger gut lesbar als eine Textabfrage, und Sie möchten diese vielleicht vermeiden. Manchmal sind sie jedoch unverzichtbar, da sie bestimmte Dinge ermöglichen, die Textabfragen nicht leisten können. Ein Lambda-Ausdruck bezeichnet eine Funktion mit einem Eingabeparameter a und einem Ausgabeparameter b in der Form a=>b. Die oben genannte OrderBy-Methode akzeptiert eine Lambda-Funktion als einzigen Parameter. Dies liefert den Parameter, nach dem eine Sammlung sortiert werden soll. Somit ist MEDECINS.OrderBy(m=>m.TITRE) die Liste der Ärzte, sortiert nach ihren Titeln. Die Anweisung sollte als Pipeline auf eine Sammlung gelesen werden. Die Sammlung der Ärzte wird als Eingabe für die OrderBy-Methode bereitgestellt. Diese Methode verarbeitet die [Doctor]-Entitäten nacheinander. Im Lambda-Ausdruck m=>m.TITLE steht m für die Eingabe der Lambda-Funktion. Sie kann beliebig benannt werden. Hier ist die Eingabe der Lambda-Funktion eine [Doctor]-Entität. Die Funktion m=>m.TITLE lässt sich wie folgt lesen: Wenn ich m als meine Eingabe (eine [Doctor]-Entität) bezeichne, dann ist meine Ausgabe m.TITLE, d. h. der Titel des Arztes. MEDECINS.OrderBy(m=>m.TITRE) ist wiederum eine Sammlung, nämlich die Sammlung der Ärzte, sortiert nach ihren Titeln. Diese neue Sammlung kann in eine andere Methode eingespeist werden, in diesem Beispiel die ThenBy-Methode. Diese Methode funktioniert nach dem gleichen Prinzip. Sie wird verwendet, um zusätzliche Parameter für die Sortierung der Sammlung anzugeben.

Das Lesen des Lambda-Codes, der dem Textcode entspricht, den wir normalerweise eingeben, ist eine gute Möglichkeit, ihn zu erlernen;

  • in 5 die an die Datenbank gesendete SQL-Abfrage. Auch hier werden wir diesen Code sorgfältig lesen. Er ermöglicht es uns, die tatsächlichen Kosten einer LINQ-Abfrage zu bewerten.

Im Folgenden stellen wir einige Beispiele für LINQ-Abfragen vor. Für jede davon zeigen wir die angezeigten Ergebnisse sowie den entsprechenden Lambda- und SQL-Code. Um diese Abfragen zu verstehen, müssen wir uns die Many-to-One-Beziehungen ins Gedächtnis rufen, die Entitäten miteinander verbinden. Über diese navigieren wir von einer Entität zur anderen. Sie werden als Navigations-Eigenschaften bezeichnet.

// Kunden, deren Anrede „Herr“ lautet, sortiert in absteigender Reihenfolge nach dem Namen

Ergebnisse:

 
LINQ

from client in CLIENTS where client.TITLE=="Mr"
orderby client.NAME absteigend   select client
Lambda

CLIENTS
.Where (client => (client.TITLE == "Mr"))
.OrderByDescending (client => client.LAST_NAME)
SQL

-- Regionsparameter
DECLARE @p0 NVarChar(1000) = 'Herr'
-- EndRegion
SELECT [t0].[ID], [t0].[TITLE], [t0].[LAST_NAME], [t0].[FIRST_NAME],
 [t0].[TIMESTAMP]
FROM [CUSTOMERS] AS [t0]
WHERE [t0].[TITLE] = @p0
SORTIEREN NACH [t0].[NACHNAME] ABSTEIGEND

// alle Zeitfenster mit dem zugehörigen Arzt

Ergebnisse (Auszug):

 
LINQ

from slot in SLOTS
Wähle new { start_time=slot.START_TIME, end_time=slot.END_TIME,
 mf=slot.MFIN, doctor=slot.DOCTOR}
Lambda
SQL

SELECT [t0].[START_TIME] AS [hd], [t0].[START_DATE] AS [md], [t0].[END_TIME] AS [hf],
 [t0].[ENDTIME] AS [mf], [t1].[ID], [t1].[TITLE], [t1].[LAST_NAME], [t1].[FIRST_NAME],
 [t1].[TIMESTAMP]
FROM [SLOTS] AS [t0]
INNER JOIN [DOCTORS] AS [t1]
 ON [t1].[ID] = [t0].[DOCTOR_ID]

// alle Termine mit dem jeweiligen Patienten und Arzt

Ergebnisse:

 
LINQ

from rv in RVS select new { rv=rv.CLIENT, doctor=rv.SLOT.DOCTOR}
Lambda
SQL

SELECT [t1].[ID], [t1].[TITLE], [t1].[LAST_NAME], [t1].[FIRST_NAME], [t1].[TIMESTAMP],
 [t3].[ID] AS [ID2], [t3].[TITLE] AS [TITLE2], [t3].[LAST_NAME] AS [LAST_NAME2],
 [t3].[VORNAME] AS [VORNAME2], [t3].[ZEITSTAMP] AS [ZEITSTAMP2]
FROM [RVS] AS [t0]
INNER JOIN [CUSTOMERS] AS [t1] ON [t1].[ID] = [t0].[CUSTOMER_ID]
INNER JOIN [SLOTS] AS [t2] ON [t2].[ID] = [t0].[SLOT_ID]
INNER JOIN [DOCTORS] AS [t3] ON [t3].[ID] = [t2].[DOCTOR_ID]

// Ärzte ohne Termine

Ergebnisse:

 
LINQ
 
Lambda
SQL

SELECT [t0].[ID], [t0].[TITLE], [t0].[LAST_NAME], [t0].[FIRST_NAME], [t0].[TIMESTAMP]
FROM [DOCTORS] AS [t0]
WHERE NOT (EXISTS(
    SELECT NULL AS [EMPTY]
    FROM [TERMINE] AS [t1]
    INNER JOIN [TERMINE] AS [t2] ON [t2].[ID] = [t1].[TERMIN_ID]
    INNER JOIN [ÄRZTE] AS [t3] ON [t3].[ID] = [t2].[ÄRZT_ID]
    WHERE [t3].[ID] = [t0].[ID]
    ))

Für diese Anforderung gibt es keine LINQ-Abfrage. Sie müssen Lambda-Ausdrücke verwenden. Dieser lautet wie folgt: Ich nehme die Sammlung der Ärzte (DOCTORS) und behalte (Where) nur jene Ärzte (m) bei, für die ich keinen Termin (rv) bei diesem Arzt (m) in der Termin-Sammlung (APPOINTMENTS) finden kann.

// Zeitfenster von Frau Pélissier

(Teil-)Ergebnisse:

 
LINQ

from slot in SLOTS where slot.DOCTOR.NAME=="Pelissier"
 wähle Zeitfenster
Lambda
SQL

-- Regionalparameter
DECLARE @p0 NVarChar(1000) = 'Pelissier'
-- EndRegion
SELECT [t0].[ID], [t0].[HSTART], [t0].[MSTART], [t0].[HEND], [t0].[MEND],
 [t0].[DOCTOR_ID], [t0].[TIMESTAMP]
FROM [SLOTS] AS [t0]
INNER JOIN [DOCTORS] AS [t1] ON [t1].[ID] = [t0].[DOCTOR_ID]
WHERE [t1].[LAST_NAME] = @p0

// Anzahl der Termine von Frau Pélissier am 8. Oktober 2012

Ergebnisse:

 
LINQ

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

-- Regionsparameter
DECLARE @p0 NVarChar(1000) = 'Pelissier'
DECLARE @p1 DateTime = '2012-10-08 00:00:00.000'
-- EndRegion
SELECT COUNT(*) AS [Wert]
FROM [RVS] AS [t0]
INNER JOIN [SLOTS] AS [t1] ON [t1].[ID] = [t0].[SLOT_ID]
INNER JOIN [DOCTORS] AS [t2] ON [t2].[ID] = [t1].[DOCTOR_ID]
WHERE ([t2].[NAME] = @p0) AND ([t0].[DAY] = @p1)

// Liste der Kunden, die am 08.10.2012 einen Termin bei Frau Pélissier vereinbart haben

Ergebnisse:

 
LINQ

from rv in RVS where (rv.DATE == new DateTime(2012, 10, 08)
 && rv.SLOT.DOCTOR.NAME=="Pelissier") select rv.CLIENT
Lambda
SQL

-- Regionsparameter
DECLARE @p0 DateTime = '2012-10-08 00:00:00.000'
DECLARE @p1 NVarChar(1000) = 'Pelissier'
-- EndRegion
SELECT [t3].[ID], [t3].[TITLE], [t3].[LAST_NAME], [t3].[FIRST_NAME], [t3].[TIMESTAMP]
FROM [RVS] AS [t0]
INNER JOIN [SLOTS] AS [t1] ON [t1].[ID] = [t0].[SLOT_ID]
INNER JOIN [DOCTORS] AS [t2] ON [t2].[ID] = [t1].[DOCTOR_ID]
INNER JOIN [CLIENTS] AS [t3] ON [t3].[ID] = [t0].[CLIENT_ID]
WHERE ([t0].[DAY] = @p0) AND ([t2].[NAME] = @p1)

// Anzahl der Zeitfenster pro Arzt

Ergebnisse:

 
LINQ

aus slot in SLOTS
gruppiere slot nach slot.DOCTOR in doctorSlots
wähle new { name=appointmentDoctor.Key.NAME,
 Vorname=creneauxMedecin.Key.FIRSTNAME,
 nbRv=creneauxMedecin.Count()}
Lambda
SQL

SELECT [t2].[NOM] AS [Nachname], [t2].[PRENOM] AS [Vorname], [t1].[value] AS [AnzahlBesuche]
FROM (
    SELECT COUNT(*) AS [value], [t0].[DOCTOR_ID]
    FROM [SLOTS] AS [t0]
    GROUP BY [t0].[DOCTOR_ID]
    ) AS [t1]
INNER JOIN [DOCTORS] AS [t2] ON [t2].[ID] = [t1].[DOCTOR_ID]

3.5.5. Ändern einer an den Persistenzkontext angehängten Entität

Wir haben die folgenden Operationen im Persistenzkontext behandelt:

  • Hinzufügen eines Elements zum Kontext ([dbContext].[DbSet].Add);
  • Ein Element aus dem Kontext entfernen ([dbContext].[DbSet].Remove);
  • Abfrage eines Kontexts mithilfe von LINQ-Abfragen.

Um den Kontext mit der Datenbank zu synchronisieren, schreiben Sie [dbContext].SaveChanges().

Der Code [ModifyAttachedEntity] zeigt, wie eine an den Kontext angehängte Entität geändert wird:


using System;
using System.Data;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
  class ModifyAttachedEntity
  {
    static void Main(string[] args)
    {
      Client client1, client2, client3;
      // 1st context
      using (var context = new RdvMedecinsContext())
      {
        // empty the current base
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // add a customer
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // follow-up
        Console.WriteLine("client1--avant");
        Console.WriteLine(client1);
        // save context
        context.SaveChanges();
        // follow-up
        Console.WriteLine("client1--après");
        Console.WriteLine(client1);
      }
      // 2nd context
      using (var context = new RdvMedecinsContext())
      {
        // retrieve client1 from client2
        client2 = context.Clients.Find(client1.Id);
        // follow-up
        Console.WriteLine("client2");
        Console.WriteLine(client2);
        // modify client2
        client2.Nom = "yy";
        // save context
        context.SaveChanges();
      }
      // 3rd context
      using (var context = new RdvMedecinsContext())
      {
        // retrieve client2 from client3
        client3 = context.Clients.Find(client2.Id);
        // follow-up
        Console.WriteLine("client3");
        Console.WriteLine(client3);
      }
    }
  }
}
  • Zeile 15: Der Anwendungskontext wird geöffnet;
  • Zeilen 18–25: Der Kontext wird geleert. Genauer gesagt werden alle Entitäten aus der Datenbank in den Kontext geladen und dann in den Status „gelöscht“ versetzt. Beachten Sie, dass sich die Datenbank zu diesem Zeitpunkt noch nicht verändert hat. Solange der Kontext nicht mit der Datenbank synchronisiert wird, bleibt die Datenbank unverändert. Erinnern Sie sich daran, dass das Löschen der Entitäten [Doctor] und [Client] ausreicht, um die Datenbank durch kaskadierende Löschungen zu leeren;
  • Zeilen 27–28: Ein neuer Kunde wird zur Datenbank hinzugefügt;
  • Zeilen 30–31: Der Kunde wird angezeigt, bevor er in der Datenbank gespeichert wird;
  • Zeile 33: Der Kontext wird mit der Datenbank synchronisiert. Als „gelöscht“ markierte Entitäten werden einer SQL-DELETE-Operation unterzogen, die hinzugefügte Entität einer SQL-INSERT-Operation;
  • Zeilen 35–36: Der Kunde wird nach der Synchronisierung mit der Datenbank angezeigt;

Das in der Konsole angezeigte Ergebnis lautet wie folgt:

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

Beachten Sie folgende Punkte:

  • Vor der Synchronisierung mit der Datenbank verfügt der Client weder über einen Primärschlüssel noch über einen Zeitstempel;
  • nach der Synchronisierung verfügt er über beides. Erinnern Sie sich daran, dass der Primärschlüssel so konfiguriert wurde, dass er von SQL Server generiert wird. Ebenso generiert dieses DBMS den Zeitstempel automatisch;
  • Zeile 37: Der Persistenzkontext wird geschlossen. Die darin enthaltenen Entitäten werden „getrennt“. Sie existieren als Objekte, jedoch nicht als an einen Persistenzkontext gebundene Entitäten;
  • Zeile 39: Ein neuer leerer Kontext wird gestartet;
  • Zeile 42: Der Client wird über seinen Primärschlüssel direkt aus der Datenbank abgerufen. Er wird dann in den Kontext aufgenommen. Wird er nicht gefunden, gibt die Find-Methode einen Null-Zeiger zurück;
  • Zeilen 48–49: Wir zeigen ihn an;

Dies führt zu folgendem Ergebnis:

client2
Client[16,xx,xx,xx,000000132209]
  • Zeile 47: Wir ändern sie;
  • Zeile 49: Wir synchronisieren den Kontext mit der Datenbank. EF erkennt, dass bestimmte Elemente des Kontexts geändert wurden, seit sie hinzugefügt wurden. Für diese Elemente generiert es SQL-UPDATE-Anweisungen für die Datenbank. Hier besteht die Synchronisation also aus einer einzigen UPDATE-Anweisung;
  • Zeile 50: Der zweite Kontext wird geschlossen. Die Entität client2, die dem Kontext zugeordnet war, wird nun von ihm getrennt;
  • Zeile 52: Ein dritter leerer Kontext wird geöffnet;
  • Zeile 55: Der einzelne Client aus der Datenbank wird erneut in den Kontext geladen. Wir wollen prüfen, ob die im vorherigen Kontext vorgenommene Änderung in der Datenbank übernommen wurde;
  • Zeilen 57–58: Der Client wird angezeigt. Dies führt zu folgendem Ergebnis:
client3
Client[16,xx,xx,yy,000000132210]

Der Name des Kunden wurde tatsächlich in der Datenbank aktualisiert. Beachten Sie, dass auch der Zeitstempel aktualisiert wurde.

  • Zeile 59: Wir schließen den Kontext. Beachten Sie übrigens, dass wir im Gegensatz zu den beiden vorherigen Fällen den Kontext nicht zuvor mit der Datenbank synchronisieren mussten (SaveChanges), da der Kontext nicht verändert worden war.

3.5.6. Verwaltung von getrennten Entitäten

Kehren wir zur Schichtenarchitektur einer Anwendung wie der in der Fallstudie zurück:

Die [DAO]-Schicht nutzt das EF5-ORM für den Zugriff auf Daten. Wir verfügen über die grundlegenden Bausteine dieser Schicht. Jede Methode öffnet einen Persistenzkontext, führt die erforderlichen Operationen (Einfügen, Aktualisieren, Löschen, Abfragen) durch und schließt ihn anschließend wieder. Die von der [DAO]-Schicht verwalteten Entitäten werden an die ASP.NET-Webschicht weitergeleitet. In dieser Schicht befinden sie sich außerhalb des Persistenzkontexts und sind daher losgelöst. In der Webschicht kann ein Benutzer diese Entitäten ändern (Hinzufügen, Aktualisieren, Löschen). Wenn sie zur [DAO]-Schicht zurückkehren, sind sie immer noch getrennt. Die [DAO]-Schicht muss jedoch die vom Benutzer vorgenommenen Änderungen in der Datenbank widerspiegeln. Sie muss daher mit getrennten Entitäten arbeiten. Betrachten wir die drei möglichen Fälle:

Hinzufügen einer losgelösten Entität

Dies ist das Standardverfahren für ein Hinzufügen. Fügen Sie die getrennte Entität einfach zum Kontext hinzu (Add) und stellen Sie sicher, dass ihr Primärschlüssel null ist.

Ändern einer losgelösten Entität

Sie können den folgenden Code verwenden:

[DbContext].Entry(entité-détachée).State=EntityState.Modified ;
  • Die Methode [DbContext].Entry(detached-entity) fügt die Entität dem Kontext hinzu;
  • der Status dieser Entität wird auf „modified“ gesetzt, sodass sie einer SQL-UPDATE-Anweisung unterliegt.

Löschen einer losgelösten Entität

Sie können den folgenden Code verwenden:

Entity e=[DbContext].[DbSet].Find(clé primaire de l'entité détachée) ;
[DbContext].[DbSet].Remove(e) ;
  • Zeile 1: Fügen Sie die Entität mit demselben Primärschlüssel wie die abgelöste Entität zum Kontext hinzu;
  • Zeile 2: Wir löschen sie:

Beachten Sie, dass hierfür in der Datenbank ein SELECT gefolgt von einem DELETE erforderlich ist, während normalerweise ein DELETE allein ausreicht. Sie können auch dem Beispiel zur Änderung einer losgelösten Entität folgen und schreiben:

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

Da es mir nicht gelungen ist, eine Protokollierung für SQL-Operationen in der Datenbank zu implementieren, weiß ich nicht, ob eine der beiden Methoden der anderen vorzuziehen ist.

Hier ist ein Beispiel:

Der Code für das Programm [ModifyDetachedEntities] lautet wie folgt:


using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
  class ModifyDetachedEntities
  {
    static void Main(string[] args)
    {
      Client client1;
 
      // empty the current base
      Erase();
      // add a customer
      using (var context = new RdvMedecinsContext())
      {
        // customer creation
        client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
        // add customer to context
        context.Clients.Add(client1);
        // save the context
        context.SaveChanges();
      }
      // basic view
      Dump("1-----------------------------");
      // client1 is not in the context - we modify it
      client1.Nom = "y";
      // new context
      using (var context = new RdvMedecinsContext())
      {
        // here we have an empty context
        // we put client1 in the context in a modified state
        context.Entry(client1).State = EntityState.Modified;
        // save the context
        context.SaveChanges();
      }
      // basic view
      Dump("2-----------------------------");
      // remove out-of-context entity
      using (var context = new RdvMedecinsContext())
      {
        // here we have a new empty context
        // we put client1 in the context in a deleted state
        context.Entry(client1).State = EntityState.Deleted;
        // save the context
        context.SaveChanges();
      }
      // basic view
      Dump("3-----------------------------");
    }
 
    static void Erase()
    {
      // empties base
      using (var context = new RdvMedecinsContext())
      {
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // save the context
        context.SaveChanges();
      }
    }
 
    static void Dump(string str)
    {
      Console.WriteLine(str);
      // displays the base
      using (var context = new RdvMedecinsContext())
      {
        foreach (var rv in context.Rvs)
        {
          Console.WriteLine(rv);
        }
        foreach (var creneau in context.Creneaux)
        {
          Console.WriteLine(creneau);
        }
        foreach (var client in context.Clients)
        {
          Console.WriteLine(client);
        }
        foreach (var medecin in context.Medecins)
        {
          Console.WriteLine(medecin);
        }
      }
    }
  }
}
  • Zeile 15: Die Datenbank wird gelöscht;
  • Zeilen 17–25: Ein Kunde wird zur Datenbank hinzugefügt;
  • Zeile 27: Der Inhalt der Datenbank wird angezeigt;
1-----------------------------
Client[20,x,x,x,0000011209]
  • Nach Zeile 25 existiert der Persistenzkontext nicht mehr. Daher gibt es keine angehängten Entitäten mehr. Die Entität client1 ist in den Zustand „detached“ übergegangen;
  • Zeile 29: Der Name der getrennten Entität wird geändert;
  • Zeile 31: Ein neuer leerer Kontext wird geöffnet;
  • Zeile 35: Die abgelöste Entität client1 wird im Kontext im Status „modified“ abgelegt;
  • Zeile 37: Der Kontext wird mit der Datenbank synchronisiert;
  • Zeile 38: Er wird geschlossen;
  • Zeile 40: Die Datenbank wird angezeigt;
2-----------------------------
Client[20,x,x,y,0000011210]

Der Name des Kunden wurde erfolgreich in der Datenbank aktualisiert. Beachten Sie, dass der Zeitstempel aktualisiert wurde;

  • Zeile 42: Öffnen eines neuen leeren Kontexts;
  • Zeile 46: Die abgelöste Entität client1 wird im Kontext im Status „gelöscht“ abgelegt;
  • Zeile 48: Der Kontext wird mit der Datenbank synchronisiert;
  • Zeile 49: Er wird geschlossen;
  • Zeile 51: Die Datenbank wird angezeigt;
3-----------------------------

Die Entität wurde tatsächlich aus der Datenbank gelöscht.

Nun werden wir uns die beiden Modi zum Laden der Abhängigkeiten einer Entität ansehen: Lazy und Eager Loading.

3.5.7. Lazy Loading und Eager Loading

Werfen wir noch einmal einen Blick auf das Many-to-One-Abhängigkeitsschema unserer vier Entitäten:

Oben verfügt die Entität [Creneau] über eine Navigationseigenschaft [Creneau.Medecin], die auf die Entität [Medecin] verweist. Dies wird als Abhängigkeit bezeichnet. Wir haben gesehen, dass es auch Eins-zu-Viele-Abhängigkeiten gibt. Das hier erläuterte Prinzip gilt auch für diese.

Standardmäßig befindet sich EF 5 im Lazy-Loading-Modus: Wenn es eine Entität aus der Datenbank in den Persistenzkontext lädt, lädt es deren Abhängigkeiten nicht mit. Diese werden erst geladen, wenn sie zum ersten Mal verwendet werden. Dies ist eine sinnvolle Maßnahme. Wäre dies nicht der Fall, würde das Laden der Termine in den Kontext aufgrund der oben genannten Abhängigkeiten dazu führen, dass:

  • die mit den Terminen verknüpften [Time Slot]-Entitäten;
  • die mit diesen Zeitfenstern verknüpften [Doctor]-Entitäten;
  • die mit den Terminen verknüpften [Clients]-Entitäten.

Manchmal benötigen wir jedoch eine Entität und ihre Abhängigkeiten. Wir werden beide Lademodi veranschaulichen.

Der Code für [LazyEagerLoading] lautet wie folgt:


using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
 
namespace RdvMedecins_01
{
  class LazyEagerLoading
  {
    // entities
    static Medecin[] medecins;
    static Client[] clients;
    static Creneau[] creneaux;
 
    static void Main(string[] args)
    {
      // initialize the base      
      InitBase();
      Console.WriteLine("Initialisation terminée");
      // eager loading
      Creneau creneau;
      int idCreneau = (int)creneaux[0].Id;
      using (var context = new RdvMedecinsContext())
      {
        // crenel n° 0
        creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // dependent display
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
      }
      // lazy loading - default mode
      using (var context = new RdvMedecinsContext())
      {
        // crenel n° 0
        creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // dependent display
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 2 suivante s'est produite : {0}", e);
      }
 
    }
 
    static void InitBase()
    {
      // initialize the base
      using (var context = new RdvMedecinsContext())
      {
        // empty the current base
        ...
        // initialize the base
        // our customers
        clients = new Client[] {
        new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
        new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
        new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
        new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
     };
...
        // dates
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // save the persistence context
        context.SaveChanges();
      }
    }
  }
}
  • Zeile 18: Wir gehen von einer bekannten Basis aus, der bisher verwendeten. Nach dieser Operation sind die Arrays in den Zeilen 11–13 mit losgelösten Entitäten gefüllt;
  • Zeilen 21–22: Wir konzentrieren uns auf den ersten Zeitabschnitt und den zugehörigen Arzt;
  • Zeile 23: neuer Kontext;
  • Zeile 26: Wir fügen den Zeitfenster zusammen mit seiner Abhängigkeit in den Kontext ein (Eager Loading). Da dies nicht der Standardmodus ist, müssen wir diese Abhängigkeit explizit anfordern. Die Include-Methode ermöglicht uns dies. Ihr Parameter ist der Name der Abhängigkeit innerhalb der Entität, die in den Kontext gebracht wird. Die Abfrage, die die Entität in den Kontext bringt, verwendet Lambda-Ausdrücke. Mit der Single-Methode können Sie eine Bedingung angeben, um eine einzelne Entität abzurufen. Hier suchen wir in der Datenbank nach der [Creneau]-Entität, die den Primärschlüssel des Zeitfensters Nr. 0 hat;
  • Zeile 27: Wir zeigen die abgerufene Entität an. Sehen wir uns die beiden in Entitäten verwendeten Schreibmethoden an:

// signature
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
 
   // short signature
    public string ShortIdentity()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
    }
  • Zeilen 2–5: Die [ToString]-Methode zeigt die [Doctor]-Abhängigkeit an. Wenn diese noch nicht im Kontext vorhanden ist, wird sie in der Datenbank nachgeschlagen und hinzugefügt;
  • Zeilen 8–11: Die Methode [ShortIdentity] zeigt die Abhängigkeit [Doctor] nicht an. Sie wird daher nicht in der Datenbank nachgeschlagen, wenn sie nicht im Kontext vorhanden ist;

Zu diesem Zeitpunkt sieht die Konsolenausgabe wie folgt aus:

Initialisation terminée
Creneau[181,8,0,8,20, 21, 00000195150]
  • Zeile 28: Der Kontext wird geschlossen;
  • Zeilen 30–37: Wir versuchen, die Abhängigkeit [Doctor] der Entität zu schreiben. Erinnern Sie sich daran, wie Lazy Loading funktioniert: Eine Abhängigkeit wird bei ihrer ersten Verwendung geladen, wenn sie nicht vorhanden ist. Hier ist sie normalerweise vorhanden. Die Ausgabe lautet wie folgt:
Médecin=Medecin[21,Mme,Marie,Pelissier,00000195149]
  • Zeilen 39–44: In einem neuen Kontext wird erneut in der Datenbank nach Slot #0 gesucht und dieser in den Kontext übernommen. Hier wird die Abhängigkeit [Doctor] nicht explizit angefordert. Sie wird daher nicht übernommen (Lazy Loading);
  • Zeile 43: Die Kurzbezeichnung des Slots wird wie folgt angezeigt:
Creneau[181,8,0,8,20, 21, 00000195150]

Hier ist es wichtig, ShortIdentity anstelle von ToString zu verwenden, um die Entität anzuzeigen. Wenn ToString verwendet wird, wird die Abhängigkeit [Doctor] angezeigt, und dazu wird in der Datenbank nachgeschlagen. Das wollen wir jedoch nicht.

  • Zeile 44: Der Kontext wird geschlossen;
  • Zeilen 46–53: Wir versuchen, die Abhängigkeit der Entität anzuzeigen. Es ist wichtig, dies außerhalb des Kontexts zu tun; andernfalls wird sie in der Datenbank nachgeschlagen und gefunden. Hier befinden wir uns außerhalb des Kontexts. Die Entität [Creneau] ist abgekoppelt und ihre Abhängigkeit [Medecin] fehlt (Lazy Loading). Was wird passieren? Die Bildschirmanzeige sieht wie folgt aus:
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:ligne 48

EF hat festgestellt, dass die Abhängigkeit [Medecin] fehlte. Es wurde versucht, sie zu laden, doch da der Kontext geschlossen war, war dieser Vorgang nicht mehr möglich. Wir werden diese Ausnahme [System.ObjectDisposedException] vermerken, da sie charakteristisch für das Laden einer Abhängigkeit außerhalb eines offenen Kontexts ist.

Betrachten wir nun den gleichzeitigen Zugriff auf Entitäten.

3.5.8. Parallelität beim Entitätszugriff

Schauen wir uns noch einmal die Definition der Entität [Client] an:


public class Client
  {
    // data
    [Key]
    [Column("ID")]
    public int? Id { get; set; }
    [Required]
    [MaxLength(5)]
    [Column("TITRE")]
    public string Titre { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("NOM")]
    public string Nom { get; set; }
    [Required]
    [MaxLength(30)]
    [Column("PRENOM")]
    public string Prenom { get; set; }
    // customer rvs
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
 
    // signature
    ...
  }

Wir konzentrieren uns auf das Feld [Timestamp] in Zeile 23. Wir wissen, dass sein Wert vom DBMS generiert wird. Wir haben außerdem festgestellt, dass die Annotation [Timestamp] in Zeile 22 dazu führt, dass EF 5 das annotierte Feld verwendet, um die Parallelität beim Zugriff auf Entitäten zu verwalten. Erinnern wir uns daran, was Parallelitätsverwaltung ist:

  • Ein Prozess P1 liest zum Zeitpunkt T1 eine Zeile L aus der Tabelle [DOCTORS]. Die Zeile hat den Zeitstempel TS1;
  • Ein Prozess P2 liest zur Zeit T2 dieselbe Zeile L aus der Tabelle [DOCTORS]. Die Zeile hat den Zeitstempel TS1, da Prozess P1 seine Änderung noch nicht festgeschrieben hat;
  • Prozess P1 bestätigt seine Änderung an Zeile L. Der Zeitstempel von Zeile L ändert sich daraufhin zu TS2;
  • Prozess P2 bestätigt seine Änderung an Zeile L. Das ORM löst daraufhin eine Ausnahme aus, da Prozess P2 einen Zeitstempel TS1 für Zeile L hat, der sich von dem in der Datenbank gefundenen Zeitstempel TS2 unterscheidet.

Dies wird als optimistisches Parallelitätsmanagement bezeichnet. Bei EF 5 muss ein Feld, das diese Rolle übernimmt, eines von zwei Attributen aufweisen: [Timestamp] oder [ConcurrencyCheck]. SQL Server verfügt über einen [timestamp]-Typ. Der Wert einer Spalte dieses Typs wird bei jedem Einfügen oder Ändern einer Zeile automatisch von SQL Server generiert. Eine solche Spalte kann dann zur Verwaltung der Parallelität verwendet werden.

Wir veranschaulichen diesen gleichzeitigen Zugriff anhand von zwei Threads, die gleichzeitig dieselbe [Client]-Entität in der Datenbank ändern. Das Projekt entwickelt sich wie folgt:

Der Code für das Programm [ConcurrentAccess] lautet wie folgt:


using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
 
  // object exchanged with threads
  class Data
  {
    public int Duree { get; set; }
    public string Nom { get; set; }
    public Client Client { get; set; }
  }
 
  // test program
  class AccèsConcurrents
  {
 
    static void Main(string[] args)
    {
      Client client1;
      using (var context = new RdvMedecinsContext())
      {
        // main thread
        Thread.CurrentThread.Name = "main";
        // empty the current base
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // add a customer
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // follow-up
        Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
        // backup
        context.SaveChanges();
        // follow-up
        Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
      }
      // we'll modify client1 with two threads
      // thead t1
      Thread t1 = new Thread(Modifie);
      t1.Name = "t1";
      t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
      // thread t2
      Thread t2 = new Thread(Modifie);
      t2.Name = "t2";
      t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
      // we wait for the end of the 2 threads
      Console.WriteLine("Thread {0} -- début attente fin des deux threads", Thread.CurrentThread.Name);
      t1.Join();
      t2.Join();
      Console.WriteLine("Thread {0} -- fin attente fin des deux threads", Thread.CurrentThread.Name);
      // the modification is displayed - only one was successful
      using (var context = new RdvMedecinsContext())
      {
        // retrieve client1 from client2
        Client client2 = context.Clients.Find(client1.Id);
        Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
      }
    }
 
    // thread
    static void Modifie(object infos)
    {
 ...
}
  • Zeile 26: Wir starten einen leeren Kontext;
  • Zeile 29: Wir benennen den aktuellen Thread, um ihn von den beiden Threads zu unterscheiden, die später erstellt werden;
  • Zeilen 31–38: Die Entitäten [Doctor] und [Client] werden in den Status „deleted“ versetzt;
  • Zeilen 40–41: Ein Client wird dem Kontext hinzugefügt;
  • Zeilen 43–44: Anzeige vor der Kontextsynchronisation;
  • Zeile 46: Kontextsynchronisation mit der Datenbank: Entitäten im Status „gelöscht“ werden aus der Datenbank entfernt. Die im Kontext platzierte Entität [Client] wird in die Datenbank eingefügt. Sie wird das einzige Element in der Datenbank sein;
  • Zeilen 47–49: Der Kunde wird nach der Kontextsynchronisation angezeigt. Zu diesem Zeitpunkt sieht die Bildschirmanzeige wie folgt aus:
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]

Beachten Sie, dass der Client nach der Kontextsynchronisation über einen Primärschlüssel und einen Zeitstempel verfügt;

  • Zeile 50: Der Kontext wird geschlossen;
  • Zeile 53: Ein Thread t1 wird der Methode [Modify] in Zeile 84 zugeordnet. Das bedeutet, dass er bei seiner Ausführung die Methode [Modify] ausführt;
  • Zeile 54: Der Thread t1 erhält einen Namen;
  • Zeile 55: Der Thread t1 wird gestartet. Parameter werden ihm in Form einer in den Zeilen 12–17 definierten [Data]-Struktur übergeben:
    • Duration: Der Thread wird X Sekunden vor Abschluss seiner Ausführung angehalten,
    • Client: eine Referenz auf den Client, der in der Datenbank aktualisiert werden soll,
    • Name: der Name, der diesem Client zugewiesen werden soll;
  • Zeilen 57–59: Gleiches Verfahren mit einem zweiten Thread. Letztendlich versuchen zwei Threads, den Namen desselben Clients in der Datenbank zu ändern;
  • Zeilen 60–63: Nach dem Start der beiden Threads wartet der Haupt-Thread darauf, dass diese ihre Ausführung beenden;
  • Zeile 62: Warten auf den Abschluss von Thread t1;
  • Zeile 63: Warten auf den Abschluss von Thread t2;
  • Zeile 64: Wir wissen nicht, in welcher Reihenfolge die beiden Threads beendet werden. Sicher ist, dass sie bis Zeile 64 beendet sind;
  • Zeilen 66–72: In einem neuen Kontext suchen wir den Client in der Datenbank, um seinen Status zu überprüfen.

Schauen wir uns nun an, was die beiden Threads t1 und t2 tun. Sie führen die folgende [Modify]-Methode aus:


static void Modifie(object infos)
    {
      // parameter is retrieved
      Data data = (Data)infos;
      try
      {
        using (var context = new RdvMedecinsContext())
        {
          Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
          // retrieve client1 from client2
          Client client2 = context.Clients.Find(data.Client.Id);
          Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
          Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
          // modify client2
          client2.Nom = data.Nom;
          // we wait a little
          Thread.Sleep(data.Duree);
          // save changes
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        // exception
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
      }
      // end of thread
      Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
    }
  • Zeile 4: Abrufen der Thread-Parameter (Duration, Name, Client);
  • Zeile 7: neuer Kontext;
  • Zeile 11: Der Client wird in den Kontext aufgenommen;
  • Zeilen 12–13: Überwachung zur Überprüfung des Client-Status;
  • Zeile 15: Ändern seines Namens;
  • Zeile 17: Der Thread pausiert für eine Dauer von Millisekunden. Dies hat einen interessanten Effekt. Der Thread gibt den Prozessor frei, der ihn ausgeführt hat, und macht so Platz für einen anderen Thread. In unserem Beispiel haben wir drei Threads: main, t1 und t2. Der Haupt-Thread ist angehalten und wartet darauf, dass die Threads t1 und t2 fertig werden. Angenommen, Thread t1 hat den Prozessor zuerst, gibt er ihn nun an Thread t2 ab. Dies führt dazu, dass Thread t2 genau dieselben Daten liest wie Thread t1 – denselben Client mit demselben Zeitstempel;
  • Zeile 19: Der Kontext wird mit der Datenbank synchronisiert. Nehmen wir erneut an, dass Thread t1 als Erster wieder aktiv wird. Er wird den Client mit dem Namen „yy“ speichern. Dies ist möglich, da er denselben Zeitstempel wie in der Datenbank hat. Aufgrund dieser Aktualisierung wird das DBMS den Zeitstempel ändern. Wenn Thread t2 seinerseits wieder aktiv wird, hat er einen Client mit einem Zeitstempel, der sich von dem derzeit in der Datenbank vorhandenen unterscheidet. Seine Aktualisierung wird abgelehnt.

Die Bildschirmanzeigen sehen wie folgt aus:

main client1--before saving the context
Client[,xx,xx,xx,]
main client1--after saving the context
Client[33,xx,xx,xx,000001126209]
Thread main -- start wait end both 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 été modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager. ---> System.Data.OptimisticConcurrencyException: 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 char
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 -- end wait end both threads
Thread main client2
Thread main Client[33,xx,xx,zz,000001126210]
  • Zeile 4: der Client in der Datenbank;
  • Zeile 9: der Client, wie er von Thread t2 gelesen wurde;
  • Zeile 11: der Client, wie er von Thread t1 gelesen wurde. Beide Threads haben also dasselbe gelesen;
  • Zeile 12: Thread t2 ist zuerst fertig. Er konnte daher seine Aktualisierung durchführen. Der Name muss sich in „zz“ geändert haben;
  • Zeile 13: Thread t1 löst eine [System.Data.OptimisticConcurrencyException] aus. EF hat festgestellt, dass er nicht über den korrekten Zeitstempel verfügte;
  • Zeile 21: Thread t1 ist nun seinerseits fertig;
  • Zeile 22: Der Hauptthread hat das Warten beendet;
  • Zeile 24: Der Hauptthread zeigt den Client in der Datenbank an. Es ist tatsächlich Thread t2, der gewonnen hat. Der Name lautet „zz“. Beachten Sie, dass sich der Zeitstempel geändert hat.

Betrachten wir nun einen weiteren Aspekt: die Transaktion, die die Synchronisation des Persistenzkontexts mit der Datenbank steuert.

3.5.9. Synchronisation innerhalb einer Transaktion

Die Tabelle [CRENEAUX] verfügt über eine Eindeutigkeitsbeschränkung, die wir manuell hinzugefügt haben (siehe Abschnitt 2.2.4, Seite 12):

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

Wir gehen wie folgt vor: Wir fügen zwei Termine gleichzeitig für denselben Arzt, am selben Tag und im selben Zeitfenster hinzu. Schauen wir mal, was passiert.

Das Projekt entwickelt sich wie folgt:

Der Code für das Programm [SynchronisationTransaction] lautet wie folgt:


using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
 
  // test program
  class SynchronisationTransaction
  {
 
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // empty the current base
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        context.SaveChanges();
      }
 
      // create a customer
      Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // we create a doctor
      Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // we create a niche for this doctor
      Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
      // create two appointments for this doctor and this customer, same day, same time slot
      Rv rv1 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
      Rv rv2 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
      try
      {
        // we put it all in the context of persistence
        using (var context = new RdvMedecinsContext())
        {
          context.Clients.Add(client1);
          context.Creneaux.Add(creneau1);
          context.Medecins.Add(medecin1);
          context.Rvs.Add(rv1);
          context.Rvs.Add(rv2);
          // save the context - you should have an exception
          // because the underlying BD has a uniqueness constraint preventing
          // to have two RDV on the same day, in the same slot
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("Erreur : {0}", e);
      }
      // if the save occurs in a transaction, then nothing must have been inserted in the database
      // because of the previous exception - we check
 
      using (var context = new RdvMedecinsContext())
      {
        // our customers
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // the doctors
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // time slots
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // dates
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}
  • Zeilen 15–27: Ein Persistenzkontext wird verwendet, um die Datenbank zu leeren;
  • Zeile 30: Erstellung eines [Client]-Objekts;
  • Zeile 32: Erstellung eines [Doctor]-Objekts;
  • Zeile 34: Erstellung eines [Slot]-Objekts;
  • Zeile 36: Erstellung eines [Appointment]-Objekts;
  • Zeile 37: Erstellung eines zweiten [Appointment]-Objekts, das mit dem vorherigen identisch ist;
  • Zeile 41: Öffnen eines neuen Kontexts;
  • Zeilen 43–47: Die zuvor erstellten Objekte werden dem neuen Kontext hinzugefügt. Beachten Sie hierbei, dass wir durch Berücksichtigung von Abhängigkeiten die Anzahl der Add-Operationen hätten minimieren können. EF optimiert jedoch die an die Datenbank zu sendenden SQL-INSERT-Anweisungen;
  • Zeile 51: Der Kontext wird mit der Datenbank synchronisiert. Wie der Kommentar andeutet, muss das Einfügen eines der beiden Termine aufgrund der Eindeutigkeitsbeschränkung in der Tabelle [RVS] fehlschlagen. Darüber hinaus muss jedoch, falls die Synchronisierung innerhalb einer Transaktion erfolgt, alles zurückgesetzt werden. Daher sollte kein Einfügen stattfinden. Die Datenbank muss leer bleiben;
  • Zeile 53: Der Kontext wird geschlossen;
  • Zeilen 61–90: Anzeige des Datenbankinhalts. Dieser muss leer sein.

Die Bildschirmanzeige sieht wie folgt aus:

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: Une erreur s'est produite lors de la mise à jour des entrées. Pour plus d'informations, consultez l'exception interne. ---> System.Data.SqlClient.SqlException: Violation de la contrainte UNIQUE KEY « RVS_uq ». Impossible d'insérer une clé en double dans l'objet « dbo.RVS ». Valeur de clé dupliquée : (oct 18 2012 12:00AM, 34).
L'instruction a été 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)...
   --- Fin de la trace de la pile d'exception interne ---
   ...
   à 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:ligne 59
Clients--------------------------------------
Médecins--------------------------------------
Créneaux horaires--------------------------------------
Rendez-vous--------------------------------------
  • Zeile 1: Ausnahme aufgrund eines Verstoßes gegen die Eindeutigkeitsbeschränkung in der Tabelle [RVS];
  • Zeilen 9–12: Die Datenbank ist tatsächlich leer. Die Synchronisierung des Kontexts mit der Datenbank erfolgte daher innerhalb einer Transaktion.

Zweifellos gibt es noch weitere Aspekte in EF 5 zu untersuchen. Aber wir wissen genug, um zu unserer Untersuchung einer mehrschichtigen Architektur zurückzukehren. Am Anfang dieses Dokuments findet der Leser Verweise auf Artikel und Bücher, die es ihm ermöglichen, sein Wissen über EF 5 zu vertiefen.

3.6. Untersuchung einer mehrschichtigen Architektur auf Basis von EF 5

Wir kehren zu unserer in Abschnitt 2 beschriebenen Fallstudie zurück. Es handelt sich um eine ASP.NET-Webanwendung mit folgender Struktur:

Wir beginnen mit der Erstellung der [DAO]-Datenzugriffsschicht. Diese Schicht basiert auf EF5.

3.6.1. Das neue Projekt

Wir erstellen ein neues VS 2012-Konsolenprojekt [RdvMedecins-SqlServer-02] in der aktuellen Lösung [1]:

Wir fügen vier Ordner [2] hinzu, in denen wir unseren Code organisieren werden. Der Ordner [Entities] ist eine Kopie des Ordners [Entities] aus dem vorherigen Projekt. Nach dem Kopieren treten Fehler auf, da wir nicht über die richtigen Verweise verfügen. Wir müssen eine Referenz zu Entity Framework 5 hinzufügen. Dazu folgen wir der in Abschnitt 3.4, Seite 21, beschriebenen Vorgehensweise. Die Liste der Referenzen sieht nun wie folgt aus [3]:

Zu diesem Zeitpunkt sollte das Projekt keine Kompilierungsfehler mehr aufweisen. Aus dem vorherigen Projekt kopieren wir außerdem die Datei [App.config], die die Datenbankverbindung konfiguriert:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
  </entityFramework>
 
  <!-- connection chain on base -->
  <connectionStrings>
    <add name="monContexte"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- the factory provider -->
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider"
       invariant="System.Data.SqlClient"
       description=".Net Framework Data Provider for SqlServer"
       type="System.Data.SqlClient.SqlClientFactory, System.Data,
     Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
    />
    </DbProviderFactories>
  </system.data>
 
</configuration>

3.6.2. Die Ausnahmeklasse

Wir verwenden eine projektspezifische Ausnahmeklasse. Diese wird von der [DAO]-Schicht ausgelöst:

Die [DAO]-Schicht fängt alle Ausnahmen ab, die an sie weitergeleitet werden, und kapselt sie in eine Ausnahme vom Typ [RdvMedecinsException]. Diese Ausnahme sieht wie folgt aus:


using System;
 
namespace RdvMedecins.Exceptions
{
  public class RdvMedecinsException : Exception
  {
 
    // properties
    public int Code { get; set; }
 
    // manufacturers
    public RdvMedecinsException()
      : base()
    {
    }
 
    public RdvMedecinsException(string message)
      : base(message)
    {
    }
 
    public RdvMedecinsException(int code, string message)
      : base(message)
    {
      Code = code;
    }
 
    public RdvMedecinsException(int code, string message, Exception ex)
      : base(message, ex)
    {
      Code = code;
    }
 
    // identity
    public override string ToString()
    {
      if (InnerException == null)
      {
        return string.Format("RdvMedecinsException[{0},{1}]", Code, base.Message);
      }
      else
      {
        return string.Format("RdvMedecinsException[{0},{1},{2}]", Code, base.Message, base.InnerException.Message);
      }
    }
  }
}
  • Zeile 5: Die Klasse erweitert die Klasse [Exception];
  • Zeile 9: Sie fügt ihrer Basisklasse einen Fehlercode hinzu;
  • Zeilen 12–32: Die verschiedenen Konstruktoren enthalten das Feld [Code].

Das Projekt entwickelt sich wie folgt:

3.6.3. Die [DAO]-Schicht

Die [DAO]-Schicht stellt eine Schnittstelle zur [ASP.NET]-Schicht bereit. Um dies zu erkennen, sehen Sie sich die Webseiten der Anwendung an:

  • In [1] oben wurde die Dropdown-Liste mit der Liste der Ärzte gefüllt. Die [DAO]-Schicht stellt diese Liste bereit;
  • in [2] stellt die [DAO]-Schicht Folgendes bereit:
  • die Liste der Termine eines Arztes für einen bestimmten Tag,
  • eine Liste der verfügbaren Zeitfenster eines Arztes,
  • zusätzliche Informationen über den ausgewählten Arzt;
  • in [3] wird die Dropdown-Liste der Kunden von der [DAO]-Schicht bereitgestellt;
  • in [4] bestätigt der Benutzer einen Termin. Die [DAO]-Schicht muss in der Lage sein, diesen in die Datenbank einzutragen. Sie muss außerdem zusätzliche Informationen über den ausgewählten Kunden bereitstellen können;
  • In 5 löscht der Benutzer einen Termin. Die [DAO]-Schicht muss dies zulassen.

Mit diesen Informationen könnte die [IDao]-Schnittstelle der [DAO]-Schicht wie folgt aussehen:


using System;
using System.Collections.Generic;
using RdvMedecins.Entites;
 
namespace RdvMedecins.Dao
{
  public interface IDao
  {
    // customer list
    List<Client> GetAllClients();
    // list of doctors
    List<Medecin> GetAllMedecins();
    // list of physician slots
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // list of RV from a given doctor on a given day
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // add a RV
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // delete a RV
    void SupprimerRv(int idRv);
    // find a T entity via its primary key
    T Find<T>(int id) where T : class;
  }
}

Die Methoden in den Zeilen 10–20 leiten sich aus der soeben durchgeführten Analyse ab. Die Methode in Zeile 22 dient dazu, dem Umstand Rechnung zu tragen, dass wir mit Lazy Loading arbeiten. Wenn wir in der [ASP.NET]-Schicht eine Abhängigkeit von einer Entität benötigen, rufen wir diese mithilfe dieser Methode aus der Datenbank ab.

Die [Dao]-Implementierung dieser Schnittstelle sieht wie folgt aus:


using System;
using System.Collections.Generic;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using RdvMedecins.Models;
 
namespace RdvMedecins.Dao
{
  public class Dao : IDao
  {
 
    //customer list
    public List<Client> GetAllClients()
    {
      // customer list
      List<Client> clients = null;
      try
      {
        // opening persistence context
        using (var context = new RdvMedecinsContext())
        {
          // customer list
          clients = context.Clients.ToList();
        }
 
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(1, "GetAllClients", ex);
      }
      // we return the result
      return clients;
    }
 
    // list of doctors
    public List<Medecin> GetAllMedecins()
    {
      // list of doctors
      List<Medecin> medecins = null;
      try
      {
        // opening persistence context
        using (var context = new RdvMedecinsContext())
        {
          // list of doctors
          medecins = context.Medecins.ToList();
        }
 
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(2, "GetAllMedecins", ex);
      }
      // we return the result
      return medecins;
    }
 
    // list of time slots for a given doctor
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
   ...
    }
 
    // list of a doctor's RV for a given day
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
 ...
    }
 
    // add a RV to the list
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
 ...
    }
 
    // delete a RV
    public void SupprimerRv(int idRv)
    {
...
    }
 
    // find a customer
    public Client FindClient(int id)
    {
...
    }
 
    // find a niche
    public Creneau FindCreneau(int id)
    {
 ...
    }
 
    // find a doctor
    public Medecin FindMedecin(int id)
    {
....
    }
 
    // find an rv
    public Rv FindRv(int id){
...
    }
 
  }
}

Lassen Sie uns die Methode [GetAllClients] erläutern, die eine Liste aller Clients zurückgeben soll:

  • Zeilen 18–31: Die Clientsuche wird innerhalb eines try/catch-Blocks durchgeführt. Das Gleiche gilt für alle nachfolgenden Methoden;
  • Zeile 21: Öffnen eines neuen Kontexts;
  • Zeile 24: [Client]-Entitäten werden in den Kontext geladen und in eine Liste aufgenommen.

Die Methode [GetAllMedecins], die eine Liste aller Ärzte zurückgibt, ist ähnlich aufgebaut (Zeilen 37–57).

Die Methode [GetCreneauxMedecin] lautet wie folgt:


// list of time slots for a given doctor
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
      // list of slots
      try
      {
        // opening persistence context
        using (var context = new RdvMedecinsContext())
        {
          // we get the doctor back with his slots
          Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
          // list of doctor's slots
          return medecin.Creneaux.ToList<Creneau>();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
      }
    }
  • Zeile 9: Öffnen eines neuen Persistenzkontexts;
  • Zeile 11: Suche nach dem Arzt, dessen Primärschlüssel bekannt ist. Anforderung, dass die [Creneaux]-Abhängigkeit – eine Sammlung der Zeitfenster des Arztes – einbezogen wird. Wenn der Arzt nicht existiert, löst die Single-Methode eine Ausnahme aus;
  • Zeile 13: Gibt die Liste der Zeitfenster zurück.

Die Methode [GetRvMedecinJour] muss die Liste der Termine eines Arztes für einen bestimmten Tag zurückgeben. Ihr Code könnte wie folgt aussehen:


// list of a doctor's RV for a given day
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
      // rv list
      List<Rv> rvs = null;
 
      try
      {
        // opening persistence context
        using (var context = new RdvMedecinsContext())
        {
          // we get the doctor back
          Medecin medecin = context.Medecins.Find(idMedecin);
          if (medecin == null)
          {
            throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
          }
          // appointment list
          rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
      }
      // we return the result
      return rvs;
    }
  • Zeile 13: Rufe den Arzt mit dem angegebenen Primärschlüssel ab;
  • Zeilen 14–17: Falls er nicht existiert, eine Ausnahme auslösen;
  • Zeile 19: die LINQ-Abfrage zum Abrufen der Termine für diesen Arzt;

Die Methode [AddAppointment] muss einen Termin zur Datenbank hinzufügen und den Primärschlüssel des eingefügten Elements zurückgeben. Der Code könnte wie folgt aussehen:


// add a RV to the list
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
      // rdv n° added
      int idRv;
      try
      {
        // opening persistence context
        using (var context = new RdvMedecinsContext())
        {
          // we get the slot back
          Creneau creneau = context.Creneaux.Find(idCreneau);
          if (creneau == null)
          {
            throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
          }
          // we get the customer back
          Client client = context.Clients.Find(idClient);
          if (client == null)
          {
            throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
          }
          // niche creation
          Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
          // added in context
          context.Rvs.Add(rv);
          // save context
          context.SaveChanges();
          // retrieve the primary key of the added rv
          idRv = (int)rv.Id;
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(7, "AjouterRv", ex);
      }
      // result
      return idRv;
    }
  • Zeile 12: Suche nach dem Terminfenster in der Datenbank;
  • Zeilen 13–16: Wenn er nicht gefunden wird, wird eine Ausnahme ausgelöst;
  • Zeile 18: Suche nach dem Kunden des Termins in der Datenbank;
  • Zeilen 19–22: Wenn er nicht gefunden wird, wird eine Ausnahme ausgelöst;
  • Zeile 24: Erstellen eines [Rv]-Objekts mit den erforderlichen Informationen;
  • Zeile 26: Füge es dem Persistenzkontext hinzu;
  • Zeile 28: Wir synchronisieren den Persistenzkontext mit der Datenbank. Der Termin wird dann in der Datenbank gespeichert;
  • Zeile 30: Wir wissen, dass nach der Datenbanksynchronisation die Primärschlüssel der eingefügten Elemente verfügbar sind. Wir rufen denjenigen für den hinzugefügten Termin ab;
  • Zeile 31: Wir schließen den Persistenzkontext.

Die Methode [DeleteAppointment] muss einen Termin löschen, dessen Primärschlüssel an sie übergeben wird.


// delete a RV
    public void SupprimerRv(int idRv)
    {
      try
      {
        // opening persistence context
        using (var context = new RdvMedecinsContext())
        {
          // we recover the Rv
          Rv rv = context.Rvs.Find(idRv);
          if (rv == null)
          {
            throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
          }
          // deletion Rv
          context.Rvs.Remove(rv);
          // save context
          context.SaveChanges();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(8, "SupprimerRv", ex);
      }
    }
  • Zeile 7: neuer Persistenzkontext;
  • Zeile 10: Der zu löschende Termin wird an den Kontext übergeben;
  • Zeilen 11–15: Falls er nicht existiert, wird eine Ausnahme ausgelöst;
  • Zeile 16: aus dem Kontext entfernen;
  • Zeile 18: Synchronisieren des Kontexts mit der Datenbank;
  • Zeile 19: Schließe den Kontext.

Mit der Methode [Find<T>] können Sie die Datenbank anhand des Primärschlüssels nach einer Entität vom Typ T durchsuchen. Der Code könnte wie folgt aussehen:


public T Find<T>(int id)  where T : class
    {
      try
      {
        // opening persistence context
        using (var context = new RdvMedecinsContext())
        {
          return context.Set<T>().Find(id);
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(20, "Find<T>", ex);
      }
    }
  • Zeile 8: Mit der Set<T>-Methode können Sie ein DbSet<T> abrufen, auf das Sie die üblichen Methoden anwenden können.

Das Projekt entwickelt sich wie folgt:

3.6.4. Testen der [DAO]-Schicht

Wir werden ein Testprogramm für die [DAO]-Schicht erstellen. Die Testarchitektur sieht wie folgt aus:

Ein Konsolenprogramm fordert [Spring.net] auf, die [DAO]-Schicht zu instanziieren. Sobald dies geschehen ist, testet es die verschiedenen Funktionen der [DAO]-Schicht-Schnittstelle. Anstelle eines Konsolenprogramms wäre es vorzuziehen gewesen, ein Testprogramm im NUnit-Stil zu schreiben. Ein Testprogramm für die [DAO]-Schicht könnte wie folgt aussehen:


using System;
using System.Collections.Generic;
using RdvMedecins.Dao;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using Spring.Context.Support;

namespace RdvMedecins.Tests
{
  class Program
  {
    public static void Main()
    {
      IDao dao = null;
      try
      {
        // instantiation layer [DAO] via Spring
        dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
 
        // customer display
        List<Client> clients = dao.GetAllClients();
        DisplayClients("Liste des clients :", clients);
 
        // physician display
        List<Medecin> medecins = dao.GetAllMedecins();
        DisplayMedecins("Liste des médecins :", medecins);
 
        // list of time slots for doctor no. 0
        List<Creneau> creneaux = dao.GetCreneauxMedecin((int)medecins[0].Id);
        DisplayCreneaux(string.Format("Liste des créneaux horaires du médecin {0}", medecins[0]), creneaux);
 
        // list of doctor's appointments for a given day
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
 
        // add a RV to doctor n°1 in slot n° 0
        Console.WriteLine(string.Format("Ajout d'un RV au médecin {0} avec client {1} le 23/11/2013", medecins[0], clients[0]));
        int idRv1 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
        Console.WriteLine("Rdv ajouté");
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
 
        // add an appointment in an already occupied slot - must trigger an exception
        int idRv2;
        Console.WriteLine("Ajout d'un RV dans un créneau déjà occupé");
        try
        {
          idRv2 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
          Console.WriteLine("Rdv ajouté");
          DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
        }
        catch (RdvMedecinsException ex)
        {
          Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
        }
 
        // delete an appointment
        Console.WriteLine(string.Format("Suppression du RV n° {0}", idRv1));
        dao.SupprimerRv(idRv1);
        DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
      }
      catch (Exception ex)
      {
        Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
      }
      //break 
      Console.ReadLine();
    }
 
    // utility methods - display lists
    public static void DisplayClients(string Message, List<Client> clients)
    {
      Console.WriteLine(Message);
      foreach (Client c in clients)
      {
        Console.WriteLine(c.ShortIdentity());
      }
    }
    public static void DisplayMedecins(string Message, List<Medecin> medecins)
    {
...
    }
    public static void DisplayCreneaux(string Message, List<Creneau> creneaux)
    {
...
    }
    public static void DisplayRvs(string Message, List<Rv> rvs)
    {
...
    }
  }
}
  • Zeile 14: die Referenz auf die [DAO]-Schicht. Um den Test unabhängig von der tatsächlichen Implementierung der [DAO]-Schicht zu machen, ist diese Referenz vom Typ [IDao] (die Schnittstelle) und nicht vom Typ [Dao] (die Klasse);
  • Zeile 18: Die [DAO]-Schicht wird von Spring instanziiert. Wir werden später auf die dafür erforderliche Konfiguration zurückkommen. Wir wandeln die von Spring zurückgegebene Objektreferenz in eine Referenz vom Typ der [IDao]-Schnittstelle um;
  • Zeilen 21–22: Anzeige der Kunden;
  • Zeilen 25–26: Anzeige der Ärzte;
  • Zeilen 29–30: Anzeige der Liste der Zeitfenster für Arzt Nr. 0;
  • Zeile 33: Zeigt die Termine von Arzt Nr. 0 für den 23. November 2013 an. Es sollte keine geben;
  • Zeile 37: Fügt einen Termin für Arzt Nr. 0 am 23.11.2013 hinzu;
  • Zeile 39: Zeigt die Termine von Arzt Nr. 0 am 23.11.2013 an. Es sollte einen geben;
  • Zeile 46: Der gleiche Termin wird ein zweites Mal hinzugefügt. Es sollte eine Ausnahme auftreten;
  • Zeile 57: Löscht den einzigen Termin, der hinzugefügt wurde;
  • Zeile 58: Zeigt die Termine für Arzt Nr. 0 am 23.11.2013 an. Es sollte keiner vorhanden sein.

3.6.5. Spring.net-Konfiguration

Im obigen Testprogramm haben wir kurz die Anweisung angesprochen, die die [DAO]-Schicht instanziiert:


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

Die Klasse [ContextRegistry] ist eine Spring-Klasse im Namespace [Spring.Context.Support]. Um Spring zu verwenden, müssen wir dessen DLL zu den Projektreferenzen hinzufügen. Wir gehen wie folgt vor:

  • Suchen Sie in [1] mit dem [NuGet]-Tool nach Paketen;
  • in [2] online nach Paketen suchen;
  • in [3] geben Sie das Stichwort „spring“ in das Suchfeld ein;
  • in [4] werden Pakete angezeigt, deren Beschreibung dieses Stichwort enthält. Hier ist [Spring.Core] das, was wir brauchen. Wir installieren es.

Die Projektverweise ändern sich wie folgt:

Das Paket [Spring.Core] hatte eine Abhängigkeit vom Paket [Common.Logging]. Dieses wurde ebenfalls geladen. Zu diesem Zeitpunkt sollte das Projekt keine Fehler mehr aufweisen.

Das bedeutet jedoch nicht, dass es auch funktioniert. Wir müssen zunächst Spring in der Datei [App.config] konfigurieren. Dies ist der kniffligste Teil des Projekts. Die neue Datei [App.config] sieht wie folgt aus:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
    <!-- spring -->
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
    <!-- common logging-->
    <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
  </configSections>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <!-- Entity Framework -->
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="v11.0" />
      </parameters>
    </defaultConnectionFactory>
  </entityFramework>
  <!-- Connection chains -->
  <connectionStrings>
    <add name="monContexte" connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;" providerName="System.Data.SqlClient" />
  </connectionStrings>
  <system.data>
    <DbProviderFactories>
      <add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </DbProviderFactories>
  </system.data>
  <!-- spring configuration -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
  </spring>
  <!-- configuration common.logging -->
  <logging>
    <factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
      <arg key="showLogName" value="true" />
      <arg key="showDataTime" value="true" />
      <arg key="level" value="DEBUG" />
      <arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
    </factoryAdapter>
  </logging>
</configuration>

Beginnen wir damit, alles zu entfernen, was bereits bekannt ist: Entity Framework, Verbindungszeichenfolgen, ProviderFactory. Die Datei entwickelt sich wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" ... />
    <!-- spring -->
    <sectionGroup name="spring">
      <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
      <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
    </sectionGroup>
    <!-- common logging-->
    <sectionGroup name="common">
      <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
    </sectionGroup>
  </configSections>
...
  <!-- spring configuration -->
  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
  </spring>
  <!-- configuration common.logging -->
  <common>
    <logging>
      <factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
        <arg key="showLogName" value="true" />
        <arg key="showDataTime" value="true" />
        <arg key="level" value="DEBUG" />
        <arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
      </factoryAdapter>
    </logging>
  </common>
</configuration>
  • Zeilen 3–15: definieren Konfigurationsabschnitte;
  • Zeile 8: definiert die Klasse, die den Abschnitt <spring><context> der XML-Datei verwaltet (Zeilen 19–21);
  • Zeile 9: definiert die Klasse, die den Abschnitt <spring><objects> der XML-Datei verwaltet (Zeilen 22–24);
  • Zeile 13: definiert die Klasse, die den Abschnitt <common><logging> der XML-Datei verwaltet (Zeilen 27–36);
  • Zeilen 7–14: sind stabil. Müssen in einem anderen Projekt nicht geändert werden;
  • Zeilen 18–25: Spring-Konfiguration. Ist stabil, mit Ausnahme der Zeilen 22–24, die die Objekte definieren, die Spring instanziieren wird;
  • Zeile 23: Definition eines Objekts. Das Attribut „id“ ist beliebig. Es dient als Kennung des Objekts. Das Attribut „type“ gibt die zu instanziierende Klasse in der Form „vollständiger Klassenname, Assembly, die die Klasse enthält“ an. Die Klasse ist hier diejenige, die die [DAO]-Schicht implementiert: [RdvMedecins.Dao.Dao]. Um die zugehörige Assembly zu finden, überprüfen Sie die Projekteigenschaften:

In [1] den Namen der bereitzustellenden Assembly;

  • Zeilen 27–36: Die Konfiguration „Common Logging“ ist stabil. Möglicherweise müssen Sie die Protokollierungsstufe in Zeile 32 anpassen. Nach der Debugging-Phase können Sie die Stufe auf INFO setzen.

Letztendlich erweist sich die Spring-Konfigurationsdatei, obwohl sie auf den ersten Blick komplex erscheint, als einfach. Die einzigen erforderlichen Änderungen sind:

  • Zeilen 22–24, die die zu instanziierenden Objekte definieren;
  • Zeile 32: die Protokollierungsstufe.

Im Testprogramm lautet die Anweisung, die die [DAO]-Schicht instanziiert, wie folgt:


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

[ContextRegistry] ist eine Spring-Klasse, die die in einer [Web.config]- oder [App.config]-Datei angegebene Spring-Konfiguration verwendet. Hier wird der folgende Abschnitt der [App.config]-Datei verwendet:


  <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() verwendet den in den Zeilen 2–4 definierten Kontext. Zeile 3 bedeutet, dass die Spring-Objekte im Abschnitt [spring/objects] der Konfigurationsdatei definiert sind. Dieser Abschnitt umfasst die Zeilen 5–7;
  • ContextRegistry.GetContext().GetObject("rdvmedecinsDao") verwendet den Abschnitt in den Zeilen 5–7. Es gibt eine Referenz auf das Objekt mit dem Attribut id="rdvmedecinsDao" zurück. Dies ist das in Zeile 6 definierte Objekt. Spring instanziiert dann die durch das Attribut type definierte Klasse unter Verwendung ihres parameterlosen Konstruktors. Dieser Konstruktor muss daher vorhanden sein. Sobald dies geschehen ist, wird die Referenz auf das erstellte Objekt an den aufrufenden Code zurückgegeben. Wird das Objekt im Code ein zweites Mal angefordert, gibt Spring einfach eine Referenz auf das zuerst erstellte Objekt zurück. Dies ist das als Singleton bekannte Entwurfsmuster.

Die Objektkonstruktion kann komplexer sein. Sie können einen Konstruktor mit Parametern verwenden oder die Initialisierung bestimmter Objektfelder festlegen, sobald das Objekt erstellt wurde. Weitere Informationen zu diesem Thema finden Sie im Artikel „Spring IOC Tutorial for .NET“ unter [http://tahe.developpez.com/dotnet/springioc/].

Sobald dies erledigt ist, können wir die Anwendung ausführen. Die Bildschirmausgabe sieht wie folgt aus:

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 :

Die Ergebnisse entsprechen den Erwartungen. Wir betrachten unsere [DAO]-Schicht nun als gültig. Das Tutorial könnte hier enden. Bisher haben wir behandelt:

  • die Grundlagen des Entity Framework 5 ORM;
  • eine [DAO]-Schicht, die dieses ORM nutzt.

Erinnern wir uns an unsere Fallstudie, die zu Beginn dieses Dokuments beschrieben wurde. Wir beginnen mit einer bestehenden Anwendung mit der folgenden Architektur:

die wir in Folgendes umwandeln möchten:

wobei EF5 NHibernate ersetzt hat. Wir haben soeben die [DAO2]-Schicht erstellt. Tatsächlich hat sie nicht dieselbe Schnittstelle wie die [DAO1]-Schicht, deren Schnittstelle eingeschränkter war:


  public interface IDao
  {
    // customer list
    List<Client> GetAllClients();
    // list of doctors
    List<Medecin> GetAllMedecins();
    // list of physician slots
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // list of RV from a given doctor on a given day
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // add a RV to the list
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // delete a RV
    void SupprimerRv(int idRv);
  }

Die [DAO2]-Schicht hat dieser Schnittstelle die folgende Methode hinzugefügt:


// find a T entity via its primary key
T Find<T>(int id) where T : class;

Diese Methode wurde hinzugefügt, da das EF 5 ORM standardmäßig im Lazy-Loading-Modus arbeitet. Entitäten gelangen ohne ihre Abhängigkeiten in die [ASP.NET]-Schicht. Die obige Methode ermöglicht es uns, diese bei Bedarf abzurufen, und in manchen Fällen benötigen wir sie tatsächlich. NHibernate arbeitet standardmäßig ebenfalls im Lazy-Loading-Modus, ich hatte es jedoch im Eager-Loading-Modus verwendet. Entitäten kamen mit ihren Abhängigkeiten in der [ASP.NET]-Schicht an.

Wir werden die Portierung der ASP.NET/NHibernate-Anwendung auf die ASP.NET/EF 5-Anwendung abschließen. Da dies jedoch nicht mehr EF5 betrifft, werden wir den Webcode nicht kommentieren. Wir werden lediglich erklären, wie man die Webanwendung einrichtet und testet. Sie ist auf der Website dieses Tutorials verfügbar.

3.6.6. Generieren der [DAO]-Schicht-DLL

In der folgenden Architektur:

stehen der [ASP.NET]-Schicht die Schichten zu ihrer Rechten in Form von DLLs zur Verfügung. Wir werden daher die DLL der [DAO]-Schicht erstellen.

  • Wählen Sie in [1] das Testprogramm aus und schließen Sie es in [2] nicht in die zu erstellende DLL ein;
  • Legen Sie in [3] in den Projekteigenschaften fest, dass die zu erstellende Assembly eine DLL ist;
  • In [4] legen wir im VS-Menü fest, dass wir eine [Release]-Assembly generieren, die weniger Informationen enthält als eine [Debug]-Assembly;
  • Erstellen Sie in 5 die Projekt-Assembly neu. Die DLL wird generiert;
  • Zeigen Sie in [6] alle Projektdateien an;
  • In [7] die DLL für das [DAO]-Layer-Projekt. Diese wird vom ASP.NET-Webprojekt verwendet;
  • In [8] aktualisieren wir die Projektansicht;
  • in [9] werden die DLLs aus dem Ordner [Release] in einem externen Ordner [lib] zusammengefasst [10]. Von dort bezieht das Webprojekt seine Verweise.

3.6.7. Die [ASP.NET]-Schicht

Hier erklären wir, wie die [ASP.NET / NHibernate]-Anwendung auf die [ASP.NET / EF 5]-Anwendung portiert wird. Wir arbeiten mit Visual Studio Express 2012 for Web, das kostenlos unter [http://www.microsoft.com/visualstudio/fra/downloads] erhältlich ist.

Wir beginnen mit dem bestehenden Webprojekt, das mit VS 2010 erstellt wurde.

  • In [1] öffnen wir das vorhandene Projekt:
  • In [2] weist das geladene Projekt die folgenden Verweise auf [3]:
  • [NHibernate] ist die DLL des NHibernate-Frameworks,
  • [Spring.Core] ist die DLL für das Spring.net-Framework,
  • [log4net] ist die DLL für das log4net-Logging-Framework. Dieses Framework wird von Spring.net verwendet,
  • [MySql.Data] ist der ADO.NET-Treiber für das MySQL-DBMS,
  • [rdvmedecins] ist die DLL für die mit NHibernate erstellte [DAO]-Schicht;
  • in [4] ändern wir den Projektnamen und in 5 entfernen wir die bisherigen Verweise;
  • in [6] fügen wir Verweise auf das Projekt hinzu;
  • in [7] verwenden wir im Assistenten die Option [Durchsuchen];
  • in [8] wählen wir alle DLLs aus Projekt Nr. 2 aus, die zuvor im Ordner [lib] abgelegt wurden;
  • in [9] eine Zusammenfassung, die wir bestätigen;
  • in [10] das Webprojekt mit seinen neuen Verweisen.

Sobald dies erledigt ist, sieht das Projekt wie folgt aus:

  • In [1] ist der Code zur Verwaltung der Webseiten auf die beiden Dateien [Global.asax] und [Default.aspx] aufgeteilt. Der Utility-Code wurde im Ordner [Entities] abgelegt. Schließlich wird die Anwendung über die Datei [Web.config] konfiguriert;
  • in [2] generieren wir die Projekt-Assembly;
  • in [3] treten Fehler auf.

Sehen wir uns die Fehler einmal an, zum Beispiel den folgenden:

Image

und dessen Erklärung:

Image

Der Typ von [medecin.Id] ist int?, während die Methode [GetCreneauxMedecin] vom Typ int ist. Daher ist ein Typumwandlung erforderlich. Dieser Fehler tritt im gesamten Code wiederholt auf, da die Entitäten im ASP.NET/NHibernate-Projekt Primärschlüssel vom Typ int hatten, während diejenigen im ASP.NET/EF 5-Projekt vom Typ int? sind. Wir korrigieren alle Fehler dieser Art und generieren das Projekt neu. Danach sind keine Fehler mehr vorhanden.

Vor dem Ausführen des Projekts muss noch ein weiterer Punkt geklärt werden: die Instanziierung der [DAO]-Schicht durch das Spring-Framework. Dies erfolgt in [Global.asax]:


protected void Application_Start(object sender, EventArgs e)
    {
      // caching of certain database data
      try
      {
        // layer instantiation [dao]
        Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
        ...
      }
      catch (Exception ex)
      {...
      }
    }

Im Testprogramm für die [DAO]-Schicht wurde die [DAO]-Schicht wie folgt instanziiert:


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

Die beiden Methoden sind identisch. Erinnern Sie sich daran, dass diese Instanziierung der [DAO]-Schicht auf einer in [App.config] angegebenen Konfiguration beruhte. Wir ersetzen dann den aktuellen Inhalt von [Web.config] des Webprojekts durch den von [App.config] aus dem [DAO]-Schichtprojekt, um die gleiche Konfiguration sicherzustellen.

Wir sind bereit für den ersten Lauf. Die Startseite wird angezeigt [1]:

  • In [2] geben wir ein Termin-Datum ein und klicken auf „Absenden“;
  • in [3] tritt ein Fehler auf.

Bei der Untersuchung der von der Seite angezeigten Fehlermeldung stellen wir fest, dass die gemeldete Ausnahme mit Lazy Loading zusammenhängt: Wir haben versucht, eine Abhängigkeit eines Objekts zu laden, während der Persistenzkontext, der dieses verwaltet, geschlossen war. Das Objekt befindet sich nun in einem „detached“-Zustand. Dieser Fehler ist darauf zurückzuführen, dass NHibernate im Eager-Loading-Modus verwendet wurde, während EF 5 standardmäßig im Lazy-Loading-Modus arbeitet. In der oben rot markierten Zeile:

  • steht „rdv“ für ein [Rv]-Objekt, das ohne seine Abhängigkeiten geladen wurde;
  • Um „rdv.Creneau.Id“ auszuwerten, versucht die Anwendung, die Abhängigkeit „rdv.Creneau“ zu laden. Da wir uns jedoch nicht mehr im Kontext befinden, ist dies nicht möglich, weshalb die Ausnahme ausgelöst wird.

Hier ist die Lösung einfach. Zeile 108: Wir erstellen einen Eintrag in einem Wörterbuch mit dem Primärschlüssel des Terminfensters als Schlüssel. Es stellt sich jedoch heraus, dass die Entität [Rv] den Primärschlüssel des zugehörigen Fensters kapselt. Also schreiben wir:


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

Wir versuchen, den Code erneut auszuführen. Diesmal lautet die Fehlermeldung wie folgt:

Der Fehler ist ähnlich. Zeile 132: Wir versuchen, die [Client]-Abhängigkeit eines [Rv]-Objekts in der ASP.NET-Schicht zu laden, was außerhalb des Kontexts liegt. Wir müssen das [Client]-Objekt aus der Datenbank abrufen. Um dieses Problem zu beheben, wurde die [IDao]-Schnittstelle um die folgende Methode erweitert:


    // find a T entity via its primary key
    T Find<T>(int id) where T : class;

Dadurch können wir Abhängigkeiten abrufen. Somit wird die fehlerhafte Zeile oben wie folgt umgeschrieben:


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

Erneut wird der Vorteil deutlich, dass Entitäten ihre Fremdschlüssel einbetten. Hier ermöglicht uns die Entität [Rdv] den Zugriff auf den Fremdschlüssel der zugehörigen Abhängigkeit [Creneau]. Nach diesen beiden Korrekturen funktioniert die Anwendung. Der Leser ist eingeladen, die Anwendung [RdvMedecins-SqlServer-03] zu testen, die in den Beispiel-Downloads auf der Website dieses Artikels verfügbar ist.

3.7. Fazit

Wir haben erfolgreich eine ASP.NET-/NHibernate-Anwendung portiert:

auf eine ASP.NET/EF 5-Anwendung:

Obwohl diese Architektur es uns eigentlich ermöglicht hätte, die [ASP.NET]-Schicht unverändert zu lassen, mussten wir sie aus zwei Gründen anpassen:

  • Die Entitäten waren nicht exakt identisch. Der Primärschlüsseltyp für NHibernate-Entitäten war int, während er für EF 5 int?</span>**<span style="color: #000000"> war. Dies führte dazu, dass wir im Webcode Typumwandlungen einführen mussten;
  • der Entitätslademodus war bei den beiden ORMs nicht identisch: Eager Loading bei NHibernate, Lazy Loading bei EF 5. Dies veranlasste uns, die Schnittstelle der [DAO]-Schicht um eine generische Methode zu erweitern, die es uns ermöglicht, eine Entität über ihren Primärschlüssel abzurufen.

Dennoch erwies sich die Portierung als recht unkompliziert, was einmal mehr – falls es eines Beweises bedurfte – die geschichtete Architektur und die Abhängigkeitsinjektion mit Spring oder einem anderen Abhängigkeitsinjektions-Framework rechtfertigte.

Wir werden nun die Auswirkungen eines DBMS-Wechsels auf die bisherige Architektur bewerten. Wir werden alle bisherigen Projekte auf vier weitere DBMS portieren:

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

Der Code bleibt unverändert. Es ändern sich lediglich die folgenden Elemente:

  • die Definition des Feldes in den Entitäten, das zur Steuerung des gleichzeitigen Zugriffs auf eine Entität dient;
  • die Konfigurationsdateien [App.config] oder [Web.config];

Wir werden nur auf die Elemente eingehen, die sich ändern.