Skip to content

3. Fallstudie mit SQL Server Express 2012

3.1. Introduction

Die im Internet zu findenden Beispiele für Entity Framework beziehen sich größtenteils auf den SQL-Server. Das ist durchaus normal. Wahrscheinlich ist der SGBD weltweit der am weitesten verbreitete Server dieser Art in Unternehmen. Wir werden diesem Trend folgen. Die Beispiele werden anschließend 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 nämlich eine Vielzahl von Screenshots erfordern, die recht schnell veralten. Diese Aufgabe (die zugegebenermaßen nicht immer einfach ist) überlassen wir dem Leser.

Wir müssen die folgenden Tools installieren:

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

Sobald SGBD installiert ist, starten wir es:

  • [1]: Starten Sie im Startmenü den „SQL Server-Konfigurationsmanager“;
  • [2]: Starten Sie in diesem Manager den Server;
  • [3]: Er ist gestartet.

Nun starten wir das Verwaltungstool des SQL-Servers:

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

Wir werden uns nun mit dem Server verbinden:

  • In [1] öffnen wir den Objekt-Explorer;
  • in [2] geben wir die Verbindungsparameter ein:
  • [3]: Der (lokale) Server (bitte auf die erforderlichen Klammern achten) bezeichnet den auf dem Rechner installierten Server,
  • [4]: Man wählt die Windows-Authentifizierung. Man muss Administrator des eigenen Rechners sein, damit diese Verbindung erfolgreich hergestellt werden kann,
  • [5]: Man stellt eine Verbindung her;
  • [6]: Die Verbindung ist hergestellt;
  • [7]: Man möchte bestimmte Eigenschaften des Servers ändern;
  • [8]: Es werden zwei Authentifizierungsmodi angefordert:
  • Windows-Authentifizierung, wie sie gerade verwendet wurde. Ein Windows-Benutzer mit den entsprechenden Rechten kann sich dann anmelden,
  • SQL-Server-Authentifizierung. Der Benutzer muss zu den in SGBD registrierten Benutzern gehören;

Anschließend können die Eigenschaften des Servers bestätigt werden;

  • [9]: Man bearbeitet die Eigenschaften des Benutzers „sa“ (Systemadministrator);
  • In [10] wird ein Passwort festgelegt. Im weiteren Verlauf dieses Dokuments lautet dieses „sqlserver2012“;
  • in [10] wird ihm die Berechtigung zum Anmelden erteilt;
  • in [11] wird die Verbindung aktiviert. Damit kann der Assistent abgeschlossen werden;
  • in [12] wird die Verbindung zum Server getrennt.

Nun melden wir uns erneut mit dem Login „sa/sqlserver2012“ an:

  • in [1] wird die Verbindung wiederhergestellt;
  • in [2] erfolgt die Authentifizierung in SQL Server;
  • in [3] lautet der Benutzer „sa“;
  • in [4] lautet sein Passwort „sqlserver2012“;
  • in [5] wird die Verbindung hergestellt;
  • in [6] ist man angemeldet.

Wir werden nun eine Demo-Datenbank erstellen:

  • in [1] erstellen wir eine neue BD;
  • in [2], sie wird „demo“ heißen;
  • in [3] bestätigen wir;
  • in [4]: Die Datenbank wurde angelegt;
  • in [5] wird eine neue Tabelle in der Datenbank erstellt; demo;
  • In [6] wird eine Tabelle mit zwei Spalten definiert: ID und NOM;
  • in [7] wird die Spalte [ID] zum Primärschlüssel;
  • in [8] wird der Primärschlüssel durch ein Schlüsselsymbol gekennzeichnet;
  • in [9] wird die Tabelle gesichert;
  • in [10] wird ihr ein Name zugewiesen;
  • in [11] muss die Datenbank aktualisiert werden, damit die Tabelle in der Datenbank [demo] erscheint;
  • in [12] wurde die Tabelle [PERSONNES] erfolgreich angelegt.

Damit wissen wir vorerst genug über die Verwendung des Verwaltungstools von SQL Server.

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

VS Express 2012 wird mit einem integrierten Server SQL ausgeliefert. Hier wird davon ausgegangen, dass VS Express 2012 installiert wurde. Man startet VS 2012:

Das Verwaltungstool von SQL Server 2012 [2] wird gestartet und eine Verbindung zu [3] hergestellt.

  • in [4] stellt man eine Verbindung zum Server (localdb)\v11.0 her;
  • in [5], mit Windows-Authentifizierung;
  • in [6] werden nach erfolgreicher Verbindung die Datenbanken des Servers angezeigt. Wie zuvor könnte man eine neue Datenbank anlegen.

Wir werden diesen in VS 2012 integrierten Server nicht verwenden.

3.4. Erstellung der Datenbank anhand der Entitäten

Mit Entity Framework 5 Code First lässt sich eine Datenbank anhand von Entitäten erstellen. Das sehen wir uns nun an. 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] ermöglicht das Tool NuGet das Herunterladen von Abhängigkeiten;
  • in [2]; damit wird die Entity-Framework-Abhängigkeit heruntergeladen;
  • 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 Version von DLL. Es wird Version 5 benötigt;
  • in [2], ihr Speicherort im Dateisystem: <solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll, wobei <solution> der Ordner der Lösung VS ist. Alle von NuGet hinzugefügten Pakete werden in den Ordner <solution>/packages verschoben;
  • in [3] wurde eine Datei [packages.config] erstellt. Ihr Inhalt lautet wie folgt:

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

Sie listet die von NuGet importierten Pakete auf.

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

  • in [1], Hinzufügen eines Ordners zum Projekt;
  • in [2], er wird [Models] heißen.

Wir werden diese Vorgehensweise beibehalten und die Definitionen unserer Entitäten künftig im Ordner [Models] ablegen.

Um unsere Entitäten zu erstellen, werden wir uns auf die Definition der Datenbank MySQL 5 stützen, die im Projekt NHibernate verwendet wird. Zur Erinnerung: Die Rolle der Entitäten EF:

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

3.4.1. Die Entität [Medecin]

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

  • ID: Identifikationsnummer des Arztes – Primärschlüssel der Tabelle
  • VERSION: Nummer zur Identifizierung der Zeilenversion in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • NOM: Name des Arztes
  • PRENOM: sein Vorname
  • TITRE: sein Titel (Fräulein, Frau, Herr)

Wir könnten von der folgenden Klasse [Medecin] ausgehen:


using System;

[Table("MEDECINS", Schema = "dbo")]
  namespace RdvMedecins.Entites
{
  public class Medecin
  {
    // Daten
    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 der Tabelle [MEDECINS] in der Datenbank zugeordnet. Diese befindet sich in einem Schema namens „dbo“.

Wir speichern diese Klasse in einer Datei mit dem Namen [Entites.cs] [1]. Dort werden wir alle unsere Entitäten ablegen.

Ebenfalls im Ordner „[Models]“ erstellen wir die folgende Datei „[Context.cs]“:


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

namespace RdvMedecins.Models
{

  // der Kontext
  public class RdvMedecinsContext : DbContext
  {
    // die Ärzte
    public DbSet<Medecin> Medecins { get; set; }
  }

  // Initialisierung der Datenbank
  public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
  {
  }
}
  • Zeile 8: Die Klasse [RdvMedecinsContext] stellt den Persistenzkontext c.-à-d dar. die Gesamtheit der von ORM verwalteten Entitäten. Sie muss von der Klasse [System.Data.Entity.DbContext] abgeleitet sein;
  • Zeile 11: Das Feld [Medecins] repräsentiert die Entitäten vom Typ [Medecin] des Persistenzkontexts. Sie ist vom Typ DbSet<Medecin>. In der Regel gibt es so viele [DbSet] wie Tabellen in der Datenbank, nämlich eine pro Tabelle;
  • Zeile 15: Es wird eine Klasse [RdvMedecinsInitializer] definiert, um die erstellte Datenbank zu initialisieren. Diese leitet sich hier 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 in der Entwicklungsphase von BD praktisch. Der Parameter der Klasse [DropCreateDataBaseAlways] ist der Typ des Persistenzkontexts, der der Datenbank zugeordnet ist. Für die Initialisierungsklasse können auch andere übergeordnete Klassen als [DropCreateDataBaseAlways] verwendet werden:
  • [DropCreateDatabaseIfModelChanges]: Erstellt die Datenbank neu, wenn sich die Entitäten geändert haben,
  • [CreateDatabaseIfNotExists]: Erstellt die Datenbank, falls sie nicht existiert;

Nun müssen wir noch ein Hauptprogramm erstellen. Dieses lautet wie folgt: [CreateDB_01.cs]:


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

namespace RdvMedecins_01
{
  class CreateDB_01
  {
    static void Main(string[] args)
    {
      // Die Datenbank wird erstellt
      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] lässt sich die Initialisierungsklasse der Datenbank festlegen. Dies löst die Initialisierung jedoch nicht aus;
  • Zeile 13: Um mit einem Persistenzkontext zu arbeiten, muss dieser instanziiert werden. Genau das geschieht hier. Es wird eine „using“-Klausel verwendet, damit der Kontext beim Verlassen der Klausel automatisch geschlossen wird. In Zeile 17 wird der Kontext also geschlossen;
  • Zeile 15: Die Generierung der zum Persistenzkontext [RdvMedecinsContext] gehörenden Datenbank wird explizit gestartet. Der Parameter false gibt an, dass dieser Vorgang nicht ausgeführt werden soll, wenn er für diesen Kontext bereits durchgeführt wurde. Hier hätte man genauso gut true angeben können.

Bei der Arbeit mit einer Datenbank werden die Verbindungsparameter in der Regel in die Datei [App.config] geschrieben. Wir stellen fest, dass sie dort derzeit noch nicht vorhanden sind:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Weitere Informationen zur Konfiguration des Entity Frameworks finden Sie unter 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 in die Datei „[App.config]“ geschrieben, als die Entity-Framework-Abhängigkeit zu den Projektverweisen hinzugefügt wurde.

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

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

Wir stellen fest, dass eine Datenbank mit dem vollständigen Namen der Klasse [RdvMedecinsContext] angelegt wurde und dass sie eine Tabelle [dbo.MEDECINS] (so haben wir sie benannt) mit Spalten, deren Namen den Feldnamen der Entität [Medecin] entsprechen. Wenn der Code erfolgreich ausgeführt wurde und die oben genannte Datenbank nicht angezeigt wird, muss der eingebettete Server (localdb)\v11.0 überprüft werden (siehe Seite 19). Bei VS 2012 Pro wird dieser Server verwendet, wenn der Server SQL zum Zeitpunkt der Codeausführung nicht aktiv ist. Bei VS 2012 Express ist dies nicht der Fall.

Betrachten wir die Struktur der Tabelle [MEDECINS]:

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

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

Bei [1] sehen wir, dass der Primärschlüssel vom Typ [Identité] ist, was bedeutet, dass sein Wert automatisch vom SQL-Server generiert wird. Wir werden diese Strategie bei allen SGBD anwenden.

Durch die Verwendung von Annotationen werden wir uns weniger an die Konventionen von EF halten. Der Code der Entität in [Entites.cs] sieht dann wie folgt aus:


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

namespace RdvMedecins.Entites
{
  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin
  {
    // Daten
    [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 in den Namensräumen [System.ComponentModel.DataAnnotations] (Key, Required, MaxLength) und [System.ComponentModel.DataAnnotations.Schema] (Column). Weitere Annotationen finden sich in den Namensräumen URL und [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
  • Zeile 11: [Key] bezeichnet den Primärschlüssel;
  • Zeile 12: [Column] legt den Namen der Spalte fest, die dem Feld entspricht;
  • Zeile 14: [Required] gibt an, dass das Feld ein Pflichtfeld ist (SQL, NOT, NULL);
  • Zeile 15: [MaxLength] legt die maximale Länge der Zeichenkette fest, [MinLength] ihre minimale Länge;

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

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

In der Anwendung NHibernate diente die Spalte [VERSION] dazu, gleichzeitige Zugriffe auf dieselbe Zeile einer Tabelle zu verhindern. Das Prinzip lautet wie folgt:

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

Dies wird als optimistische Verwaltung gleichzeitiger Zugriffe bezeichnet. Bei EF 5 muss ein Feld, das diese Rolle spielt, eines der beiden Attribute [Timestamp] oder [ConcurrencyCheck] aufweisen. SQL Server hat den Typ [timestamp]. Bei einer Spalte dieses Typs wird der Wert bei jedem Einfügen oder Ändern einer Zeile automatisch vom SQL Server generiert. Eine solche Spalte kann dann zur Verwaltung von Zugriffskonflikten verwendet werden. Um das vorherige Beispiel aufzugreifen: Der Prozess P2 findet einen timestamp, der sich von dem unterscheidet, den er gelesen hat, da er in der Zwischenzeit durch die Änderung des Prozesses P1 geändert wurde.

Unsere Entität [Medecin] 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
  {
    // Daten
    [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 gewählt werden. Das Attribut [Required] wird nicht hinzugefügt, da dieser Wert nicht von der Anwendung, sondern vom SGBD selbst bereitgestellt wird.

Wenn man das Projekt mit dieser neuen Entität ausführt, ändert sich die Datenbank wie folgt:

Nun müssen wir noch einen letzten Punkt klären. Der Persistenzkontext „weiß“, dass eine Entität in die Datenbank eingefügt werden muss, da ihr Primärschlüssel zu diesem Zeitpunkt den Wert „null“ hat. Erst durch das Einfügen in die Datenbank erhält der Primärschlüssel einen Wert. Hier ist der Typ „int“, der dem Primärschlüssel „[Id]“ zugewiesen wurde, nicht geeignet, da dieser Typ den Wert „null“ nicht akzeptiert. Man weist ihm daher den Typ „int?“ zu, der die Werte „int“ sowie den Zeiger „null“ akzeptiert. Die verwendete Entität „[Medecin]“ lautet somit wie folgt:


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

Nun müssen wir noch klären, wie das Konzept des Fremdschlüssels zwischen Tabellen in einer Entität dargestellt werden kann.

3.4.2. Die Entität [Creneau]

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

  • ID: Nummer zur Identifizierung des Zeitfensters – Primärschlüssel der Tabelle
  • VERSION: Nummer zur Identifizierung der Version des Datensatzes in der Tabelle. Diese Nummer wird bei jeder Änderung des Datensatzes um 1 erhöht.
  • ID_MEDECIN: Nummer zur Identifizierung des Arztes, zu dem dieses Zeitfenster gehört – Fremdschlüssel auf die Spalte MEDECINS(ID).
  • HDEBUT: Startzeit des Zeitfensters
  • MDEBUT: Startminute des Zeitfensters
  • HFIN: Endzeit des Zeitfensters
  • MFIN: Minuten des Zeitfensterschlusses

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

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


[Table("CRENEAUX", Schema = "dbo")]
  public class Creneau
  {
    // Daten
    [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 Neuerung betrifft die 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] wider, Zeile 21. Der Name des Feldes spielt keine Rolle, wichtig ist lediglich der Typ. Die Eigenschaft muss mit dem Schlüsselwort virtual als virtuell deklariert werden. Tatsächlich muss EF alle sogenannten Navigationseigenschaften neu definieren, d. h. diejenigen, die einem Fremdschlüssel entsprechen und den Wechsel von einer Tabelle zur anderen 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
{

  // der Kontext
  public class RdvMedecinsContext : DbContext
  {
    // die Entitäten
    public DbSet<Medecin> Medecins { get; set; }
    public DbSet<Creneau> Creneaux { get; set; }
  }

  // Initialisierung der Datenbank
  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 erfolgreich angelegt; neu hinzugekommen sind die Fremdschlüssel [1] und [2]. Sein Name wurde aus dem Namen des entsprechenden Feldes in der Entität (Medecin) generiert, an den das Suffix „_Id“ angehängt wurde. Um die Eigenschaften dieses Fremdschlüssels zu erfahren, versuchen wir, ihn in [3] zu ändern.

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

Wenn man die Entitäten für eine bestehende Datenbank anlegt, heißt die Fremdschlüsselspalte nicht unbedingt [Medecin_Id]. Bei den anderen Spalten hatten wir gesehen, dass die Anmerkung [Column] dieses Problem löste. Seltsamerweise ist es bei einem Fremdschlüssel komplizierter. Man muss wie folgt vorgehen:


public class Creneau
  {
    // Daten
    ...
    [Required]
    [Column("MEDECIN_ID")]
    public int MedecinId { get; set; }
    [Required]
    [ForeignKey("MedecinId")]
    public virtual Medecin Medecin { get; set; }
    ...
}
  • Zeilen 5–7: Man legt ein Feld vom Typ des Fremdschlüssels (int) an. Mit Hilfe des Attributs [Column] gibt man den Namen der Spalte an, die in der der Entität zugeordneten Tabelle als Fremdschlüssel dienen soll;
  • Zeile 9: Man fügt dem Feld vom Typ [Medecin] die Anmerkung [ForeignKey] hinzu. Das Argument dieser Anmerkung ist der Name des Feldes (nicht der Spalte), das der Fremdschlüsselspalte der Tabelle zugeordnet ist.

Bei der Ausführung des Projekts wird diesmal die folgende Tabelle erstellt:

Wie oben zu sehen ist, trägt die Fremdschlüsselspalte tatsächlich den Namen, den wir ihr gegeben haben. Es ist zu beachten, dass die Felder:


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

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

Das oben genannte Feld [Medecin] spiegelt die Mehr-zu-Eins-Beziehung wider, die die Entität [Creneau] mit der Entität [Medecin] verbindet. Mehrere Objekte vom Typ [Creneau] sind mit ein und demselben Objekt vom Typ [Medecin] verknüpft. Die umgekehrte Beziehung, bei der ein Objekt [Medecin] mehreren Objekten [Creneau] zugeordnet ist, lässt sich durch ein zusätzliches Feld in der Entität [Medecin] modellieren:


public class Medecin
  {
    // Daten
    [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 wurde das Feld [Creneaux] hinzugefügt, das eine Sammlung von Objekten vom Typ [Creneau] darstellt. Über dieses Feld erhalten wir Zugriff auf alle Terminfenster des Arztes.

Wenn man das Projekt erneut ausführt, stellt man fest, dass sich die Tabelle [MEDECINS] nicht verändert hat:

 

Es wurde keine Spalte hinzugefügt. Die Fremdschlüsselbeziehung zwischen der Tabelle [CRENEAUX] und der Tabelle [MEDECINS] reicht aus, damit EF die damit verbundenen 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; }
    ...
  }

Das Wesentliche wissen wir bereits. Wir können nun mit der Erstellung der beiden anderen Entitäten abschließen.

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

Mit dem, was wir gelernt haben, können wir die Entitäten [Client] und [Rv] erstellen. Die Entität [Client] enthält Informationen zu den Kunden, die von der Anwendung [RdvMedecins] verwaltet werden.

  • ID: Nummer zur Identifizierung des Kunden – Primärschlüssel der Tabelle
  • VERSION: Nummer zur Identifizierung der Zeilenversion in der Tabelle. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
  • NOM: Name des Kunden
  • PRENOM: sein Vorname
  • TITRE: seine Anrede (Fräulein, Frau, Herr)

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


  [Table("CLIENTS", Schema = "dbo")]
  public class Client
  {
    // Daten
    [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; }
    // die Termine des Kunden
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }
}

Die Klasse [Client] ist nahezu identisch mit der Klasse [Medecin]. Man könnte sie von derselben übergeordneten Klasse ableiten. Die Neuerung befindet sich in Zeile 21. Sie spiegelt die Tatsache wider, dass ein Kunde mehrere Termine haben kann, und ergibt sich aus dem Vorhandensein eines Fremdschlüssels von der Tabelle [RVS] zur Tabelle [CLIENTS].

Die Entität [Rv] stellt einen Termin dar:

  • ID: Nummer, die RV eindeutig identifiziert – Primärschlüssel
  • JOUR: Tag des RV
  • ID_CRENEAU: Zeitfenster des RV – Fremdschlüssel auf die Spalte [ID] der Tabelle [CRENEAUX] – legt sowohl das Zeitfenster als auch den betreffenden Arzt fest.
  • ID_CLIENT: Nummer des Kunden, für den die Reservierung vorgenommen wird – Fremdschlüssel auf die Spalte [ID] der Tabelle [CLIENTS]

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


[Table("MEDECINS", Schema = "dbo")]
  public class Rv
  {
    // Daten
    [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: Fremdschlüssel von der Tabelle [RVS] zur Tabelle [CLIENTS];
  • Zeilen 13–15: der Kunde, der den Termin hat;
  • Zeilen 16–17: Fremdschlüssel der Tabelle [RVS] zur Tabelle [CRENEAUX];
  • Zeilen 18–20: das Zeitfenster des Termins;
  • Zeilen 21–23: das Feld zur Verwaltung gleichzeitiger Zugriffe.

In Zeile 17 ist eine Mehr-zu-Eins-Beziehung zu erkennen: Einem Zeitfenster können mehrere Termine zugeordnet sein (nicht am selben Tag). Die umgekehrte Beziehung lässt sich in der Entität [Creneau] abbilden:


public class Creneau
  {
    // die Termine des Zeitfensters
    public ICollection<Rv> Rvs { get; set; }
    ...
}

Zeile 4: die Sammlung der Termine, die für diesen Zeitblock vereinbart wurden.

Bei Ausführung des Projekts wird folgende Datenbank generiert:

 

Die Tabellen [MEDECINS] und [CRENEAUX] haben sich nicht verändert. Die Tabellen [CLIENTS] und [RVS] sehen wie folgt aus:

Das war zu erwarten. Es sind noch einige Details zu klären:

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

3.4.4. Den Namen der Datenbank festlegen

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


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Weitere Informationen zur Konfiguration des Entity Frameworks finden Sie unter 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>

  <!-- Verbindungszeichenfolge zur Datenbank -->
  <connectionStrings>
    <add name="RdvMedecinsContext"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- Der 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 Verbindungszeichenfolge zur Datenbank;
  • Zeile 16: Das Attribut [name] übernimmt den Namen der Klasse [RdvMedecinsContext], die für den Persistenzkontext verwendet wird. Es ist wichtig, dies zu beachten. Diese Einschränkung kann im Konstruktor des Kontexts umgangen werden:

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

In diesem Fall könnte „name= „monContexte““ lauten. Genau das werden wir im weiteren Verlauf des Dokuments vorfinden.

  • Zeile 17: die Verbindungszeichenfolge. [Data Source]: der Name des Servers, auf dem sich SGBD befindet, [Initial Catalog]: der Name der Datenbank, hier also [rdvmedecins-ef], [User Id]: der Eigentümer der Verbindung, [Password]: sein Passwort. Der Leser passt diese Zeichenfolge an seine Umgebung an;
  • Zeilen 21–29: definieren ein [DbProviderFactory]. Ich weiß nicht, was das ist. Dem Namen nach zu urteilen, könnte es sich um eine Klasse handeln, mit der die Ebene [ADO.NET] generiert wird, die EF von SGBD trennt:

Eigentlich sind diese Zeilen für den SQL-Server überflüssig, aber ich musste sie für die anderen SGBD-Server hinzufügen. Ich füge sie hier also nur zur Erinnerung ein. Sie stören nicht. Der einzige wichtige Punkt ist die Version in Zeile 27. Es handelt sich um die Version von DLL und [System.Data], die in den Projektreferenzen aufgeführt sind:

Das war’s. Wir sind bereit. Wir führen das Projekt aus und erhalten die folgende [rdvmedecins-ef]-Basis:

 

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

3.4.5. Befüllen der Datenbank

Die Initialisierungsklasse der Datenbank kann zum Einfügen von Daten verwendet werden:


public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
  {
    // Initialisierung der Datenbank
    public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
    {
      protected override void Seed(RdvMedecinsContext context)
      {
        base.Seed(context);
        // Die Datenbank wird initialisiert
        // die Kunden
        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);
        }
        // die Ärzte
        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);
        }
        // die Zeitfenster
        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);
        }
        // die Termine
        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 ist in der übergeordneten Klasse vorhanden. Sie wird hier neu definiert. Das Argument ist der Persistenzkontext [RdvMedecinsContext] der Anwendung;
  • Zeile 8: Das Argument wird an die übergeordnete Klasse übergeben; es ist wahrscheinlich, dass diese den ihr übergebenen Persistenzkontext öffnet, da dieses Öffnen im weiteren Verlauf nicht mehr erforderlich ist;
  • Zeilen 11–16: Erstellung von 4 Kunden;
  • Zeilen 17–20: Diese werden dem Persistenzkontext hinzugefügt, genauer gesagt dessen Ärzten. Zu beachten ist die Methode [Add], die dies ermöglicht. An dieser Stelle sei an die Definition des Kontexts erinnert:

  public class RdvMedecinsContext : DbContext
  {
    // Einrichtungen
    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 heißt auch, dass die Kunden dem Kontext zugeordnet wurden, d. h., sie werden nun von EF verwaltet. Zuvor waren sie nicht zugeordnet. Sie existierten zwar als Objekte, wurden aber nicht von EF verwaltet;

  • Zeilen 21–27: Erstellung von 4 Ärzten;
  • Zeilen 28–31: Sie werden in den Persistenzkontext aufgenommen;
  • Zeilen 33–70: Erstellung von Terminfenstern. Zeilen 34–57 für den Arzt „medecins[0]“, Zeilen 58–69 für den Arzt „medecins[1]“. Die anderen Ärzte haben keine Terminfenster;
  • 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 dessen Speicherung im Persistenzkontext.

Wenn man das Projekt ausführt, erhält man die folgende Datenbank:

Oben ist die gefüllte Tabelle [CLIENTS] zu sehen.

3.4.6. Änderung der Entitäten

Derzeit sind die Klassen [Medecin] und [Client] nahezu identisch. Wenn man die Felder entfernt, die für die Persistenzverwaltung mit EF 5 hinzugefügt wurden, sind sie sogar identisch. Wir werden sie von einer Klasse [Personne] ableiten. Diese beiden Entitäten sehen dann wie folgt aus:


// eine Person
  public abstract class Personne
  {
    // Daten
    [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; }

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

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

  }

  [Table("MEDECINS", Schema = "dbo")]
  public class Medecin : Personne
  {
    // die Sprechzeiten des Arztes
    public ICollection<Creneau> Creneaux { get; set; }
    // Unterschrift
    public override string ToString()
    {
      return String.Format("Medecin {0}", base.ToString());
    }
  }

[Table("CLIENTS", Schema = "dbo")]
    public class Client : Personne
  {
    // die Termine des Kunden
    public ICollection<Rv> Rvs { get; set; }
    // Unterschrift
    public override string ToString()
    {
      return String.Format("Client {0}", base.ToString());
    }
  }

Wenn man das Projekt ausführt, erhält man dieselbe Datenbank. EF 5 hat die untersten Klassen der Vererbung jeweils einer Tabelle zugeordnet. Tatsächlich verfügt EF 5 über verschiedene Strategien zur Tabellengenerierung, um die Vererbung von Entitäten darzustellen. Wir werden diese hier nicht vorstellen. Man kann beispielsweise den Artikel „ Entity Framework Code First Inheritance: Table Per Hierarchy and Table Per Type“ unter URL [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per] lesen.

Wir werden fortan diese Version der Entitäten verwenden.

3.4.7. Einschränkungen zur Datenbank hinzufügen

Ein Detail müssen wir noch klären. Die Tabelle [RVS] für Termine sieht wie folgt aus:

 

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

Mit dem „SQL Server Management Studio“ habe ich keine einfache Methode gefunden, diese Einschränkung hinzuzufügen, außer den Befehl SQL auszuführen, der sie erstellt:

  • In [1] wird eine Abfrage SQL für die Datenbank [rdvmedecins-ef] erstellt;
  • in [2] die Abfrage SQL, die die Eindeutigkeitsbeschränkung erstellt;
  • in [3] hat die Ausführung dieser Abfrage einen neuen Index in der Tabelle [RVS] erstellt.

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

  • In [1] speichern wir eine Datenbank;
  • in [2] stellen wir eine Verbindung zum (lokalen) Server her;
  • in [3] mit einer Authentifizierung über den Server SQL;
  • in [4] unter der Identität „sa“;
  • in [5] mit dem Passwort „sqlserver2012“;
  • in [6], dann geht es weiter zum nächsten Schritt;
  • in [7] wählen Sie die Datenbank [rdvmedecins-ef] aus;
  • In [8] schließen wir den Assistenten ab;
  • Bei [9] erscheint die Datenbank in der Datenbankstruktur. Man stellt eine Verbindung zu ihr her: [10];
  • in [11] ist die Verbindung hergestellt.

Mit „SQL Manager Lite für SQL Server“ kann die Eindeutigkeitsbeschränkung für die Tabelle [RVS] erstellt werden.

  • In [1] sehen wir die zuvor erstellte Eindeutigkeitsbeschränkung;
  • in [2] wird sie gelöscht;
  • in [3] ist der dieser Eindeutigkeitsbeschränkung entsprechende Index verschwunden.

Wir erstellen die gelöschte Einschränkung erneut:

  • In [1] erstellen wir einen neuen Index für die Tabelle [RVS];
  • in [2] wird ihr ein Name zugewiesen;
  • in [3] handelt es sich um eine Eindeutigkeitsbeschränkung;
  • in [4] für die Spalten JOUR und CRENEAU_ID;

Die Registerkarte „DDL“ gibt uns den Code „SQL“ an, der ausgeführt wird:

  • In [6] wird der Befehl SQL kompiliert;
  • in [7], bestätigen;
  • zu [8], der neue Index ist erschienen.

Die Benutzeroberfläche von „SQL Manager Lite for SQL Server“ entspricht der von „SQL Server Management Studio“. Ähnliche Benutzeroberflächen finden sich für die Versionen SGBD (Oracle), PostgreSQL, Firebird und MySQL. Daher werden wir nun mit dieser Familie von Verwaltungstools für SGBD fortfahren.

Um auf die Informationen einer Tabelle zuzugreifen, genügt ein Doppelklick darauf:

Die Informationen zur ausgewählten Tabelle sind in Registerkarten verfügbar. Oben sehen wir die Registerkarte [Fields] der Tabelle [CLIENTS]. Die Registerkarte [Data] zeigt den Inhalt der Tabelle an:

Image

3.4.8. Die endgültige Datenbank

Wir haben nun unsere endgültige Datenbank. Wir exportieren das zugehörige Skript SQL, um die Datenbank bei Bedarf neu erstellen zu können.

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

Das Skript wurde generiert und in den Skript-Editor geladen. Sie können den generierten Code SQL einsehen. Wir werden die Datenbank anhand dieses Skripts neu aufbauen.

  • In [1] wird die Datenbank gelöscht;
  • in [2] und [3] wird sie neu angelegt;
  • in [4] erfolgt die Authentifizierung;
  • in [5] wird das Skript SQL zur Erstellung der Datenbank ausgeführt;
  • in [6] wird sie im „SQL Manager“ gespeichert;
  • in [7] stellt man eine Verbindung zur soeben angelegten Datenbank her;
  • In [8] enthält die Datenbank derzeit keine Tabellen;
  • In [9a] wird ein Skript-Editor SQL geöffnet;
  • in [9b] wird das zuvor erstellte Skript SQL geöffnet;
  • in [10] wird es ausgeführt;
  • in [11] wurden die Tabellen angelegt;
  • in [12] werden sie gefüllt;
  • in [14] finden wir die Eindeutigkeitsbeschränkung wieder, die wir für die Tabelle [RVS] erstellt hatten.

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

3.5. Nutzung der Datenbank mit Entity Framework

Wir werden:

  • Elemente der Datenbank hinzufügen, löschen und ändern;
  • Abfragen an die Datenbank mit „LINQ to Entities“ durchführen;
  • den gleichzeitigen Zugriff auf dasselbe Element der Datenbank verwalten;
  • die Konzepte von Lazy Loading und Eager Loading verstehen;
  • erfahren, dass die Aktualisierung der Datenbank durch den Persistenzkontext in einer Transaktion erfolgt.

3.5.1. Löschen von Elementen aus dem Persistenzkontext

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())
      {
        // die aktuelle Datenbank wird geleert
        // die Kunden
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        // die Ärzte
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // Speichern des Persistenzkontexts
        context.SaveChanges();
      }
    }
  }
}
  • Zeile 9: Operationen in einem Persistenzkontext erfolgen immer in einer [using]-Klausel. Dadurch wird sichergestellt, dass der Kontext beim Verlassen von [using] geschlossen wurde;
  • Zeile 13: Der Kundenkontext [context.Clients] wird durchlaufen. Alle Kunden der Datenbank werden in den Persistenzkontext aufgenommen;
  • Zeile 15: Für jeden von ihnen wird die Operation [Remove] ausgeführt, die sie aus dem Kontext entfernt. Tatsächlich befinden sie sich weiterhin im Kontext, jedoch im Status „gelöscht“;
  • Zeilen 18–21: Das Gleiche wird für die Ärzte durchgeführt;
  • Zeile 23: Der Persistenzkontext wird in der Datenbank gesichert.

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

  • einen Null-Primärschlüssel haben, werden einer Operation SQL INSERT unterzogen;
  • sich im Status „gelöscht“ befinden, werden einer Operation SQL DELETE unterzogen;
  • befinden sich im Status „geändert“ und sind Gegenstand der Transaktionen SQL und UPDATE;

Wie wir später sehen werden, finden diese Vorgänge SQL innerhalb einer Transaktion statt. Wenn einer davon fehlschlägt, werden alle zuvor durchgeführten Schritte rückgängig gemacht.

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

Überprüfen wir die Datenbank. Wir werden feststellen, dass alle Tabellen leer sind ([2]). Das ist überraschend, da wir lediglich das Löschen der Ärzte und Kunden angefordert hatten. Durch das Zusammenspiel der Fremdschlüssel wurden die anderen Tabellen in einer Kettenreaktion geleert.

Die Definition des Fremdschlüssels von der Tabelle [CRENEAUX] zur Tabelle [MEDECINS] wurde vom Provider von EF 5 wie folgt definiert:

  • In [1] wählen Sie die Tabelle [CRENEAUX] aus;
  • in [2] wählt man die Registerkarte „Fremdschlüssel“ aus;
  • in [3] wird der einzige Fremdschlüssel bearbeitet;
  • in [4], auf der Registerkarte DDL, die Definition SQL der Fremdschlüsselbeschränkung;
  • In [5] bewirkt die Klausel ON DELETE CASCADE, dass beim Löschen eines Arztes auch die ihm zugeordneten Termine gelöscht werden.

Die Fremdschlüsselbeschränkungen der Tabelle [RVS] sind analog 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: Das Löschen eines Kunden führt auch hier zum Löschen der ihm zugeordneten Termine;
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: Das Löschen eines Zeitfensters löscht ebenfalls alle damit verbundenen Termine.

3.5.2. Hinzufügen von Elementen zum Persistenzkontext

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

Das Programm [Fill.cs] lautet wie folgt:


using RdvMedecins.Entites;
using RdvMedecins.Models;

namespace RdvMedecins_01
{
  class Fill
  {
    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // Die aktuelle Datenbank wird geleert
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // die Datenbank wird zurückgesetzt
        // die Kunden
        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);
        }
        // die Ärzte
        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);
        }
        // die Zeitfenster
        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);
        }
        // die Termine
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // Der Persistenzkontext wird gespeichert
        context.SaveChanges();
      }
    }
  }
}
  • Zeile 10: Der Persistenzkontext wird geöffnet;
  • Zeilen 13–20: Die Zeilen der Tabellen [CLIENTS] und [MEDECINS] werden in den Persistenzkontext gestellt und anschließend wieder daraus entfernt. Wir haben gerade gesehen, dass dies die Datenbank vollständig leert;
  • Zeilen 22–88: Dem Persistenzkontext werden Elemente hinzugefügt. Sie alle haben einen Primärschlüssel mit dem Wert „null“. Sie werden daher in die Datenbank eingefügt;
  • Zeile 90: Die am Kontext vorgenommenen Änderungen werden mit der Datenbank synchronisiert. Anschließend wird eine Reihe von Vorgängen SQL und DELETE durchgeführt, gefolgt von einer Reihe von Vorgängen SQL und INSERT;

Das Programm [Fill] wird zum neuen Startobjekt des Projekts [1] gemacht, anschließend wird dieses ausgeführt.

In [2] lässt sich feststellen, dass die Tabellen gefüllt wurden.

3.5.3. Anzeige des Datenbankinhalts

Wir werden nun den Inhalt der Datenbank mithilfe der Abfrage LINQ to Entity anzeigen. LINQ (Sprache INtegrated-Abfrage) wurde 2007 mit dem Framework .NET 3.5 eingeführt. Sie stellt eine Erweiterung der Sprachen .NET und c.a.d dar, ist in die Sprache integriert und ihre Syntax wird vom Compiler überprüft. Sie ermöglicht die Abfrage verschiedener Sammlungen mit einer Syntax, die Ähnlichkeiten mit der Datenbanksprache SQL (Structured Query Language) aufweist. Es gibt verschiedene Varianten von LINQ:

  • LINQ to Object, um im Speicher befindliche Sammlungen abzufragen;
  • LINQ to XML, zur Abfrage von XML;
  • LINQ zu Entity, um Datenbanken abzufragen;

Um zu funktionieren, stützt sich LINQ auf zahlreiche Erweiterungen der Sprachen .NET. Diese können auch außerhalb von LINQ verwendet werden. Wir werden sie hier nicht vorstellen, sondern lediglich zwei Literaturhinweise nennen, in denen der Leser eine ausführliche Beschreibung von LINQ findet:

  • „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.

Das erste habe ich gelesen und fand es ausgezeichnet. Das zweite habe ich nicht gelesen, aber von denselben Autoren „C# 3.0 in a nutshell“, das zeitgleich mit LINQ erschienen ist. Ich fand dieses Buch deutlich über dem Durchschnitt der Bücher, die ich normalerweise lese. Es scheint, dass die anderen Bücher dieser beiden Autoren auf dem gleichen Niveau sind. Wir werden außerdem LINQPad verwenden, ein von Joseph Albahari geschriebenes Lernwerkzeug für LINQ.

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


// ein Arzt
  public class Medecin
  {
    // Daten
    [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; }
    // die Zeitfenster des Arztes
    public ICollection<Creneau> Creneaux { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

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

    // Hilfsprogramm
    private string dump(byte[] timestamp){
      string str = "";
      foreach (byte b in timestamp)
      {
        str += b;
      }
      return str;
    }
  }
  • Zeilen 27–30: die Methode ToString der Klasse. Beachten Sie, dass sie die Sammlung aus Zeile 21 nicht anzeigt;
  • Zeilen 32–37: die Methode ShortIdentity, die dasselbe tut.

Um die Auswirkungen der beiden vorangegangenen Methoden zu beurteilen, müssen wir hier die Konzepte des „Lazy Loading“ und des „Eager Loading“ erläutern. Wir haben gesehen, dass eine Entität Abhängigkeiten zu einer anderen Entität haben kann. Diese sind zweierlei Art:

  • von einem zu mehreren, wie oben, wo ein Arzt mit mehreren Terminfenstern verknüpft ist;
  • von mehreren zu einem, wie in der Entität [Creneau] unten, wo mehrere Zeitfenster mit demselben Arzt verknüpft sind;

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

Wenn die Abhängigkeiten gleichzeitig mit den Entitäten geladen werden, an die sie gebunden sind, spricht man von „Eager Loading“. Andernfalls spricht man von „Lazy Loading“: Die Abhängigkeiten werden erst geladen, wenn sie zum ersten Mal referenziert werden. Standardmäßig verwendet EF 5 „Lazy Loading“: Die Abhängigkeiten werden nicht gleichzeitig mit der Entität geladen.

Sehen wir uns unsere oben genannte Methode [ToString] an:


    // Sprechzeiten des Arztes
    public ICollection<Creneau> Creneaux { get; set; }
    
    // Unterschrift
    public override string ToString()
    {
      return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // Kurzunterschrift
    public string ShortIdentity()
    {
      return ToString();
}

Die Methode [ToString] zeigt die Abhängigkeit [Creneaux] aus Zeile 2 nicht an. Hätte sie dies getan, hätte sie das Laden aller Termine des Arztes vor ihrer Ausführung erzwungen. Um dieses aufwendige Laden zu vermeiden, wurde die Abhängigkeit nicht in die Signatur der Entität aufgenommen. Im Allgemeinen werden wir in jede Entität zwei Signaturen aufnehmen:

  • eine Methode ToString, die die Entität und ihre eventuellen Abhängigkeiten im Many-to-One-Verhältnis anzeigt. Wie gerade erläutert, führt dies zum Laden der Abhängigkeit;
  • eine Methode ShortIdentity, die keine Abhängigkeiten referenziert. Es werden daher keine Abhängigkeiten geladen;

Die Anzeigemethoden der anderen Entitäten lauten wie folgt:

Die Entität [Client]:


  public class Client
  {
    // Daten
    ...
    // die Termine des Kunden
    public ICollection<Rv> Rvs { get; set; }
    
    // Unterschrift
    public override string ToString()
    {
      return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
    }
    // Kurzsignatur
    public string ShortIdentity()
    {
      return ToString();
    }

}
  • Zeilen 9–12: Die Methode [ToString] 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; }
    // die Termine des Zeitfensters
    public ICollection<Rv> Rvs { get; set; }
    
    // Unterschrift
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
    // Kurzsignatur
    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 Methode [ToString] verweist auf die Abhängigkeit in Zeile 9. Dadurch wird deren Laden erzwungen;
  • 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. Diese wird daher nicht geladen.

Die Entität [Rv]:


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

    // Signatur
    public override string ToString()
    {
      return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
    }
    // Kurzsignatur
    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. Dies führt dazu, dass diese geladen werden;
  • Zeilen 17–20: Die Methode [ShortIdentity] verhindert dies, sodass die Abhängigkeiten nicht geladen werden.

Zusammenfassend ist also auf die Methoden [ToString] der Entitäten zu achten. Wenn man dies nicht beachtet, kann das Anzeigen einer Tabelle die Hälfte der Datenbank laden, wenn die Tabelle zahlreiche 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)
    {
      // Datenbank-Dump
      using (var context = new RdvMedecinsContext())
      {
        // die Kunden
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // die Ärzte
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // Terminfenster
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // Termine
        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, die die Entitäten [Client] anzeigen. Die hier gegebene Erklärung gilt auch für die anderen Entitäten.


        // die Kunden
        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, die Angabe des genauen Typs einer Variablen zu vermeiden. Der Compiler leitet diesen dann aus dem Typ des Ausdrucks ab, der der Variablen zugewiesen wird;
  • Zeile 3: Der der Variablen clients zugewiesene Ausdruck ist eine Abfrage vom Typ „LINQ to Entity“. Darin sind Schlüsselwörter der Sprache SQL zu erkennen, die in LINQ übernommen wurden. Die hier verwendete Syntax lautet wie folgt:

from variable in DbSet select variable

Eine allgemeinere Syntax von LINQ 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. Solange dies nicht geschieht, ist die Variable [clients] lediglich eine noch 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 Methode [ToString] der Entität [Client] wird für die Anzeige verwendet. Es werden keine Abhängigkeiten geladen;

Kommen wir nun zu den folgenden Codezeilen:

  • Zeilen 24–28: Die Zeilen der Tabelle [MEDECINS] werden in den Persistenzkontext übernommen und angezeigt. Es werden keine Abhängigkeiten geladen;
  • Zeilen 31–35: Die Zeilen der Tabelle [CRENEAUX] werden in den Persistenzkontext übernommen und angezeigt. Wir haben gesehen, dass die Methode [ToString] dieser Entität die Abhängigkeit [Medecin] anzeigte. Diese ist jedoch bereits geladen. Es findet daher kein erneutes Laden statt;
  • Zeilen 38–42: Die Zeilen der Tabelle [RVS] werden in den Persistenzkontext übernommen und angezeigt. Wir haben gesehen, dass die Methode [ToString] dieser Entität die Abhängigkeiten [Client] und [Creneau] anzeigte. Diese sind jedoch bereits geladen. Es findet daher kein erneutes Laden statt.

Es ist zu beachten, dass die Reihenfolge der Anzeige nicht beliebig ist. Hätte man zunächst die Entitäten [Rv] anzeigen wollen, hätte die Methode [ToString] dieser Entität das Laden der mit diesen Terminen verknüpften Entitäten [Client] und [Creneau] ausgelöst. Die anderen wären nicht geladen worden. Sie wären erst später in einer anderen Ansicht geladen worden. Dies hat Auswirkungen auf die Leistung. Der vorstehende Code benötigt vier Befehle SQL, um alle Entitäten anzuzeigen. Nehmen wir nun an, dass zunächst die Tabelle [RVS] mit den Terminen abgefragt wird. Für die Tabelle [RVS] ist eine erste Abfrage SQL erforderlich. Anschließend führt die Methode [ToString] der Entität [Rv] dazu, dass gegebenenfalls die zugehörigen Entitäten [Client] und [Creneau] geladen werden. Für jede davon ist eine Abfrage SQL erforderlich. Angenommen, es gibt N2 Kunden und N3 Termine und alle diese Entitäten sind in der Tabelle [RVS] referenziert, erfordert die Anzeige dieser Tabelle 1 + N2 + N3 Abfragen vom Typ SQL. Somit ist die Leistung geringer als in der untersuchten Version. Um die Tabelle [RVS] mit ihren Abhängigkeiten anzuzeigen, wäre eine Tabellenverknüpfung erforderlich. Diese lässt sich mit LINQ realisieren. Wir werden anhand eines Beispiels darauf zurückkommen. Vorerst sollten wir uns vor Augen halten, dass wir die Abfragen SQL beachten müssen, die unserem Code LINQ 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 sieht wie folgt aus:

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. Einlernen von LINQ mit LINQPad

Wir haben oben die Abfragen LINQ to Entity verwendet, um den Inhalt der Datenbanktabellen anzuzeigen. Joseph Albahari hat ein Programm geschrieben, das die verschiedenen Formen von LINQ einführt. Wir stellen es nun vor.

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

Anfänger können sich mit den Beispielen auf den Registerkarten [Samples] und [2] vertraut machen, die zahlreiche Beispiele enthalten. Wählen wir das Beispiel [3] aus, das daraufhin in einem separaten Fenster [4] angezeigt wird. Der vollständige Code des Beispiels lautet wie folgt:


// Nun zu einer einfachen LINQ-to-Objects-Abfrage (beachten Sie das fehlende Semikolon):

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


// Sie können dies gerne bearbeiten... (niemand schaut zu!) Sie werden aufgefordert, alle
// Änderungen in einer separaten Datei zu speichern.
//
// Tipp:  Sie können einen Teil einer Abfrage ausführen, indem Sie ihn markieren und dann F5 drücken.

Die Zeilen 3–5 sind ein Beispiel für eine Abfrage vom Typ „LINQ to Object“. Die Abfrage LINQ folgt der folgenden Syntax:


from variable in collection orderby élément1 select élément2
  • Die Variable bezeichnet das aktuelle Element der Sammlung. In unserem Beispiel ist diese Sammlung die Liste der Wörter, die aus der aufgeteilten Zeichenkette resultieren;
  • die Sammlung ist gemäß dem Parameter élément1 von „orderby“ sortiert. In unserem Beispiel wird die Sammlung der Wörter nach ihrer Länge sortiert;
  • Das Schlüsselwort </span>**<span style="color: #000000">select *gibt an, was aus dem aktuellen Element </span>*<span style="color: #000000">variable der Sammlung extrahiert werden soll. In unserem Beispiel ist dies das Wort.

Führen wir diese Abfrage LINQ aus:

  • in [1]: Ein Ausdruck LINQ wird von [F5] oder über die Ausführungsschaltfläche ausgeführt;
  • in [2]: die Anzeige. Die Wörter werden in der Reihenfolge ihrer Länge angezeigt. Dieses einfache Beispiel verdeutlicht die Leistungsfähigkeit von LINQ;
  • in [3] können weitere Beispiele heruntergeladen werden, insbesondere diejenigen 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" };

// Wörter nach Länge gruppieren
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 };

// Jede Gruppe ausdrucken
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 Abfrage LINQ mit neuen Schlüsselwörtern;
  • Zeile 5: Die abgefragte Sammlung ist das Wortarray aus Zeile 1;
  • Zeile 6: Die Sammlung wird in alphabetischer Reihenfolge der Wörter sortiert;
  • Zeile 7: Die Sammlung wird nach dem Schlüsselwort „into“ in eine neue Sammlung lengthGroups gruppiert. lengthGroups.Key steht für den Gruppierungsfaktor (Schlüsselwort „by“), hier die Länge der Wörter. lengthGroups fasst die Wörter mit demselben Gruppierungsfaktor, also derselben Länge, zusammen;
  • Zeile 8: Die Sammlung lengthGroups ist nach dem Gruppierungsschlüssel absteigend sortiert, also hier nach absteigender Wortlänge;
  • Zeile 9: Aus dieser Sammlung werden neue Objekte (anonyme Klassen) mit zwei Feldern erzeugt:
    • Length: die Länge der Wörter,
    • „Words“: die Wörter mit dieser Länge;

Hier wird besonders deutlich, welchen Nutzen das Schlüsselwort „var“ in Zeile 4 hat. Da in Zeile 9 eine anonyme Klasse verwendet wurde, lässt sich der Typ der Variablen „groups“ nicht angeben. Der Compiler hingegen wird der anonymen Klasse einen internen Namen zuweisen und damit die Variable groups typisieren. Anschließend kann er feststellen, ob die Variable groups korrekt verwendet wird

  • Zeile 12: Durchlaufen der Abfrage aus Zeile 4. Erst zu diesem Zeitpunkt wird sie ausgewertet. Wir erinnern uns, dass ihre Ausführung eine Sammlung von Objekten erzeugt, die in Zeile 9 näher beschrieben werden;
  • Zeile 14: Die Eigenschaft Length des aktuellen Elements wird angezeigt, also die Wortlänge;
  • Zeilen 15–17: Es wird jedes Element der Sammlung der Eigenschaft Words ausgegeben, also die Menge aller Wörter mit der zuvor angezeigten Länge.

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

 

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 zur Datenbank „SQL Server“ her, die wir erstellt und mit Daten gefüllt haben:

  • In [1] fügen wir eine Datenbankverbindung hinzu;
  • in [2] die Zugriffsmethoden auf die Datenquelle. Um auf die Datenbank „SQL Server“ zuzugreifen, verwenden wir [LINQPad Driver];
  • in [3] ist es außerdem möglich, einen Persistenzkontext [DbContext] abzurufen, der in einer assembly-Datei im Format .exe oder .dll definiert ist (Option 3). Leider wird Entity Framework 5 zum jetzigen Zeitpunkt (8. Oktober 2012) noch nicht unterstützt;
  • in [4] ist es möglich, Treiber für andere SGBD als SQL Server herunterzuladen;
  • unter [5] wird der Treiber für die SGBD, MySQL und Oracle heruntergeladen;
  • in [6] wird der heruntergeladene Treiber;
  • in [7] stellen wir eine Verbindung zu einer SQL-Datenbank her;
  • in [8] befindet sich die Datenbank auf dem (lokalen) Namensserver;
  • in [9] stellen wir eine Verbindung mit der Authentifizierung „sa / sqlserver2012“ her;
  • in [10] zur Datenbank [rdvmedecins-ef], die wir erstellt haben;
  • In [11] kann die Verbindung getestet werden;
  • In [12] schließen wir den Assistenten ab;
  • in [13] erscheint die Verbindung in LINQPad.

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

  • In [1] repräsentiert [CLIENTS] die Gesamtheit der Entitäten in [Client]. Jede Entität verfügt über:
    • die Eigenschaften (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • eine 1:n-Beziehung zu [CLIENTRVS];
  • wobei [2] und [CRENEAUXes] die Gesamtheit der Entitäten [Creneau] darstellen. Jede Entität hat:
    • die Eigenschaften (ID, HDEBUT, MDEBUT, HFIN, MFIN, MEDECIN_ID, TIMESTAMP),
    • eine 1:n-Beziehung [CRENEAURVS],
  • eine „Viele-zu-1“-Beziehung [MEDECIN];
  • In [3] repräsentiert die Entität [MEDECINS] die Gesamtheit der Entitäten [Medecin]. Jede Entität hat:
    • die Eigenschaften (ID, TITRE, NOM, PRENOM, TIMESTAMP),
    • eine 1:n-Beziehung zu [MEDECINCRENEAUXes];
  • In [4] repräsentiert die Entität [RVS] die Gesamtheit der Entitäten [Rv]. Jede Entität hat:
    • die Eigenschaften (ID, JOUR, CLIET_ID, CRENEAU_ID, TIMESTAMP),
    • eine Mehr-zu-1-Beziehung zu [CLIENT],
    • eine Mehr-zu-1-Beziehung [CRENEAU].

Beachten Sie, dass sich die Namen der oben genannten Eigenschaften von den bisher verwendeten Namen unterscheiden. Das spielt jedoch keine Rolle. Wir möchten lediglich die Grundprinzipien der Datenbankabfragen erlernen.

Schauen wir uns an, wie wir diese Entitätsdatenbank abfragen können. Beispielsweise möchten wir die Liste der Ärzte, sortiert nach ihrem TITRE und NOM:

  • In [1] erstellen wir eine neue Abfrage;
  • in [2] den Abfragetext;
  • in [3] das Ergebnis der Abfrage;
  • in [4] die gleiche Abfrage mit Lambda-Ausdrücken. Eine Abfrage mit Lambda-Ausdrücken ist weniger gut lesbar als eine Textabfrage, und man könnte darauf verzichten wollen. Sie sind jedoch manchmal unverzichtbar, da sie bestimmte Dinge ermöglichen, die Textabfragen nicht zulassen. Ein Lambda-Ausdruck bezeichnet eine Funktion mit einem Eingabeparameter a und einem Ausgabeparameter b in der Form a=>b. Die oben genannte Methode OrderBy akzeptiert eine Lambda-Funktion als einzigen Parameter. Diese liefert ihr den Parameter, nach dem eine Sammlung sortiert werden soll. Somit ist MEDECINS.OrderBy(m=>m.TITRE) die nach Titeln sortierte Liste der Ärzte. Die Anweisung ist als Pipeline über eine Sammlung zu verstehen. Die Sammlung der Ärzte wird als Eingabe an die Methode OrderBy übergeben. Diese verarbeitet die Entitäten [Medecin] nacheinander. Im Lambda-Ausdruck m=>m.TITRE steht m für die Eingabe der Lambda-Funktion. Man kann sie beliebig benennen. Hier ist die Eingabe der Lambda-Funktion eine Entität [Medecin]. Die Funktion m=>m.TITRE lässt sich wie folgt interpretieren: Wenn ich m als meine Eingabe bezeichne (eine Entität [Medecin]), dann ist meine Ausgabe m.TITRE, also der Name des Arztes. MEDECINS.OrderBy(m=>m.TITRE) ist wiederum eine Sammlung, nämlich die nach Titeln geordnete Sammlung der Ärzte. Diese neue Sammlung kann als Eingabe für eine andere Methode dienen, im Beispiel die Methode ThenBy. Diese funktioniert nach dem gleichen Prinzip. Sie dient dazu, 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], der Befehl SQL, der an die Datenbank gesendet wird. Auch hier sollten wir diesen Code aufmerksam lesen. Er ermöglicht es, die tatsächlichen Kosten einer Abfrage LINQ zu bewerten.

Im Folgenden stellen wir einige Beispiele für Abfragen vom Typ LINQ vor. Jedes Mal zeigen wir die angezeigten Ergebnisse sowie die entsprechenden Lambda- und SQL-Codes. Um diese Abfragen zu verstehen, muss man sich die „Viele-zu-Eins“-Beziehungen ins Gedächtnis rufen, die die Entitäten miteinander verbinden. Über sie navigiert man von einer Entität zur anderen. Man bezeichnet sie als navigatorische Eigenschaften.

// Kunden mit dem Titel „Herr“, sortiert nach absteigender Reihenfolge der Namen

Ergebnisse:

 
LINQ

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

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

// alle Termine mit dem zugehörigen Arzt

(Teil-)Ergebnisse:

 
LINQ

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

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

// alle Termine mit dem zugehörigen Patienten und Arzt

Ergebnisse:

 
LINQ

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

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

// Ärzte ohne Termine

Ergebnisse:

 
LINQ
 
Lambda
SQL

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

Für diese Anfrage gibt es keine Abfrage LINQ. Man muss Lambda-Ausdrücke verwenden. Dieser lautet wie folgt: Ich nehme die Sammlung der Ärzte (MEDECINS) und behalte (Where) nur die Ärzte (m) bei, für die ich in der Sammlung der Termine (RVS) keinen Termin (rv) mit diesem Arzt (m) finden kann.

// Zeitfenster von Frau Pélissier

(Teil-)Ergebnisse:

 
LINQ

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

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

// Anzahl der Termine von Frau Pélissier am 08.10.2012

Ergebnisse:

 
LINQ

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

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

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

Ergebnisse:

 
LINQ

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

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

// Anzahl der Zeitfenster pro Arzt

Ergebnisse:

 
LINQ

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

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

3.5.5. Änderung einer mit dem Persistenzkontext verknüpften Entität

Wir haben die folgenden Operationen am Persistenzkontext betrachtet:

  • Hinzufügen eines Elements zum Kontext ([dbContext].[DbSet].Add);
  • ein Element aus dem Kontext entfernen ([dbContext].[DbSet].Remove);
  • einen Kontext mit Abfragen abfragen: LINQ.

Wenn man den Kontext mit der Datenbank synchronisieren möchte, schreibt man [dbContext].SaveChanges().

Der Code [ModifyAttachedEntity] veranschaulicht die Änderung einer dem Kontext zugeordneten Entität:


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;
      // 1. Kontext
      using (var context = new RdvMedecinsContext())
      {
        // Die aktuelle Datenbank wird geleert
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // einen Kunden hinzufügen
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // Weiter
        Console.WriteLine("client1--avant");
        Console.WriteLine(client1);
        // Kontext speichern
        context.SaveChanges();
        // Weiterverfolgung
        Console.WriteLine("client1--après");
        Console.WriteLine(client1);
      }
      // 2. Kontext
      using (var context = new RdvMedecinsContext())
      {
        // Kunde1 wird in Kunde2 übernommen
        client2 = context.Clients.Find(client1.Id);
        // Weiterverfolgung
        Console.WriteLine("client2");
        Console.WriteLine(client2);
        // Kunde2 wird geändert
        client2.Nom = "yy";
        // Kontext speichern
        context.SaveChanges();
      }
      // 3. Kontext
      using (var context = new RdvMedecinsContext())
      {
        // Kunde2 wird in Kunde3 übernommen
        client3 = context.Clients.Find(client2.Id);
        // Weiter
        Console.WriteLine("client3");
        Console.WriteLine(client3);
      }
    }
  }
}
  • Zeile 15: Öffnen des Anwendungskontexts;
  • Zeilen 18–25: Der Kontext wird geleert. Genauer gesagt werden alle Entitäten aus der Datenbank in den Kontext geladen und anschließend in den Status „gelöscht“ versetzt. Es ist zu beachten, dass sich die Datenbank zu diesem Zeitpunkt noch nicht verändert hat. Solange der Kontext nicht mit der Datenbank synchronisiert ist, bleibt diese unverändert. Wir erinnern uns, dass das Löschen der Entitäten [Medecin] und [Client] ausreicht, um die Datenbank durch kaskadierende Löschvorgänge zu leeren;
  • Zeilen 27–28: Ein neuer Kunde wird zur Datenbank hinzugefügt;
  • Zeilen 30–31: Er wird vor dem Speichern in der Datenbank angezeigt;
  • Zeile 33: Der Kontext wird mit der Datenbank synchronisiert. Die als „gelöscht“ markierten Entitäten werden Gegenstand einer Operation SQL DELETE sein, die hinzugefügte Entität einer Operation SQL INSERT;
  • Zeilen 35–36: Der Kunde wird nach der Synchronisierung mit der Datenbank angezeigt;

Das Ergebnis in der Konsole lautet wie folgt:

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

Folgende Punkte sind zu beachten:

  • Vor der Synchronisation mit der Datenbank hat der Kunde weder einen Primärschlüssel noch timestamp,
  • Nach der Synchronisierung verfügt er über beides. An dieser Stelle sei daran erinnert, dass der Primärschlüssel so konfiguriert wurde, dass er vom Server generiert wird. Ebenso generiert dieser automatisch den Zeitstempel;
  • Zeile 37: Der Persistenzkontext wird geschlossen. Die darin enthaltenen Entitäten werden „abgekoppelt“. 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 direkt über seinen Primärschlüssel aus der Datenbank abgerufen. Er wird dann in den Kontext übernommen. Wird er nicht gefunden, gibt die Methode „Find“ den Zeiger null zurück;
  • Zeilen 48–49: Er wird angezeigt;

Dies führt zu folgendem Ergebnis:

client2
Client[16,xx,xx,xx,000000132209]
  • Zeile 47: Er wird geändert;
  • Zeile 49: Der Kontext wird mit der Datenbank synchronisiert. EF erkennt, dass bestimmte Elemente des Kontexts geändert wurden, seit sie in den Kontext geladen wurden. Für diese Elemente generiert es die Befehle SQL und UPDATE in der Datenbank. Die Synchronisierung besteht hier also aus einem einzigen Befehl: UPDATE;
  • Zeile 50: Der zweite Kontext wird geschlossen. Die Entität client2, die dem Kontext zugeordnet war, wird nun von diesem getrennt;
  • Zeile 52: Es wird ein dritter leerer Kontext geöffnet;
  • Zeile 55: Der einzige Client der Datenbank wird erneut dorthin verschoben. Wir wollen prüfen, ob die im vorherigen Kontext an ihm vorgenommene Änderung in die Datenbank übernommen wurde;
  • Zeilen 57–58: Der Kunde wird angezeigt. Dies führt zu folgendem Ergebnis:
client3
Client[16,xx,xx,yy,000000132210]

Der Name des Kunden wurde in der Datenbank tatsächlich geändert. Interessanterweise wurde auch sein timestamp aktualisiert.

  • Zeile 59: Der Kontext wird geschlossen. Nebenbei sei angemerkt, dass im Gegensatz zu den beiden vorherigen Fällen zuvor keine Synchronisierung des Kontexts mit der Datenbank (SaveChanges) erforderlich war, da der Kontext nicht geändert worden war.

3.5.6. Verwaltung von losgelösten Entitäten

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

Die Schicht [DAO] nutzt die Schichten ORM und EF5, um auf die Daten zuzugreifen. Wir haben die Grundbausteine dieser Schicht. Jede Methode öffnet einen Persistenzkontext, führt darin die erforderlichen Operationen (Einfügen, Ändern, Löschen, Abfragen) durch und schließt ihn anschließend wieder. Die von der Schicht [DAO] verwalteten Entitäten werden bis zur Webschicht ASP.NET weitergeleitet. In dieser Schicht befinden sie sich außerhalb des Persistenzkontexts und sind somit losgelöst. In der Webschicht kann ein Benutzer diese Entitäten bearbeiten (Hinzufügen, Ändern, Löschen). Wenn sie zur Schicht [DAO] zurückkehren, sind sie weiterhin losgelöst. Die Schicht [DAO] muss jedoch die vom Benutzer vorgenommenen Änderungen in die Datenbank übernehmen. Sie muss daher mit losgelösten Entitäten arbeiten. Betrachten wir die drei möglichen Fälle:

Hinzufügen eines losgelösten Entitätsobjekts

Dies ist der normale Fall bei einem Hinzufügen. Es reicht aus, das losgelöste Objekt zum Kontext hinzuzufügen (Add), wobei sicherzustellen ist, dass es einen Primärschlüssel hat, der gleich null ist.

Eine losgelöste Entität bearbeiten

Man kann den folgenden Code verwenden:

[DbContext].Entry(entité-détachée).State=EntityState.Modified ;
  • Die Methode [DbContext].Entry(freistehende-Entität) fügt die Entität in den Kontext ein;
  • Der Status dieser Entität wird auf „geändert“ gesetzt, damit sie Gegenstand eines Befehls SQL UPDATE ist.

Eine losgelöste Entität löschen

Man kann den folgenden Code verwenden:

Entity e=[DbContext].[DbSet].Find(clé primaire de l'entité détachée) ;
[DbContext].[DbSet].Remove(e) ;
  • Zeile 1: Die Entität mit demselben Primärschlüssel wie die abgelöste Entität wird in den Kontext aufgenommen;
  • Zeile 2: Man löscht sie:

Es ist zu beachten, dass hierfür zunächst ein SELECT und anschließend ein DELETE erforderlich ist, während normalerweise der DELETE allein ausreicht. Man kann auch dem Beispiel der Änderung einer losgelösten Entität folgen und Folgendes schreiben:

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

Da ich keine Protokolle zu den in der Datenbank durchgeführten SQL-Vorgängen erstellen konnte, weiß ich nicht, ob eine Methode der anderen vorzuziehen ist.

Hier ein Beispiel:

Der Programmcode für [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;

      // die aktuelle Datenbank wird geleert
      Erase();
      // Ein Kunde wird hinzugefügt
      using (var context = new RdvMedecinsContext())
      {
        // Kundenanlage
        client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
        // Hinzufügen des Kunden zum Kontext
        context.Clients.Add(client1);
        // Kontext speichern
        context.SaveChanges();
      }
      // Datenbank anzeigen
      Dump("1-----------------------------");
      // Kunde1 ist nicht im Kontext enthalten – wird geändert
      client1.Nom = "y";
      // neuer Kontext
      using (var context = new RdvMedecinsContext())
      {
        // Hier haben wir einen leeren Kontext
        // Kunde1 wird in geänderten Zustand in den Kontext aufgenommen
        context.Entry(client1).State = EntityState.Modified;
        // Der Kontext wird gesichert
        context.SaveChanges();
      }
      // Basisansicht
      Dump("2-----------------------------");
      // Entität außerhalb des Kontexts löschen
      using (var context = new RdvMedecinsContext())
      {
        // Hier haben wir einen neuen leeren Kontext
        // „client1“ wird im gelöschten Status in den Kontext aufgenommen
        context.Entry(client1).State = EntityState.Deleted;
        // Der Kontext wird gespeichert
        context.SaveChanges();
      }
      // Datenbank anzeigen
      Dump("3-----------------------------");
    }

    static void Erase()
    {
      // Datenbank leeren
      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);
        }
        // Der Kontext wird gesichert
        context.SaveChanges();
      }
    }

    static void Dump(string str)
    {
      Console.WriteLine(str);
      // Datenbank anzeigen
      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. Es sind daher keine Entitäten mehr angehängt. Die Entität client1 wechselt in den Status „abgekoppelt“;
  • Zeile 29: Der Name der losgelösten Entität wird geändert;
  • Zeile 31: Es wird ein neuer leerer Kontext geöffnet;
  • Zeile 35: Die losgelöste Entität client1 wird im Kontext in den Status „geändert“ versetzt;
  • 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 in der Datenbank erfolgreich geändert. Es ist zu beachten, dass der Eintrag „timestamp“ aktualisiert wurde;

  • Zeile 42: Eröffnung eines neuen leeren Kontexts;
  • Zeile 46: Die abgelöste Entität client1 wird im Kontext mit dem 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 in der Datenbank erfolgreich gelöscht.

Nun sehen wir die beiden Modi zum Laden der Abhängigkeiten eines Entity: Lazy und Eager Loading.

3.5.7. Lazy und Eager Loading

Nehmen wir noch einmal das Schema der „Viele-zu-Eins“-Beziehungen unserer vier Entitäten zur Hand:

Oben hat die Entität [Creneau] eine navigationsbezogene Eigenschaft [Creneau.Medecin] zur Entität [Medecin]. Dies wird als Abhängigkeit bezeichnet. Wir haben gesehen, dass es auch „Eins-zu-Viele“-Abhängigkeiten gibt. Das nun 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. Das ist eine sinnvolle Maßnahme. Wäre dies nicht der Fall, würde das Einlesen der Termine in den Kontext gemäß den oben genannten Abhängigkeiten Folgendes nach sich ziehen:

  • die mit den Terminen verknüpften Entitäten [Creneau];
  • die mit diesen Terminfenstern verknüpften Entitäten [Medecin];
  • die mit den Terminen verknüpften Entitäten [Clients].

Manchmal benötigt man 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
  {
    // die Entitäten
    static Medecin[] medecins;
    static Client[] clients;
    static Creneau[] creneaux;

    static void Main(string[] args)
    {
      // Datenbank initialisieren      
      InitBase();
      Console.WriteLine("Initialisation terminée");
      // Eager Loading
      Creneau creneau;
      int idCreneau = (int)creneaux[0].Id;
      using (var context = new RdvMedecinsContext())
      {
        // Zeitfenster Nr. 0
        creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // Anzeige der Abhängigkeiten
      try
      {
        Console.WriteLine("Médecin={0}", creneau.Medecin);
      }
      catch (Exception e)
      {
        Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
      }
      // Lazy Loading – Standardmodus
      using (var context = new RdvMedecinsContext())
      {
        // Zeitfenster Nr. 0
        creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
        Console.WriteLine(creneau.ShortIdentity());
      }
      // Abhängigkeitsanzeige
      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()
    {
      // Datenbank wird initialisiert
      using (var context = new RdvMedecinsContext())
      {
        // Die aktuelle Datenbank wird geleert
        ...
        // Die Datenbank wird initialisiert
        // die Kunden
        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" }
     };
...
        // die Termine
        context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
        // Der Persistenzkontext wird gespeichert
        context.SaveChanges();
      }
    }
  }
}
  • Zeile 18: Wir gehen von einer bekannten Basis aus, die bisher verwendet wurde. Nach diesem Vorgang werden die Arrays in den Zeilen 11–13 mit losgelösten Entitäten gefüllt;
  • Zeilen 21–22: Man befasst sich mit dem ersten Zeitfenster und dem zugehörigen Arzt;
  • Zeile 23: neuer Kontext;
  • Zeile 26: Der Termin wird mit seiner Abhängigkeit in den Kontext eingebunden (Eager Loading). Da dies nicht der Standardmodus ist, muss diese Abhängigkeit explizit angefordert werden. Dies wird durch die Methode Include ermöglicht. Ihr Parameter ist der Name der Abhängigkeit in der Entität, die in den Kontext eingebunden wird. Die Abfrage, die die Entität in den Kontext holt, verwendet Lambda-Ausdrücke. Mit der Methode „Single“ lässt sich eine Bedingung festlegen, um eine einzelne Entität abzurufen. Hier wird in der Datenbank nach der Entität „[Creneau]“ gesucht, die den Primärschlüssel des Zeitfensters Nr. 0 hat;
  • Zeile 27: Die zurückgegebene Entität wird angezeigt. Zur Erinnerung: Die beiden in Entitäten verwendeten Schreibmethoden lauten:

// Signatur
    public override string ToString()
    {
      return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
    }
 
   // Kurzsignatur
    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 Methode [ToString] zeigt die Abhängigkeit [Medecin] an. Befindet sich diese noch nicht im Kontext, wird sie in der Datenbank gesucht und dort hinzugefügt;
  • Zeilen 8–11: Die Methode [ShortIdentity] zeigt die Abhängigkeit [Medecin] nicht an. Sie wird daher nicht in der Datenbank gesucht, 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: Es wird versucht, die Abhängigkeit [Medecin] der Entität zu schreiben. Zur Erinnerung: Beim Lazy Loading wird eine Abhängigkeit bei ihrer ersten Verwendung geladen, sofern sie nicht vorhanden ist. Hier ist sie normalerweise vorhanden. Die Ausgabe sieht wie folgt aus:
Médecin=Medecin[21,Mme,Marie,Pelissier,00000195149]
  • Zeilen 39–44: Im Rahmen eines neuen Kontexts wird das Zeitfenster Nr. 0 erneut in der Datenbank gesucht und in den Kontext übernommen. Hier wird die Abhängigkeit [Medecin] nicht explizit angefordert. Sie wird daher nicht übernommen (Lazy Loading);
  • Zeile 43: Die Kurzbezeichnung des Zeitfensters 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. Verwendet man ToString, wird die Abhängigkeit [Medecin] angezeigt und dafür in der Datenbank gesucht. Das ist jedoch nicht gewünscht.

  • Zeile 44: Der Kontext wird geschlossen;
  • Zeilen 46–53: Es wird versucht, die Abhängigkeit der Entität anzuzeigen. Es ist wichtig, dies außerhalb des Kontexts zu tun, da sie sonst in der Datenbank gesucht und gefunden wird. Hier befinden wir uns außerhalb des Kontexts. Die Entität [Creneau] ist losgelöst und ihre Abhängigkeit [Medecin] fehlt (Lazy Loading). Was wird nun 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:Zeile 48

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

Betrachten wir nun die Parallelität beim Zugriff auf Entitäten.

3.5.8. Zugriffskonkurrenz bei Entitäten

Kehren wir zur Definition der Entität [Client] zurück:


public class Client
  {
    // Daten
    [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; }
    // die Rvs des Kunden
    public ICollection<Rv> Rvs { get; set; }
    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

    // Unterschrift
    ...
  }

Wir werden uns das Feld [Timestamp] in Zeile 23 ansehen. Wir wissen, dass sein Wert durch SGBD generiert wird. Wir haben außerdem erwähnt, dass die Annotation [Timestamp] in Zeile 22 bewirkt, dass EF 5 das annotierte Feld nutzt, um Zugriffskonflikte bei den Entitäten zu verwalten. Erinnern wir uns daran, was die Verwaltung von Zugriffskonflikten ist:

  • Ein Prozess P1 liest zum Zeitpunkt T1 eine Zeile L aus der Tabelle [MEDECINS]. Die Zeile hat die timestamp TS1;
  • ein Prozess mit der ID P2 liest zum Zeitpunkt T2 dieselbe Zeile L aus der Tabelle [MEDECINS]. Die Zeile hat die Werte timestamp und TS1, da der Prozess P1 seine Änderung noch nicht bestätigt hat;
  • der Prozess P1 validiert seine Änderung an der Zeile L. Der Status timestamp der Zeile L wechselt daraufhin zu TS2;
  • Der Prozess P2 bestätigt seine Änderung an Zeile L. DerORM löst daraufhin eine Ausnahme aus, da der Prozess P2 einen timestamp TS1 für die Zeile L aufweist, der sich von dem in der Datenbank gefundenen timestamp TS2 in der Datenbank gefunden wurde.

Dies wird als optimistische Verwaltung gleichzeitiger Zugriffe bezeichnet. Bei EF 5 muss ein Feld, das diese Rolle spielt, eines der beiden Attribute [Timestamp] oder [ConcurrencyCheck] aufweisen. SQL-Server hat einen Typ [timestamp]. Bei einer Spalte dieses Typs wird der Wert bei jedem Einfügen oder Ändern einer Zeile automatisch vom SQL-Server generiert. Eine solche Spalte kann dann zur Verwaltung von konkurrierenden Zugriffen verwendet werden.

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

Der Programmcode von [AccèsConcurrents] lautet wie folgt:


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

namespace RdvMedecins_01
{

  // mit den Threads ausgetauschtes Objekt
  class Data
  {
    public int Duree { get; set; }
    public string Nom { get; set; }
    public Client Client { get; set; }
  }

  // Testprogramm
  class AccèsConcurrents
  {

    static void Main(string[] args)
    {
      Client client1;
      using (var context = new RdvMedecinsContext())
      {
        // Hauptthread
        Thread.CurrentThread.Name = "main";
        // Die aktuelle Datenbank wird geleert
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        // Ein Kunde wird hinzugefügt
        client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
        context.Clients.Add(client1);
        // Weiterverfolgung
        Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
        // Speichern
        context.SaveChanges();
        // Weiter
        Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
        Console.WriteLine(client1.ShortIdentity());
      }
      // Kunde1 wird mit zwei Threads bearbeitet
      // Thread t1
      Thread t1 = new Thread(Modifie);
      t1.Name = "t1";
      t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
      // Thread t2
      Thread t2 = new Thread(Modifie);
      t2.Name = "t2";
      t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
      // Wir warten auf das Ende der beiden 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);
      // Wir zeigen die Änderung an – nur eine davon kann erfolgreich gewesen sein
      using (var context = new RdvMedecinsContext())
      {
        // client1 wird in client2 übernommen
        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: Es wird ein leerer Kontext gestartet;
  • Zeile 29: Dem aktuellen Thread wird ein Name zugewiesen, um ihn von den beiden später erstellten Threads zu unterscheiden;
  • Zeilen 31–38: Die Entitäten [Medecin] und [Client] werden in den Status „gelöscht“ versetzt;
  • Zeilen 40–41: Ein Client wird in den Kontext aufgenommen;
  • Zeilen 43–44: Er wird vor der Synchronisierung des Kontexts angezeigt;
  • Zeile 46: Synchronisierung des Kontexts mit der Datenbank: Die Entitäten im Status „gelöscht“ werden aus der Datenbank gelöscht. Die in den Kontext aufgenommene Entität [Client] wird in die Datenbank eingefügt. Dies wird das einzige Element in der Datenbank sein;
  • Zeilen 47–49: Der Client wird nach der Synchronisierung des Kontexts angezeigt. Zu diesem Zeitpunkt sehen die Bildschirmanzeigen 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]

Es ist zu beachten, dass der Kunde nach der Synchronisierung des Kontexts über einen Primärschlüssel und einen timestamp verfügt;

  • Zeile 50: Der Kontext wird geschlossen;
  • Zeile 53: Ein Thread t1 wird der Methode [Modifie] in Zeile 84 zugeordnet. Das bedeutet, dass er bei seiner Ausführung die Methode [Modifie] ausführt;
  • Zeile 54: Dem Thread t1 wird ein Name zugewiesen;
  • Zeile 55: Der Thread t1 wird gestartet. Ihm werden Parameter in Form einer Struktur [Data] übergeben, die in den Zeilen 12–17 definiert ist:
    • Dauer: Der Thread wird Durée Sekunden vor Beendigung seiner Ausführung angehalten,
    • Client: eine Referenz auf den Client, der in der Datenbank aktualisiert werden soll,
    • Name: Name, der diesem Kunden zugewiesen werden soll;
  • Zeilen 57–59: dasselbe mit einem zweiten Thread. Letztendlich versuchen zwei Threads, den Namen desselben Kunden in der Datenbank zu ändern;
  • Zeilen 60–63: Nachdem die beiden Threads gestartet wurden, wartet der Haupt-Thread auf deren Beendigung;
  • Zeile 62: Warten auf das Ende des Threads t1;
  • Zeile 63: Warten auf das Ende von Thread t2;
  • Zeile 64: Es ist nicht bekannt, in welcher Reihenfolge die beiden Threads beendet werden. Sicher ist jedoch, dass sie in Zeile 64 beendet sind;
  • Zeilen 66–72: In einem neuen Kontext wird der Client aus der Datenbank abgerufen, um seinen Status zu überprüfen.

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


static void Modifie(object infos)
    {
      // Der Parameter wird abgerufen
      Data data = (Data)infos;
      try
      {
        using (var context = new RdvMedecinsContext())
        {
          Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
          // client1 wird in client2 abgerufen
          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());
          // „client2“ wird geändert
          client2.Nom = data.Nom;
          // man wartet ein wenig
          Thread.Sleep(data.Duree);
          // die Änderungen werden gespeichert
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        // Ausnahme
        Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
      }
      // Ende des Threads
      Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
    }
  • Zeile 4: Die Parameter des Threads (Dauer, Name, Kunde) werden abgerufen;
  • Zeile 7: neuer Kontext;
  • Zeile 11: Der Client wird in den Kontext übernommen;
  • Zeilen 12–13: Abfrage, um den Status des Kunden anzuzeigen;
  • Zeile 15: Der Name wird geändert;
  • Zeile 17: Der Thread wird für Duree Millisekunden angehalten. Dies hat einen interessanten Effekt. Der Thread gibt den Prozessor frei, auf dem er ausgeführt wurde, und macht Platz für einen anderen Thread. In unserem Beispiel haben wir drei Threads: main, t1 und t2. Der Thread main ist angehalten und wartet auf das Ende der Threads t1 und t2. Angenommen, der Thread t1 hat als Erster den Prozessor, überlässt er diesen nun dem Thread t2. Dies hat zur Folge, dass der Thread t2 genau dasselbe liest wie der Thread t1, nämlich denselben Kunden mit demselben timestamp;
  • Zeile 19: Der Kontext wird mit der Datenbank synchronisiert. Nehmen wir erneut an, dass der Thread t1 als Erster wieder aktiv wird. Er wird den Client mit dem Namen „yy“ speichern. Dies ist möglich, da er denselben timestamp wie in der Datenbank hat. Aufgrund dieser Aktualisierung wird die SGBD die timestamp ändern. Wenn der Thread t2 seinerseits wieder aktiv wird, hat er einen Kunden mit einer timestamp, die sich von der derzeit in der Datenbank befindlichen unterscheidet. Seine Aktualisierung wird abgelehnt.

Die Bildschirmausgaben lauten wie folgt:

main client1--vor dem Speichern des Kontexts
Client[,xx,xx,xx,]
main client1--nach dem Speichern des Kontexts
Client[33,xx,xx,xx,000001126209]
Thread main -- Warten auf das Ende beider Threads beginnt
Début Thread t1
Début Thread t2
Thread t2 client2
Thread t2 Client[33,xx,xx,xx,000001126209]
Thread t1 client2
Thread t1 Client[33,xx,xx,xx,000001126209]
Fin Thread t2
Thread t1 System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: Une instruction de mise à jour, d'insertion ou de suppression dans le magasin a affecté un nombre inattendu de lignes (0). Des entités ont peut-être é modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager. ---> System.Data.OptimisticConcurrencyException: Eine Aktualisierungs-, Einfüge- oder Löschanweisung im Speicher hat eine unerwartete Anzahl von Zeilen (0) betroffen. Möglicherweise wurden Entitäten seit ihrer Erstellung geändert oder gelöscht
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 -- Warten beendet, beide Threads beendet
Thread main client2
Thread main Client[33,xx,xx,zz,000001126210]
  • Zeile 4: der Client in der Datenbank;
  • Zeile 9: der Kunde, wie er vom Thread t2 gelesen wird;
  • Zeile 11: der Kunde, wie er vom Thread t1 gelesen wird. Beide Threads haben also dasselbe gelesen;
  • Zeile 12: Der Thread t2 endet als Erster. Er konnte also seine Aktualisierung vornehmen. Der Name muss nun „zz“ lauten;
  • Zeile 13: Der Thread t1 löst eine Ausnahme vom Typ [System.Data.OptimisticConcurrencyException] aus. EF hat festgestellt, dass er nicht den richtigen timestamp hatte;
  • Zeile 21: Der Thread t1 wird seinerseits beendet;
  • Zeile 22: Der Haupt-Thread hat das Warten beendet;
  • Zeile 24: Der Haupt-Thread zeigt den Kunden aus der Datenbank an. Es war tatsächlich der Thread t2, der gewonnen hat. Der Name lautet „zz“. Man beachte, dass sich der timestamp geändert hat.

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

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 gleichzeitig zwei Termine für denselben Arzt, denselben Tag und denselben Zeitblock hinzu. Mal sehen, was passiert.

Das Projekt entwickelt sich wie folgt:

Der Programmcode [SynchronisationTransaction] lautet wie folgt:


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

namespace RdvMedecins_01
{

  // Testprogramm
  class SynchronisationTransaction
  {

    static void Main(string[] args)
    {
      using (var context = new RdvMedecinsContext())
      {
        // die aktuelle Datenbank wird geleert
        foreach (var client in context.Clients)
        {
          context.Clients.Remove(client);
        }
        foreach (var medecin in context.Medecins)
        {
          context.Medecins.Remove(medecin);
        }
        context.SaveChanges();
      }

      // Ein Kunde wird angelegt
      Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // Ein Arzt wird angelegt
      Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
      // Ein Terminfenster für diesen Arzt wird angelegt
      Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
      // Es werden zwei Termine für diesen Arzt und diesen Kunden angelegt, am selben Tag, im selben Zeitfenster
      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
      {
        // Man speichert alle diese Daten im Persistenzkontext
        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);
          // Der Kontext wird gespeichert – es muss eine Ausnahme auftreten
          // da die zugrunde liegende Tabelle BD eine Eindeutigkeitsbeschränkung aufweist, die verhindert,
          //, dass am selben Tag und im selben Zeitfenster zwei RDV vorhanden sind
          context.SaveChanges();
        }
      }
      catch (Exception e)
      {
        Console.WriteLine("Erreur : {0}", e);
      }
      // Wenn die Speicherung in einer Transaktion erfolgt, darf nichts in die Datenbank eingefügt worden sein
      // aufgrund der vorherigen Ausnahme – dies wird überprüft

      using (var context = new RdvMedecinsContext())
      {
        // die Kunden
        Console.WriteLine("Clients--------------------------------------");
        var clients = from client in context.Clients select client;
        foreach (Client client in clients)
        {
          Console.WriteLine(client);
        }
        // die Ärzte
        Console.WriteLine("Médecins--------------------------------------");
        var medecins = from medecin in context.Medecins select medecin;
        foreach (Medecin medecin in medecins)
        {
          Console.WriteLine(medecin);
        }
        // die Zeitfenster
        Console.WriteLine("Créneaux horaires--------------------------------------");
        var creneaux = from creneau in context.Creneaux select creneau;
        foreach (Creneau creneau in creneaux)
        {
          Console.WriteLine(creneau);
        }
        // die Termine
        Console.WriteLine("Rendez-vous--------------------------------------");
        var rvs = from rv in context.Rvs select rv;
        foreach (Rv rv in rvs)
        {
          Console.WriteLine(rv);
        }
      }
    }
  }
}
  • Zeilen 15–27: Es wird ein Persistenzkontext verwendet, um die Datenbank zu leeren;
  • Zeile 30: Erstellung eines Objekts [Client];
  • Zeile 32: Erstellung eines Objekts [Medecin];
  • Zeile 34: Erstellung eines Objekts [Creneau];
  • Zeile 36: Erstellung eines Objekts [Rv];
  • Zeile 37: Erstellung eines zweiten Objekts [Rv], identisch mit dem vorherigen;
  • Zeile 41: Öffnen eines neuen Kontexts;
  • Zeilen 43–47: Die zuvor angelegten Objekte werden dem neuen Kontext zugeordnet. Beachten Sie hierbei, dass wir unter Berücksichtigung der Abhängigkeiten die Anzahl der Operationen Add hätten minimieren können. EF optimiert jedoch die an die Datenbank zu sendenden Befehle SQL und INSERT;
  • Zeile 51: Der Kontext wird mit der Datenbank synchronisiert. Wie der Kommentar angibt, muss das Einfügen eines der beiden Termine aufgrund der Eindeutigkeitsbeschränkung in der Tabelle [RVS] fehlschlagen. Darüber hinaus gilt: Wenn die Synchronisation innerhalb einer Transaktion stattfindet, muss alles rückgängig gemacht werden. Daher darf kein Einfügen stattfinden. Die Datenbank muss leer bleiben;
  • Zeile 53: Der Kontext wird geschlossen;
  • Zeilen 61–90: Anzeige des Datenbankinhalts. Die Datenbank 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:Zeile 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 also innerhalb einer Transaktion.

Zweifellos gäbe es in EF 5 noch weitere Aspekte zu untersuchen. Wir wissen jedoch genug, um zu unserer Untersuchung einer mehrschichtigen Architektur zurückzukehren. Der Leser findet am Anfang dieses Dokuments Verweise auf Artikel und Bücher, mit denen er sein Wissen über EF 5 vertiefen kann.

3.6. Untersuchung einer mehrschichtigen Architektur auf Basis von EF 5

Wir kehren zu unserer in Absatz 2 beschriebenen Fallstudie zurück. Es handelt sich um eine Webanwendung ASP.NET, die wie folgt strukturiert ist:

Wir beginnen mit der Erstellung der Datenzugriffsebene [DAO]. Diese Ebene stützt sich auf EF5.

3.6.1. Das neue Projekt

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

Wir fügen vier Ordner mit den Namen [2] hinzu, in denen wir unsere Codes einordnen werden. Der Ordner [Entites] ist eine Kopie des Ordners [Entites] aus dem vorherigen Projekt. Nach dieser Kopie treten Fehler auf, da wir nicht über die richtigen Referenzen verfügen. Wir müssen eine Referenz zu Entity Framework 5 hinzufügen. Dazu befolgen wir die in Abschnitt 3.4 auf Seite 21 beschriebene 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 Verbindung zur Datenbank konfiguriert:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Weitere Informationen zur Konfiguration des Entity Frameworks finden Sie unter 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>

  <!-- Verbindungszeichenfolge zur Datenbank -->
  <connectionStrings>
    <add name="monContexte"
         connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <!-- Der 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 Klasse „Exception“

Wir werden eine projektspezifische Ausnahmeklasse verwenden. Diese wird von der Schicht [DAO] ausgegeben:

Die Schicht [DAO] fängt alle Ausnahmen ab, die bis zu ihr hochgelangt sind, und kapselt sie in eine Ausnahme vom Typ [RdvMedecinsException] ein. Diese Ausnahme sieht wie folgt aus:


using System;

namespace RdvMedecins.Exceptions
{
  public class RdvMedecinsException : Exception
  {

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

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

    // Identität
    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 leitet sich von der Klasse [Exception] ab;
  • Zeile 9: Sie fügt ihrer Basisklasse einen Fehlercode hinzu;
  • Zeilen 12–32: Die verschiedenen Konstruktoren berücksichtigen das Vorhandensein des Feldes [Code].

Das Projekt entwickelt sich wie folgt weiter:

3.6.3. Die Schicht [DAO]

Die Schicht [DAO] bietet eine Schnittstelle zur Schicht [ASP.NET]. Um diese zu identifizieren, muss man sich die Webseiten der Anwendung ansehen:

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

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


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

namespace RdvMedecins.Dao
{
  public interface IDao
  {
    // Kundenliste
    List<Client> GetAllClients();
    // Liste der Ärzte
    List<Medecin> GetAllMedecins();
    // Liste der Terminfenster eines Arztes
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // Liste der RV eines bestimmten Arztes an einem bestimmten Tag
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // einen RV hinzufügen
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // einen Termin löschen
    void SupprimerRv(int idRv);
    // Eine Entität T anhand ihres Primärschlüssels suchen
    T Find<T>(int id) where T : class;
  }
}

Die Methoden in den Zeilen 10–20 ergeben sich aus der soeben durchgeführten Analyse. Die Methode in Zeile 22 dient dazu, dem Umstand Rechnung zu tragen, dass mit Lazy Loading gearbeitet wird. Wenn in der Schicht [ASP.NET] eine Abhängigkeit einer Entität benötigt wird, wird diese mit dieser Methode aus der Datenbank abgerufen.

Die Implementierung [Dao] 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
  {

    //Kundenliste
    public List<Client> GetAllClients()
    {
      // Liste der Kunden
      List<Client> clients = null;
      try
      {
        // Persistenzkontext öffnen
        using (var context = new RdvMedecinsContext())
        {
          // Liste der Kunden
          clients = context.Clients.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(1, "GetAllClients", ex);
      }
      // Das Ergebnis wird zurückgegeben
      return clients;
    }

    // Liste der Ärzte
    public List<Medecin> GetAllMedecins()
    {
      // Liste der Ärzte
      List<Medecin> medecins = null;
      try
      {
        // Persistenzkontext öffnen
        using (var context = new RdvMedecinsContext())
        {
          // Liste der Ärzte
          medecins = context.Medecins.ToList();
        }

      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(2, "GetAllMedecins", ex);
      }
      // das Ergebnis wird zurückgegeben
      return medecins;
    }

    // Liste der Terminfenster eines bestimmten Arztes
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
   ...
    }

    // Liste der RV eines Arztes für einen bestimmten Tag
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
 ...
    }

    // einen RV hinzufügen
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
 ...
    }

    // einen Termin löschen
    public void SupprimerRv(int idRv)
    {
...
    }

    // einen Kunden suchen
    public Client FindClient(int id)
    {
...
    }

    // einen Termin finden
    public Creneau FindCreneau(int id)
    {
 ...
    }

    // einen Arzt finden
    public Medecin FindMedecin(int id)
    {
....
    }

    // einen Termin finden
    public Rv FindRv(int id){
...
    }

  }
}

Erläutern wir die Methode [GetAllClients], die die Liste aller Kunden zurückgeben soll:

  • Zeilen 18–31: Die Suche nach Kunden erfolgt in einem try/catch-Block. Dies gilt auch für alle nachfolgenden Methoden;
  • Zeile 21: Eröffnung eines neuen Kontexts;
  • Zeile 24: Die Entitäten [Client] werden in den Kontext geladen und in eine Liste aufgenommen.

Die Methode [GetAllMedecins], die die Liste aller Ärzte zurückgeben soll, ist analog (Zeilen 37–57).

Die Methode [GetCreneauxMedecin] lautet wie folgt:


// Liste der Termine eines bestimmten Arztes
    public List<Creneau> GetCreneauxMedecin(int idMedecin)
    {
      // Liste der Termine
      try
      {
        // Persistenzkontext öffnen
        using (var context = new RdvMedecinsContext())
        {
          // Hier werden die Termine des Arztes abgerufen
          Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
          // Liste der Termine des Arztes
          return medecin.Creneaux.ToList<Creneau>();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
      }
    }
  • Zeile 9: Eröffnung eines neuen Persistenzkontexts;
  • Zeile 11: Es wird nach dem Arzt gesucht, dessen Primärschlüssel vorliegt. Dabei wird die Abhängigkeit [Creneaux] angefordert, bei der es sich um eine Sammlung der Termine des Arztes handelt. Falls der Arzt nicht existiert, löst die Single-Methode eine Ausnahme aus;
  • Zeile 13: Die Liste der Termine wird zurückgegeben.

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


// Liste der RV eines Arztes für einen bestimmten Tag
    public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
    {
      // Liste der Termine
      List<Rv> rvs = null;

      try
      {
        // Öffnen des Persistenzkontexts
        using (var context = new RdvMedecinsContext())
        {
          // Abruf des Arztes
          Medecin medecin = context.Medecins.Find(idMedecin);
          if (medecin == null)
          {
            throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
          }
          // Liste der Termine
          rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
      }
      // Ergebnis zurückgeben
      return rvs;
    }
  • Zeile 13: Der Arzt, dessen Primärschlüssel vorliegt, wird in den Kontext geladen;
  • Zeilen 14–17: Falls er nicht vorhanden ist, wird eine Ausnahme ausgelöst;
  • Zeile 19: Die Abfrage LINQ wird ausgeführt, um die Termine für diesen Arzt abzurufen;

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


// einen Termin hinzufügen RV
    public int AjouterRv(DateTime jour, int idCreneau, int idClient)
    {
      // Nummer des hinzugefügten Termins
      int idRv;
      try
      {
        // Persistenzkontext öffnen
        using (var context = new RdvMedecinsContext())
        {
          // Zeitfenster wird abgerufen
          Creneau creneau = context.Creneaux.Find(idCreneau);
          if (creneau == null)
          {
            throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
          }
          // Kunde wird abgerufen
          Client client = context.Clients.Find(idClient);
          if (client == null)
          {
            throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
          }
          // Terminfenster wird angelegt
          Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
          // Hinzufügen zum Kontext
          context.Rvs.Add(rv);
          // Speichern des Kontexts
          context.SaveChanges();
          // Abruf des Primärschlüssels des hinzugefügten Termins
          idRv = (int)rv.Id;
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(7, "AjouterRv", ex);
      }
      // Ergebnis
      return idRv;
    }
  • Zeile 12: Der Terminzeitraum wird in der Datenbank gesucht;
  • Zeilen 13–16: Wird er nicht gefunden, wird eine Ausnahme ausgelöst;
  • Zeile 18: Der Kunde des Termins wird in der Datenbank gesucht;
  • Zeilen 19–22: Wird er nicht gefunden, wird eine Ausnahme ausgelöst;
  • Zeile 24: Es wird ein Objekt [Rv] mit den erforderlichen Informationen erstellt;
  • Zeile 26: Es wird dem Persistenzkontext hinzugefügt;
  • Zeile 28: Der Persistenzkontext wird mit der Datenbank synchronisiert. Der Termin wird dann in die Datenbank geschrieben;
  • Zeile 30: Wir wissen, dass nach der Synchronisierung mit der Datenbank die Primärschlüssel der eingefügten Elemente verfügbar sind. Wir rufen den Primärschlüssel des hinzugefügten Termins ab;
  • Zeile 31: Der Persistenzkontext wird geschlossen.

Die Methode [SupprimerRv] soll einen Termin löschen, dessen Primärschlüssel ihr übergeben wird.


// Löschen eines RV
    public void SupprimerRv(int idRv)
    {
      try
      {
        // Persistenzkontext öffnen
        using (var context = new RdvMedecinsContext())
        {
          // Rv abrufen
          Rv rv = context.Rvs.Find(idRv);
          if (rv == null)
          {
            throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
          }
          // Rv löschen
          context.Rvs.Remove(rv);
          // Kontext speichern
          context.SaveChanges();
        }
      }
      catch (Exception ex)
      {
        throw new RdvMedecinsException(8, "SupprimerRv", ex);
      }
    }
  • Zeile 7: Neuer Persistenzkontext;
  • Zeile 10: Der zu löschende Termin wird in den Kontext übernommen;
  • Zeilen 11–15: Falls er nicht vorhanden ist, wird eine Ausnahme ausgelöst;
  • Zeile 16: Er wird aus dem Kontext entfernt;
  • Zeile 18: Der Kontext wird mit der Datenbank synchronisiert;
  • Zeile 19: Der Kontext wird geschlossen.

Die Methode [Find<T>] ermöglicht es, in der Datenbank eine Entität vom Typ T anhand ihres Primärschlüssels zu suchen. Der Code könnte wie folgt aussehen:


public T Find<T>(int id)  where T : class
    {
      try
      {
        // Öffnen des Persistenzkontexts
        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 Methode Set<T> lässt sich ein DbSet<T> abrufen, auf den die üblichen Methoden angewendet werden können.

Das Projekt entwickelt sich wie folgt weiter:

3.6.4. Test der Schicht [DAO]

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

Ein Konsolenprogramm fordert [Spring.net] auf, die Schicht [DAO] zu instanziieren. Anschließend testet es die verschiedenen Funktionen der Schnittstelle der Schicht [DAO]. Anstelle eines Konsolenprogramms wäre es besser gewesen, ein Testprogramm vom Typ NUnit zu schreiben. Ein Testprogramm für die Schicht [DAO] 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
      {
        // Instanziierung der Schicht [DAO] über Spring
        dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;

        // Anzeige der Kunden
        List<Client> clients = dao.GetAllClients();
        DisplayClients("Liste des clients :", clients);

        // Anzeige der Ärzte
        List<Medecin> medecins = dao.GetAllMedecins();
        DisplayMedecins("Liste des médecins :", medecins);

        // Liste der Termine des Arztes Nr. 0
        List<Creneau> creneaux = dao.GetCreneauxMedecin((int)medecins[0].Id);
        DisplayCreneaux(string.Format("Liste des créneaux horaires du médecin {0}", medecins[0]), creneaux);

        // Liste der Termine eines Arztes für einen bestimmten Tag
        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)));

        // einen Termin (RV) für Arzt Nr. 1 im Zeitfenster Nr. 0 hinzufügen
        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)));

        // Einen Termin in einem bereits belegten Zeitfenster hinzufügen – muss eine Ausnahme auslösen
        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));
        }

        // einen Termin löschen
        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));
      }
      //Pause 
      Console.ReadLine();
    }

    // Hilfsmethoden – zeigt Listen an
    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 Schicht [DAO]. Um den Test unabhängig von der tatsächlichen Implementierung dieser Schicht zu gestalten, ist diese Referenz vom Typ der Schnittstelle [IDao] und nicht vom Typ der Klasse [Dao];
  • Zeile 18: Die Schicht [DAO] 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 Schnittstelle [IDao] um;
  • Zeilen 21–22: zeigen die Kunden an;
  • Zeilen 25–26: zeigen die Patienten an;
  • Zeilen 29–30: zeigen die Liste der Termine von Arzt Nr. 0 an;
  • Zeile 33: Zeigt die Termine von Arzt Nr. 0 für den 23.11.2013 an. Es sollten keine vorhanden sein;
  • Zeile 37: Fügt einen Termin für Arzt Nr. 0 für den 23.11.2013 hinzu;
  • Zeile 39: Zeigt die Termine von Arzt Nr. 0 für den 23.11.2013 an. Es sollte ein Termin vorhanden sein;
  • Zeile 46: Der gleiche Termin wird ein zweites Mal hinzugefügt. Es muss eine Ausnahme auftreten;
  • Zeile 57: Der einzige hinzugefügte Termin wird gelöscht;
  • Zeile 58: Zeigt die Termine von Arzt Nr. 0 für den 23.11.2013 an. Es darf kein Termin vorhanden sein.

3.6.5. Konfiguration von Spring.net

Im obigen Testprogramm haben wir die Anweisung, die die Schicht [DAO] instanziiert, nur kurz gestreift:


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

Die Klasse [ContextRegistry] ist eine Spring-Klasse im Namensraum [Spring.Context.Support]. Um Spring nutzen zu können, müssen wir dessen DLL zu den Projektreferenzen hinzufügen. Dazu gehen wir wie folgt vor:

  • in [1] suchen wir mit dem Tool [NuGet] nach Paketen;
  • in [2], man sucht online nach Paketen;
  • in [3] gibt man das Stichwort spring in das Suchfeld ein;
  • In [4] werden die Pakete angezeigt, deren Beschreibung dieses Stichwort enthält. Hier passt [Spring.Core] zu unseren Anforderungen. Wir installieren es.

Die Projektreferenzen ä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 noch nicht, dass es funktionieren wird. Zunächst müssen wir Spring in der Datei [App.config] konfigurieren. Dies ist der heikelste Teil des Projekts. Die neue Datei [App.config] sieht wie folgt aus:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Weitere Informationen zur Konfiguration des Entity Frameworks finden Sie unter 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>
    <!-- Allgemeine Protokollierung-->
    <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>
  <!-- Verbindungsstrings -->
  <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-Konfiguration -->
  <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>
  <!-- Konfiguration 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, Verbindungsstrings, ProviderFactory. Die Datei sieht nun wie folgt aus:


<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <!-- Weitere Informationen zur Entity-Framework-Konfiguration finden Sie unter 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-Konfiguration -->
  <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>
  <!-- Konfiguration 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 Datei XML (Zeilen 19–21) verwaltet;
  • Zeile 9: definiert die Klasse, die den Abschnitt <spring><objects> der Datei XML (Zeilen 22–24) verwaltet;
  • Zeile 13: definiert die Klasse, die den Abschnitt <common><logging> der Datei XML (Zeilen 27–36) verwaltet;
  • Zeilen 7–14: sind unverändert. 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 frei wählbar. Es handelt sich um die Kennung des Objekts. Das Attribut „type“ bezeichnet die zu instanziierende Klasse in der Form „vollständiger Name der Klasse, Assembly, die die Klasse enthält“. Die Klasse ist hier diejenige, die die Schicht [DAO] implementiert: [RdvMedecins.Dao.Dao]. Um ihre Assembly zu ermitteln, muss man die Projekteigenschaften überprüfen:

In [1], der Name der bereitzustellenden Assembly;

  • Zeilen 27–36: Die Konfiguration von „Common Logging“ ist stabil. Gegebenenfalls muss die Protokollierungsstufe in Zeile 32 angepasst werden. Nach der Debugging-Phase kann die Stufe auf INFO gesetzt werden.

Letztendlich erweist sich die Spring-Konfigurationsdatei, die auf den ersten Blick komplex erscheint, als einfach. Es müssen lediglich folgende Änderungen vorgenommen werden:

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

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


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

[ContextRegistry] ist eine Spring-Klasse, die die in einer Datei namens [Web.config] oder [App.config] vorgenommene Spring-Konfiguration nutzt. In diesem Fall nutzt sie den folgenden Abschnitt der Datei [App.config]:


  <spring>
    <context>
      <resource uri="config://spring/objects" />
    </context>
    <objects xmlns="http://www.springframework.net">
      <object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
    </objects>
</spring>
  • ContextRegistry.GetContext() nutzt den Kontext der Zeilen 2–4. 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") nutzt den Abschnitt der Zeilen 5–7. Sie gibt eine Referenz auf das Objekt zurück, das das Attribut id= „rdvmedecinsDao“ besitzt. Dies ist das in Zeile 6 definierte Objekt. Spring instanziiert daraufhin die durch das Attribut type definierte Klasse unter Verwendung ihres Konstruktors ohne Parameter. Dieser muss also vorhanden sein. Anschließend wird die Referenz auf das erstellte Objekt an den aufrufenden Code zurückgegeben. Wird das Objekt im Code ein zweites Mal angefordert, gibt Spring lediglich eine Referenz auf das zuerst erstellte Objekt zurück. Dies ist das Entwurfsmuster (Design Pattern) namens Singleton.

Die Erstellung des Objekts kann komplexer sein. Man kann einen Konstruktor mit Parametern verwenden oder die Initialisierung bestimmter Felder des Objekts nach dessen Erstellung festlegen. Weitere Informationen zu diesem Thema finden Sie im Artikel „Spring-Tutorial IOC für .NET“, unter URL [http://tahe.developpez.com/dotnet/springioc/].

Anschließend 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 gehen nun davon aus, dass unsere Schicht [DAO] gültig ist. Das Tutorial könnte an dieser Stelle enden. Wir haben bisher Folgendes gezeigt:

  • die Grundlagen von Entity Framework 5 (ORM);
  • eine [DAO]-Schicht, die dieses ORM verwendet.

Erinnern wir uns an unsere zu Beginn dieses Dokuments beschriebene Fallstudie. Wir gehen von einer bestehenden Anwendung mit folgender Architektur aus:

die wir in folgende umwandeln möchten:

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


  public interface IDao
  {
    // Kundenliste
    List<Client> GetAllClients();
    // Liste der Ärzte
    List<Medecin> GetAllMedecins();
    // Liste der Terminfenster eines Arztes
    List<Creneau> GetCreneauxMedecin(int idMedecin);
    // Liste der RV eines bestimmten Arztes an einem bestimmten Tag
    List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
    // einen RV hinzufügen
    int AjouterRv(DateTime jour, int idCreneau, int idClient);
    // einen Termin löschen
    void SupprimerRv(int idRv);
  }

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


// eine Entität T anhand ihres Primärschlüssels suchen
T Find<T>(int id) where T : class;

Die Hinzufügung dieser Methode ist darauf zurückzuführen, dass die Schicht ORM standardmäßig im Lazy-Loading-Modus arbeitet. Die Entitäten gelangen ohne ihre Abhängigkeiten in die Schicht [ASP.NET]. Die oben genannte Methode ermöglicht es uns, diese bei Bedarf abzurufen – und in bestimmten Fällen sind sie tatsächlich erforderlich. NHibernate arbeitet standardmäßig ebenfalls im Lazy-Loading-Modus, ich hatte es jedoch im Eager-Loading-Modus verwendet. Die Entitäten wurden in der Ebene [ASP.NET] zusammen mit ihren Abhängigkeiten bereitgestellt.

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

3.6.6. Generierung der DLL aus der Schicht [DAO]

In der folgenden Architektur:

stehen der Schicht [ASP.NET] die rechts davon liegenden Schichten in Form von DLL zur Verfügung. Wir erstellen daher die DLL aus der Schicht [DAO].

  • In [1] wählen wir das Testprogramm aus, und in [2] nehmen wir es nicht in die zu generierende DLL auf;
  • In [3] wird in den Projekteigenschaften angegeben, dass die zu erstellende Assembly eine DLL ist;
  • In [4] wird im Menü von VS angegeben, dass eine Assembly vom Typ [Release] generiert werden soll, die weniger Informationen enthält als eine Assembly vom Typ [Debug];
  • In [5] wird die Projekt-Assembly neu generiert. Dabei wird die DLL generiert;
  • in [6] werden alle Dateien des Projekts angezeigt;
  • Mit [7] wird die Datei DLL des Projekts der Ebene [DAO] erstellt. Diese wird vom Webprojekt ASP.NET verwendet;
  • in [8] aktualisieren wir die Projektansicht;
  • In [9] werden die Dateien DLL aus dem Ordner [Release] in einem externen Ordner [lib] namens [10] zusammengefasst. Von dort bezieht das Webprojekt seine Referenzen.

3.6.7. Die Ebene [ASP.NET]

Hier erläutern wir die Portierung der Anwendung [ASP.NET / NHibernate] auf die Anwendung [ASP.NET / EF 5]. Wir werden mit Visual Studio Express 2012 für das Web arbeiten, das kostenlos unter URL [http://www.microsoft.com/visualstudio/fra/downloads] verfügbar ist.

Wir werden auf der Grundlage des bestehenden Webprojekts arbeiten, das mit VS 2010 erstellt wurde.

  • In [1] öffnen wir das vorhandene Projekt:
  • In [2] hat das geladene Projekt folgende Verweise: [3]:
  • [NHibernate] ist die DLL des Frameworks NHibernate,
  • [Spring.Core] ist die DLL des Frameworks Spring.net,
  • [log4net] ist die DLL des log4net-Logging-Frameworks. Dieses Framework wird von Spring.net verwendet,
  • [MySql.Data] ist der Treiber ADO.NET des SGBD MySQL,
  • [rdvmedecins] ist die DLL der Schicht [DAO], die mit NHibernate erstellt wurde;
  • 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 [Parcourir];
  • In [8] wählen wir alle DLL aus Projekt Nr. 2 aus, die zuvor in den Ordner [lib] verschoben wurden;
  • in [9], eine Zusammenfassung, die wir bestätigen;
  • in [10], das Webprojekt mit seinen neuen Referenzen.

Danach sieht das Projekt wie folgt aus:

  • In [1] ist der Code zur Verwaltung der Webseiten auf die beiden Dateien [Global.asax] und [Default.aspx] verteilt. Hilfs-Code wurde im Ordner [Entites] abgelegt. Schließlich wird die Anwendung durch die Datei [Web.config] konfiguriert;
  • in der Datei „[2]“ generieren wir die Assembly des Projekts;
  • 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. Es muss also ein cast verwendet werden. Dieser Fehler tritt im gesamten Code immer wieder auf, da die Entitäten des Projekts ASP.NET / NHibernate Primärschlüssel vom Typ „int“ hatten, während diejenigen des Projekts ASP.NET / EF vom Typ „int?“ sind. Wir beheben alle Fehler dieser Art und generieren das Projekt neu. Danach sind keine Fehler mehr vorhanden.

Bevor wir das Projekt ausführen, müssen wir noch ein Detail klären: die Instanziierung der Schicht [DAO] durch das Spring-Framework. Diese erfolgt in [Global.asax]:


protected void Application_Start(object sender, EventArgs e)
    {
      // Bestimmte Daten aus der Datenbank werden zwischengespeichert
      try
      {
        // Instanziierung der Schicht [dao]
        Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
        ...
      }
      catch (Exception ex)
      {...
      }
    }

Im Testprogramm der Schicht [DAO] wurde die Schicht [DAO] wie folgt instanziiert:


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

Beide Methoden sind identisch. Wir erinnern uns, dass diese Instanziierung der Schicht [DAO] auf einer in [App.config] vorgenommenen Konfiguration beruhte. Wir ersetzen nun den aktuellen Inhalt [Web.config] des Webprojekts durch den Inhalt [App.config] aus dem Projekt der Ebene [DAO], um die gleiche Konfiguration zu erhalten.

Wir sind bereit für eine erste Ausführung. Die Startseite wird angezeigt: [1]:

  • In [2] geben wir einen Termin ein und bestätigen;
  • in [3] tritt ein Fehler auf.

Bei Betrachtung des von der Seite angezeigten Fehlertextes stellt man fest, dass es sich bei der gemeldeten Ausnahme um eine „Lazy Loading“-Ausnahme handelt: Es wurde versucht, eine Abhängigkeit eines Objekts zu laden, obwohl der Persistenzkontext, der dieses verwaltet, geschlossen wurde. Das Objekt befindet sich nun im Zustand „getrennt“. Dieser Fehler ist darauf zurückzuführen, dass NHibernate im Eager-Loading-Modus verwendet wurde, während EF standardmäßig im Lazy-Loading-Modus arbeitet. In der oben rot markierten Zeile:

  • steht „rdv“ für ein Objekt [Rv], das ohne seine Abhängigkeiten geladen wurde;
  • Um rdv.Creneau.Id auszuwerten, versucht die Anwendung, die Abhängigkeit rdv.Creneau zu laden. Da sich diese jedoch nicht mehr im Kontext befindet, ist dies nicht möglich, woraus sich die Ausnahme ergibt.

Hier ist die Lösung einfach. In Zeile 108 wird ein Eintrag in einem Wörterbuch angelegt, dessen Schlüssel der Primärschlüssel des Zeitfensters eines Termins ist. Nun kapselt die Entität [Rv] den Primärschlüssel des zugehörigen Zeitfensters. Wir schreiben also:


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

Wir versuchen die Ausführung erneut. Diesmal lautet der Fehler wie folgt:

Der Fehler ist ähnlich. In Zeile 132 wird versucht, die Abhängigkeit [Client] eines Objekts [Rv] in die Ebene ASP.NET zu laden, also außerhalb des Kontexts. Das Objekt [Client] muss aus der Datenbank abgerufen werden. Um dieses Problem zu beheben, wurde die Schnittstelle [IDao] um die folgende Methode erweitert:


    // Eine Entität T über ihren Primärschlüssel finden
    T Find<T>(int id) where T : class;

Damit lassen sich die Abhängigkeiten abrufen. Die oben genannte fehlerhafte Zeile wird somit wie folgt umgeschrieben:


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

Erneut wird deutlich, wie wichtig es ist, dass Entitäten ihre Fremdschlüssel enthalten. Hier ermöglicht uns die Entität [Rv] 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 zu diesem Artikel verfügbar ist.

3.7. Conclusion

Wir haben die Portierung einer Anwendung ASP.NET / NHibernate erfolgreich abgeschlossen:

in eine Anwendung ASP.NET / EF 5:

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

  • Die Entitäten waren nicht exakt identisch. Der Typ der Primärschlüssel der Entitäten NHibernate war „int“, während der von EF 5 „int?“ war. Dies führte dazu, dass wir cast in den Webcode einfügen mussten;
  • die Art des Ladens der Entitäten war bei den beiden ORM nicht identisch: Eager Loading für NHibernate, Lazy Loading für EF 5. Dies veranlasste uns, die Schnittstelle der Schicht [DAO] um eine generische Methode zu erweitern, mit der eine Entität über ihren Primärschlüssel abgerufen werden kann.

Dennoch erwies sich die Portierung als recht einfach, was – falls es noch eines Beweises bedurfte – erneut die Schichtenarchitektur und die Abhängigkeitsinjektion mit Spring oder einem anderen Dependency-Injection-Framework rechtfertigt.

Wir werden nun die Auswirkungen einer Änderung an SGBD auf die bisherige Architektur untersuchen. Wir werden alle bisherigen Projekte auf vier weitere SGBD portieren:

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

Die Codes werden sich nicht mehr ändern. Es werden sich lediglich folgende Elemente ändern:

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

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