Skip to content

9. Zugriff auf Datenbanken

9.1. Konnektor ADO.NET

Kehren wir zu der bereits mehrfach verwendeten Schichtenarchitektur zurück

In den untersuchten Beispielen hat die Schicht [dao] bislang zwei Arten von Datenquellen genutzt:

  • im Code fest eingebettete Daten
  • Daten aus Textdateien

In diesem Kapitel untersuchen wir den Fall, in dem die Daten aus einer Datenbank stammen. Die 3-Schichten-Architektur entwickelt sich dann zu einer mehrschichtigen Architektur weiter. Davon gibt es verschiedene. Wir werden die Grundkonzepte anhand der folgenden Architektur untersuchen:

Im obigen Schema kommuniziert die Schicht [dao] [1] mit der Schicht SGBD [3] übereiner klassenbasierten Bibliothek, die für das verwendete SGBD spezifisch ist und mit diesem ausgeliefert wird. Diese Schicht implementiert Standardfunktionen, die unter dem Begriff ADO (ActiveX Data Objects) zusammengefasst sind. Eine solche Schicht wird als Provider (hier: Anbieter für den Zugriff auf eine Datenbank) oder auch als Konnektor bezeichnet. Die meisten SGBD verfügen mittlerweile über einen ADO.NET-Konnektor, was zu Beginn der .NET-Plattform noch nicht der Fall war. Die .NET-Konnektoren bieten keine Standardschnittstelle zur [dao]-Schicht, weshalb diese im Code die Namen der Klassen des Konnektors enthält. Wechselt man den SGBD, wechselt man den Konnektor und die Klassen, und man muss dann die Schicht [dao] anpassen. Dies ist einerseits eine leistungsfähige Architektur, da der Konnektor .NET für einen bestimmten SGBD geschrieben wurde und diesen optimal nutzen kann, und gleichzeitig starr, da ein Wechsel des SGBD eine Änderung der Schicht [dao] erfordert. Dieses zweite Argument ist jedoch zu relativieren: Unternehmen wechseln nicht sehr oft den SGBD. Außerdem werden wir später sehen, dass es seit Version 2.0 von .NET einen generischen Konnektor gibt, der Flexibilität bietet, ohne die Leistung zu beeinträchtigen.

9.2. Die beiden Betriebsmodi einer Datenquelle

Die .NET-Plattform ermöglicht die Nutzung einer Datenquelle auf zwei verschiedene Arten:

  1. Verbundener Modus
  2. Offline-Modus

Im verbundenen Modus stellt die Anwendung

  1. eine Verbindung zur Datenquelle
  2. arbeitet mit der Datenquelle im Lese-/Schreibmodus
  3. schließt die Verbindung

Im Offline-Modus

  1. eine Verbindung zur Datenquelle herstellt
  2. ruft eine Speicherkopie aller oder eines Teils der Daten aus der Quelle ab
  3. schließt die Verbindung
  4. arbeitet mit der Speicherkopie der Daten im Lese-/Schreibmodus
  5. Wenn die Arbeit abgeschlossen ist, öffnet sie eine Verbindung, sendet die geänderten Daten an die Datenquelle, damit diese sie berücksichtigt, und schließt die Verbindung

Wir betrachten hier ausschließlich den verbundenen Modus.

9.3. Grundbegriffe der Datenbanknutzung

Wir werden die wichtigsten Konzepte der Datenbanknutzung anhand einer SQL Server Compact 3.5-Datenbank erläutern. Diese SGBD wird mit Visual Studio Express ausgeliefert. Es handelt sich um eine schlanke Datenbank, die jeweils nur einen Benutzer gleichzeitig verwalten kann. Sie reicht jedoch aus, um einen Einstieg in die Programmierung mit Datenbanken zu geben. Später werden wir weitere Datenbanken vorstellen.

Die verwendete Architektur sieht wie folgt aus:

Eine Konsolenanwendung [1] wird eine Datenbank vom Typ SqlServer Compact [3,4] über den Konnektor Ado.Net dieses SGBD QZ nutzenXW2HTMLP003400ZQX.

9.3.1. Die Beispiel-Datenbank „

Wir werden die Datenbank direkt in Visual Studio Express erstellen. Dazu erstellen wir ein neues Konsolenprojekt.

  • [1]: das Projekt
  • [2]: Wir öffnen die Ansicht „Datenbank-Explorer“
  • [3]: Wir erstellen eine neue Verbindung
  • [4]: Man wählt den Typ von SGBD aus
  • [5,6]: Man wählt den SGBD SQL Server Compact aus
  • [7]: Die Datenbank wird erstellt
  • [8]: Eine SQL Server Compact-Datenbank wird in einer einzigen Datei mit der Endung .sdf gekapselt. Man gibt an, wo sie angelegt werden soll, hier im C#-Projektordner.
  • [9]: Die neue Datenbank wurde mit dem Namen [dbarticles.sdf] versehen
  • [10]: Es wird die Sprache Französisch ausgewählt. Dies hat Auswirkungen auf die Sortiervorgänge.
  • [11,12]: Die Datenbank kann durch ein Passwort geschützt werden. Hier „dbarticles“.
  • [13]: Die Informationsseite wird bestätigt. Die Datenbank wird nun physisch angelegt:
  • [14]: Der Name der soeben erstellten Datenbank
  • [15]: Die Option „Save my password“ wird aktiviert, damit das Passwort nicht jedes Mal neu eingegeben werden muss
  • [16]: Wir überprüfen die Verbindung
  • [17]: Alles in Ordnung
  • [18]: Die Informationsseite wird bestätigt
  • [19]: Die Verbindung wird im Datenbank-Explorer angezeigt
  • [20]: Derzeit enthält die Datenbank noch keine Tabellen. Wir erstellen eine. Ein Artikel wird folgende Felder haben:
    • id: eine eindeutige Kennung – Primärschlüssel
    • nom: Artikelname – eindeutig
    • prix: Preis des Artikels
    • stockactuel: der aktuelle Lagerbestand
    • stockminimum: der Mindestbestand, bei Unterschreitung dessen der Artikel nachbestellt werden muss
  • [21]: Das Feld [id] ist vom Typ Ganzzahl und ist der Primärschlüssel [22] der Tabelle.
  • [23]: Dieser Primärschlüssel ist vom Typ Identity. Dieses für den SGBD-SQL-Server spezifische Konzept besagt, dass der Primärschlüssel vom SGBD selbst generiert wird. Hier ist der Primärschlüssel eine ganze Zahl, die bei 1 beginnt und bei jedem neuen Schlüssel um 1 erhöht wird.
  • [24]: Die übrigen Felder werden angelegt. Es ist zu beachten, dass das Feld [nom] eine Eindeutigkeits s [25] aufweist.
  • [26]: Die Tabelle erhält einen Namen
  • [27]: Nachdem die Tabellenstruktur validiert wurde, erscheint die Tabelle in der Datenbank.
  • [28]: Der Inhalt der Tabelle wird angefordert
  • [29]: Sie ist derzeit leer
  • [30]: Wir füllen sie mit einigen Daten. Eine Zeile wird validiert, sobald wir zur Eingabe der nächsten Zeile übergehen. Das Feld [id] wird nicht eingegeben: Es wird automatisch generiert, sobald die Zeile validiert wird.

Nun müssen wir das Projekt noch so konfigurieren, dass diese Datenbank, die sich derzeit im Stammverzeichnis des Projekts befindet, automatisch in den Ausführungsordner des Projekts kopiert wird:

  • [1]: Wir fordern an, alle Dateien anzuzeigen
  • [2]: Die Datenbank [dbarticles.sdf] wird angezeigt
  • [3]: Sie wird in das Projekt aufgenommen
  • [4]: Beim Hinzufügen einer Datenquelle zu einem Projekt wird ein Assistent gestartet, den wir hier nicht benötigen ([5]).
  • [6]: Die Datenbank ist nun Teil des Projekts. Wir kehren in den Normalmodus zurück [7].
  • [8]: Das Projekt mit seiner Datenbank
  • [9]: In den Eigenschaften der Datenbank ist unter [10] zu sehen, dass diese automatisch in den Ausführungsordner des Projekts kopiert wird. Dort wird das Programm, das wir schreiben werden, sie abrufen.

Da wir nun über eine Datenbank verfügen, können wir diese nutzen. Zuvor möchten wir noch einige Grundbegriffe wiederholen SQL.

9.3.2. Die vier Grundbefehle der Sprache SQL

SQL (Structured Language Query) ist eine teilweise standardisierte Sprache zur Abfrage und Aktualisierung von Datenbanken. Alle SGBD-Dialekte entsprechen dem standardisierten Teil von SQL, fügen der Sprache jedoch proprietäre Erweiterungen hinzu, die bestimmte Besonderheiten von SGBD nutzen. Wir sind bereits auf zwei Beispiele gestoßen: Die automatische Generierung von Primärschlüsseln und die zulässigen Typen für die Spalten einer Tabelle hängen oft vom SGBD ab.

Die vier grundlegenden Befehle der Sprache SQL, die wir hier vorstellen, sind standardisiert und werden von allen SGBD akzeptiert:

select col1, col2,... 
from table1, table2,...
where condition
order by expression
...
Die Abfrage, mit der die in einer Datenbank enthaltenen Daten abgerufen werden können. Nur die Schlüsselwörter in der ersten Zeile sind obligatorisch, die anderen sind optional. Es gibt weitere Schlüsselwörter, die hier nicht aufgeführt sind.
  1. Es wird eine Verknüpfung mit allen Tabellen durchgeführt, die hinter dem Schlüsselwort „from“ stehen
  2. Es werden nur die Spalten beibehalten, die hinter dem Schlüsselwort „select“ stehen
  3. Es werden nur die Zeilen beibehalten, die die Bedingung des Schlüsselworts „where“ erfüllen
  4. Die resultierenden Zeilen, sortiert nach dem Ausdruck des Schlüsselworts „order by“, bilden das Ergebnis der Abfrage. Dieses Ergebnis ist eine Tabelle.
insert into table(col1,col2, ...)
values (val1,val2, ...)
Fügt eine Zeile in die Tabelle ein. (col1, col2, ...) gibt die Spalten der Zeile an, die mit den Werten (val1, val2, ...) initialisiert werden sollen.
update table
set col1=val1, col2=val2
where condition
Aktualisiert die Tabellenzeilen, die die Bedingung erfüllen (alle Zeilen, wenn kein WHERE-Klausel vorhanden ist). Für diese Zeilen erhält die Spalte „coli“ den Wert „vali“
delete from table
where condition
scht alle Tabellenzeilen, die die Bedingung erfüllen

Wir werden eine Konsolenanwendung schreiben, mit der sich Befehle SQL auf der zuvor erstellten Datenbank [dbarticles] ausführen lassen. Hier ist ein Beispiel für die Ausführung. Der Leser wird gebeten, die ausgeführten Befehle SQL und deren Ergebnisse nachzuvollziehen.

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;Persist Security Info=True]

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

Requête SQL (rien pour arrêter) : insert into articles(nom,prix,stockactuel,stockminimum) values('x',100,10,1)
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 100 10 1

Requête SQL (rien pour arrêter) : update articles set prix=prix*1.1 where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 110 10 1

Requête SQL (rien pour arrêter) : delete from articles where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
  • Zeile 1: die sogenannte Verbindungszeichenfolge: Sie enthält alle Parameter, die für die Verbindung zur Datenbank erforderlich sind.
  • Zeile 3: Der Inhalt der Tabelle [articles] wird abgefragt.
  • Zeile 16: Es wird eine neue Zeile eingefügt. Dabei ist zu beachten, dass das Feld id bei diesem Vorgang nicht initialisiert wird, da der Wert dieses Feldes von SGBD generiert wird.
  • Zeile 19: Überprüfung. Zeile 28: Die Zeile wurde erfolgreich hinzugefügt.
  • Zeile 30: Der Preis des gerade hinzugefügten Artikels wird um 10 % erhöht.
  • Zeile 33: Überprüfung
  • Zeile 42: Die Preiserhöhung hat tatsächlich stattgefunden
  • Zeile 44: Der zuvor hinzugefügte Artikel wird gelöscht
  • Zeile 47: Überprüfung
  • Zeilen 53–55: Der Artikel ist nicht mehr vorhanden.

9.3.3. Die grundlegenden Schnittstellen von ADO.NET für den verbundenen Modus

Kehren wir zum Schema einer Anwendung zurück, die eine Datenbank über einen ADO.NET-Konnektor nutzt:

Im verbundenen Modus führt die Anwendung folgende Schritte aus:

  1. eine Verbindung zur Datenquelle herstellen
  2. arbeitet mit der Datenquelle im Lese-/Schreibmodus
  3. schließt die Verbindung

Drei Schnittstellen ADO.NET sind hauptsächlich von diesen Vorgängen betroffen:

  • IDbConnection, die die Eigenschaften und Methoden der Verbindung kapselt.
  • IDbCommand, das die Eigenschaften und Methoden des ausgeführten Befehls SQL kapselt.
  • IDataReader, das die Eigenschaften und Methoden des Ergebnisses eines SQL-Select-Befehls kapselt.

Die Schnittstelle „ “ IDbConnection

Dient zur Verwaltung der Verbindung zur Datenbank. Die Methoden M und Eigenschaften P dieser Schnittstelle, die wir verwenden werden, sind die folgenden:

Name
Typ
Rolle
ConnectionString
P
Verbindungszeichenfolge zur Datenbank. Sie enthält alle Parameter, die für den Aufbau einer Verbindung zu einer bestimmten Datenbank erforderlich sind.
Open
M
Stellt die Verbindung zu der durch ConnectionString definierten Datenbank her
Close
M
schließt die Verbindung
BeginTransaction
M
startet eine Transaktion.
State
P
Verbindungsstatus: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken

Wenn Connection eine Klasse ist, die die Schnittstelle IDbConnection implementiert, kann die Verbindung wie folgt hergestellt werden:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

Die Schnittstelle „ “ IDbCommand

Dient zur Ausführung eines Befehls SQL oder einer gespeicherten Prozedur. Die Methoden M und Eigenschaften P dieser Schnittstelle, die wir verwenden werden, sind die folgenden:

Name
Typ
Rolle
CommandType
P
gibt an, was ausgeführt werden soll – bezieht seine Werte aus einer Aufzählung:
- CommandType.Text: Führt den Befehl SQL aus, der in der Eigenschaft CommandText definiert ist. Dies ist der Standardwert.
- CommandType.StoredProcedure: Führt eine in der Datenbank gespeicherte Prozedur aus
CommandText
P
- der Text des Befehls SQL, der ausgeführt werden soll, wenn CommandType = CommandType.Text
- Name der auszuführenden gespeicherten Prozedur, wenn CommandType = CommandType.StoredProcedure
Connection
P
die Verbindung IDbConnection, die zur Ausführung des Auftrags SQL verwendet werden soll
Transaction
P
die Transaktion IDbTransaction, in der der Auftrag SQL ausgeführt werden soll
Parameters
P
die Liste der Parameter eines konfigurierten Befehls SQL. Der Befehl „update articles set price=price*1.1 where id=@id“ hat den Parameter @id.
ExecuteReader
M
Um einen Befehl SQL Select auszuführen. Man erhält ein Objekt IDataReader, das das Ergebnis von Select darstellt.
ExecuteNonQuery
M
um den Befehl SQL „Update, Insert, Delete“ auszuführen. Man erhält die Anzahl der von der Operation betroffenen Zeilen (aktualisiert, eingefügt, gelöscht).
ExecuteScalar
M
zum Ausführen eines Befehls SQL Select, der nur ein einziges Ergebnis liefert, wie in: select count(*) from articles.
CreateParameter
M
zum Anlegen der Parameter IDbParameter für einen bereits parametrierten Auftrag SQL.
Prepare
M
ermöglicht die Optimierung der Ausführung einer parametrisierten Abfrage, wenn diese mehrfach mit unterschiedlichen Parametern ausgeführt wird.

Wenn Command eine Klasse ist, die die Schnittstelle IDbCommand implementiert, sieht die Ausführung eines Befehls SQL ohne Transaktion wie folgt aus:

// Verbindung wird hergestellt 
IDbConnection connexion=...
connexion.Open();
// Auftragsvorbereitung
IDbCommand commande=new Command();
commande.Connection=connexion;
// Ausführung des SELECT-Befehls
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// Ausführung der Anweisung „update“, „insert“, „delete“
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// Verbindung schließen
connexion.Close();

Die Schnittstelle „ “ IDataReader

dient dazu, die Ergebnisse eines Auftrags SQL Select zu kapseln. Ein Objekt IDataReader stellt eine Tabelle mit Zeilen und Spalten dar, die nacheinander ausgewertet wird: zuerst die erste Zeile, dann die zweite, … Die Methoden M und Eigenschaften P dieser Schnittstelle, die wir verwenden werden, sind die folgenden:

Name
Typ
Rolle
FieldCount
P
Anzahl der Spalten in der Tabelle IDataReader
GetName
M
GetName(i) gibt den Namen der Spalte Nr. i der Tabelle IDataReader zurück.
Item
P
Item[i] steht für die Spalte Nr. i der aktuellen Zeile der Tabelle IDataReader.
Read
M
springt zur nächsten Zeile der Tabelle IDataReader. Gibt den Booleschen Wert True zurück, wenn das Auslesen erfolgreich war, andernfalls False.
Close
M
schließt die Tabelle IDataReader.
GetBoolean
M
GetBoolean(i): Gibt den booleschen Wert der Spalte Nr. i der aktuellen Zeile der Tabelle IDataReader zurück. Die anderen analogen Methoden lauten wie folgt: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString.
Getvalue
M
Getvalue(i): Gibt den Wert der Spalte Nr. i der aktuellen Zeile der Tabelle IDataReader als Typ object zurück.
IsDBNull
M
IsDBNull(i) gibt True zurück, wenn die Spalte Nr. i der aktuellen Zeile der Tabelle IDataReader keinen Wert enthält, was durch den Wert SQL NULL symbolisiert wird.

Die Auswertung eines Objekts IDataReader sieht häufig wie folgt aus:

// Verbindung öffnen 
IDbConnection connexion=...
connexion.Open();
// Befehlsvorbereitung
IDbCommand commande=new Command();
commande.Connection=connexion;
// Ausführung des Select-Befehls
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// Ergebnisauswertung
while(reader.Read()){
     // aktuelle Zeile auswerten
        ...
}
// Reader schließen
reader.Close();
// Verbindung schließen
connexion.Close();

9.3.4. Fehlerbehandlung

Werfen wir noch einmal einen Blick auf die Architektur einer datenbankgestützten Anwendung:

Die Schicht [dao] kann beim Betrieb der Datenbank auf zahlreiche Fehler stoßen. Diese werden als Ausnahmen gemeldet, die vom Konnektor ADO.NET ausgelöst werden. Der Code der Schicht [dao] muss diese Fehler behandeln. Jeder Vorgang mit der Datenbank muss in einem try/catch/finally-Block erfolgen, um eine mögliche Ausnahme abzufangen und zu behandeln sowie die erforderlichen Ressourcen freizugeben. Somit sieht der oben gezeigte Code zur Auswertung des Ergebnisses eines Befehls Select wie folgt aus:

// Verbindung initialisieren
IDbConnection connexion=...
// Verbindung auswerten
try{
     // Öffnen
    connexion.Open();
     // Auftragsvorbereitung
    IDbCommand commande=new Command();
    commande.Connection=connexion;
     // Ausführung des Select-Befehls
    commande.CommandText="select ...";
    IDbDataReader reader=commande.ExecuteReader();
     // Ergebnisauswertung
    try{
        while(reader.Read()){
         // aktuelle Zeile auswerten
        ...
    }finally{
         // Reader schließen
        reader.Close();
    }
}catch(Exception ex){
     // Ausnahmebehandlung
    ...
}finally{
     // Verbindung schließen
    connexion.Close();
}
...

Unabhängig davon, was passiert, müssen die Objekte IDataReader und IDbConnection geschlossen werden. Deshalb erfolgt diese Schließung in den Klauseln finally.

Das Schließen der Verbindung und das Schließen des Objekts IDataReader können mit einer „using“-Klausel automatisiert werden:

// Verbindung auswerten
try{
    using(IDbConnection connexion=...){
         // Öffnen
        connexion.Open();
         // Auftragsvorbereitung
        IDbCommand commande=new Command();
        commande.Connection=connexion;
         // Ausführung des Select-Befehls
        commande.CommandText="select ...";
        using(IDbDataReader reader=commande.ExecuteReader()){
             // Ergebnisauswertung
            while(reader.Read()){
             // aktuelle Zeile auswerten
            ...
        }// unter Verwendung von IData
    }//unter Verwendung von IDbConnection
}catch(Exception ex){
     // Ausnahmebehandlung
    ...
}
..
  • Zeile 3: Die „using“-Klausel stellt sicher, dass die im Block „using(...){...}“ geöffnete Verbindung außerhalb dieses Blocks geschlossen wird, unabhängig davon, wie der Block verlassen wird: auf normale Weise oder durch das Auftreten einer Ausnahme. Man spart sich ein `finally`, doch der Vorteil liegt nicht in dieser geringfügigen Einsparung. Die Verwendung eines `using` erspart es dem Entwickler, die Verbindung selbst zu schließen. Das Vergessen, eine Verbindung zu schließen, kann jedoch unbemerkt bleiben und die Anwendung auf eine Weise „abstürzen“ lassen, die zufällig erscheint – jedes Mal, wenn das SGBD die maximale Anzahl offener Verbindungen erreicht, die es unterstützen kann.
  • Zeile 11: Auf ähnliche Weise wird das Objekt IDataReader geschlossen.

9.3.5. Konfiguration des Beispielprojekts

Das endgültige Projekt sieht wie folgt aus:

  • [1]: Das Projekt verfügt über eine Konfigurationsdatei [App.config]
  • [2]: Es verwendet Klassen aus zwei DLL, die standardmäßig nicht referenziert sind und daher zu den Projektreferenzen hinzugefügt werden müssen:
    • [System.Configuration], um die Konfigurationsdatei [App.config] zu nutzen
    • [System.Data.SqlServerCe], um die SQL Server Compact-Datenbank zu nutzen
  • [3, 4]: Erläutert, wie man Verweise zu einem Projekt hinzufügt.
  • [5, 6]: Eine Anleitung zum Hinzufügen der Datei [App.config] zu einem Projekt.

Die Konfigurationsdatei [App.config] sieht wie folgt aus:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="dbSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
 </connectionStrings>
</configuration>
  • Zeilen 3–5: Das Tag <connectionStrings> im Plural definiert Verbindungszeichenfolgen zu Datenbanken. Eine Verbindungszeichenfolge hat die Form „Parameter1=Wert1;Parameter2=Wert2;...“. Sie definiert alle Parameter, die zum Aufbau einer Verbindung mit einer bestimmten Datenbank erforderlich sind. Diese Verbindungszeichenfolgen ändern sich mit jedem SGBD. Auf der Seite [http://www.connectionstrings.com/] wird das Format dieser Zeichenfolgen für die wichtigsten SGBD angegeben.
  • Zeile 4: Definiert eine bestimmte Verbindungszeichenfolge, in diesem Fall die der Datenbank SQL Server Compact dbarticles.sdf, die wir zuvor erstellt haben:
    • name = Name der Verbindungszeichenfolge. Über diesen Namen wird eine Verbindungszeichenfolge vom C#-Programm abgerufen
    • connectionString: Die Verbindungszeichenfolge für eine SQL Server Compact-Datenbank
    • DataSource: Bezeichnet den Pfad zur Datenbank. Die Syntax |DataDirectory| bezeichnet den Ausführungsordner des Projekts.
    • Password: Das Passwort der Datenbank. Dieser Parameter fehlt, wenn kein Passwort vorhanden ist.

Der C#-Code zum Abrufen der vorherigen Verbindungszeichenfolge lautet wie folgt:


string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
  • ConfigurationManager ist die Klasse von DLL [System.Configuration], die die Nutzung der Datei [App.config] ermöglicht.
  • ConnectionsStrings["nom"].ConnectionString: Bezeichnet das Attribut connectionString des Tags < add name="name" connectionString="..."> im Abschnitt <connectionStrings> von [App.config]

Das Projekt ist nun konfiguriert. Wir betrachten nun die Klasse [Program.cs], von der wir zuvor ein Ausführungsbeispiel gesehen haben.

9.3.6. Das Beispielprogramm

Das Programm [program.cs] lautet wie folgt:


using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Text;
using System.Text.RegularExpressions;
using System.Configuration;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // Konsolenanwendung – führt über die Tastatur eingegebene SQL-Abfragen aus
            // in einer Datenbank, deren Verbindungszeichenfolge aus einer Konfigurationsdatei abgerufen wird

            // Auswertung der Konfigurationsdatei [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // Anzeige der Verbindungszeichenfolge
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // Es wird ein Verzeichnis der akzeptierten SQL-Befehle erstellt
            string[] commandesSQL = new string[] { "select", "insert", "update", "delete" };
            Dictionary<string, bool> dicoCommandes = new Dictionary<string, bool>();
            for (int i = 0; i < commandesSQL.Length; i++) {
                dicoCommandes.Add(commandesSQL[i], true);
            }

            // Einlesen und Ausführen der über die Tastatur eingegebenen Befehle SQL
            string requête = null;    // Text der Abfrage SQL
            string[] champs;            // die Felder der Abfrage    
            Regex modèle = new Regex(@"\s+");    // Reihe von Leerzeichen

            // Schleife zur Eingabe und Ausführung der über die Tastatur eingegebenen Befehle SQL
            while (true) {
                // Abfrageanforderung
                Console.Write("\nRequête SQL (rien pour arrêter) : ");
                requête = Console.ReadLine().Trim().ToLower();
                // Fertig?
                if (requête == "")
                    break;
                // Die Abfrage wird in Felder zerlegt
                champs = modèle.Split(requête);
                // Gültige Abfrage?
                if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) {
                    // Fehlermeldung
                    Console.WriteLine("Requête invalide. Utilisez select, insert, update, delete ou rien pour arrêter");
                    // Nächste Anfrage
                    continue;
                }
                // Abfrage wird ausgeführt
                if (champs[0] == "select") {
                    ExecuteSelect(connectionString, requête);
                } else
                    ExecuteUpdate(connectionString, requête);
            }
        }

        // Ausführung einer Aktualisierungsanfrage
        static void ExecuteUpdate(string connectionString, string requête) {
    ...
        }

        // Ausführung einer Select-Abfrage
        static void ExecuteSelect(string connectionString, string requête) {
....
        }
    }
}
  • Zeilen 1–6: Die in der Anwendung verwendeten Namespaces. Für die Verwaltung einer SQL Server Compact-Datenbank ist der Namespace [System.Data.SqlServerCe] aus Zeile 3 erforderlich. Hier besteht eine Abhängigkeit von einem proprietären Namensraum, der zu SGBD gehört. Daraus lässt sich ableiten, dass das Programm geändert werden muss, wenn der SGBD gewechselt wird.
  • Zeile 18: Die Verbindungszeichenfolge zur Datenbank wird aus der Datei [App.config] gelesen und in Zeile 25 angezeigt. Sie dient zum Aufbau einer Verbindung zur Datenbank.
  • Zeilen 28–32: Ein Wörterbuch, in dem die Namen der vier zulässigen Befehle SQL gespeichert sind: select, insert, update, delete.
  • Zeilen 40–62: Die Schleife zur Erfassung der über die Tastatur eingegebenen Befehle SQL und deren Ausführung in der Datenbank
  • Zeile 48: Die über die Tastatur eingegebene Zeile wird in Felder zerlegt, um den ersten Term zu ermitteln, der einer der folgenden sein muss: select, insert, update, delete
  • Zeilen 50–55: Ist die Abfrage ungültig, wird eine Fehlermeldung angezeigt und es wird zur nächsten Abfrage übergegangen.
  • Zeilen 57–61: Der eingegebene Befehl „SQL“ wird ausgeführt. Diese Ausführung erfolgt unterschiedlich, je nachdem, ob es sich um einen Befehl vom Typ „select“ oder um einen Befehl vom Typ „insert“, „update“ oder „delete“ handelt. Im ersten Fall ruft der Befehl Daten aus der Datenbank ab, ohne diese zu ändern; im zweiten Fall aktualisiert er sie, ohne Daten abzurufen. In beiden Fällen wird die Ausführung an eine Methode delegiert, die zwei Parameter benötigt:
    • die Verbindungszeichenfolge, mit der sie eine Verbindung zur Datenbank herstellen kann
    • die Anweisung SQL, die über diese Verbindung ausgeführt werden soll

9.3.7. Ausführung einer Abfrage SELECT

Die Ausführung von Befehlen vom Typ SQL erfordert folgende Schritte:

  1. Verbindung zur Datenbank herstellen
  2. Senden der Befehle SQL an die Datenbank
  3. Verarbeitung der Ergebnisse des Befehls SQL
  4. Schließen der Verbindung

Die Schritte 2 und 3 werden wiederholt ausgeführt, wobei das Schließen der Verbindung erst am Ende der Datenbankauswertung erfolgt. Offene Verbindungen sind begrenzte Ressourcen eines SGBD. Sie müssen sparsam eingesetzt werden. Daher wird stets versucht, die Lebensdauer einer offenen Verbindung zu begrenzen. Im untersuchten Beispiel wird die Verbindung nach jedem Befehl SQL geschlossen. Für den folgenden Befehl SQL wird eine neue Verbindung geöffnet. Das Öffnen und Schließen einer Verbindung ist ressourcenintensiv. Um diesen Aufwand zu verringern, bieten einige SGBD das Konzept von Pools offener Verbindungen an: Bei der Initialisierung der Anwendung werden N Verbindungen geöffnet und dem Pool zugewiesen. Sie bleiben bis zum Ende der Anwendung offen. Wenn die Anwendung eine Verbindung öffnet, erhält sie eine der N bereits offenen Verbindungen aus dem Pool. Wenn sie die Verbindung schließt, wird diese einfach wieder in den Pool zurückgegeben. Der Vorteil dieses Systems besteht darin, dass es für den Entwickler transparent ist: Das Programm muss nicht geändert werden, um den Verbindungspool zu nutzen. Die Konfiguration des Verbindungspools hängt von SGBD ab.

Wir befassen uns zunächst mit der Ausführung der Befehle SQL und Select. Die Methode ExecuteSelect unseres Beispielprogramms lautet wie folgt:


// Ausführung einer SELECT-Abfrage
        static void ExecuteSelect(string connectionString, string requête) {
            // Eventuelle Ausnahmen werden behandelt
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // Verbindung wird hergestellt
                    connexion.Open();
                    // Führt sqlCommand mit einer SELECT-Abfrage aus
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    SqlCeDataReader reader= sqlCommand.ExecuteReader();
                    // Anzeige der Ergebnisse
                    AfficheReader(reader);
                }
            } catch (Exception ex) {
                // Fehlermeldung
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // Reader anzeigen
        static void AfficheReader(IDataReader reader) {
...
        }
  • Zeile 2: Die Methode erhält zwei Parameter:
    • die Verbindungszeichenfolge [connectionString], mit der sie eine Verbindung zur Datenbank herstellen kann
    • den Befehl SQL Select [requête], der über diese Verbindung ausgeführt werden soll
  • Zeile 4: Jeder Vorgang mit einer Datenbank kann eine Ausnahme auslösen, die möglicherweise abgefangen werden soll. Dies ist hier umso wichtiger, als die vom Benutzer eingegebenen Befehle SQL syntaktische Fehler enthalten können. Der Benutzer muss darauf hingewiesen werden. Der gesamte Code befindet sich daher innerhalb eines try/catch-Blocks.
  • Zeile 5: Hier gibt es mehrere Punkte:
    • Die Verbindung zur Datenbank wird mit der Verbindungszeichenfolge [connectionString] initialisiert. Sie ist noch nicht geöffnet. Dies geschieht in Zeile 7.
    • Die Anweisung `using (Ressource) {...}` ist eine syntaktische Erleichterung, die sicherstellt, dass die Ressource Ressource – in diesem Fall eine Verbindung – beim Verlassen des durch using gesteuerten Blocks freigegeben wird.
    • Die Verbindung ist vom proprietären Typ: SqlCeConnection, spezifisch für den SGBD SQL Server Compact.
  • Zeile 7: Die Verbindung wird geöffnet. Zu diesem Zeitpunkt werden die Parameter der Verbindungszeichenfolge verwendet.
  • Zeile 9: Ein Befehl SQL wird über ein proprietäres Objekt SqlCeCommand gesendet. In Zeile 9 wird dieses Objekt mit zwei Informationen initialisiert: der zu verwendenden Verbindung und dem Befehl SQL, der darüber gesendet werden soll. Das Objekt SqlCeCommand dient sowohl zur Ausführung eines Befehls Select als auch eines Befehls Update, Insert oder Delete. Seine Eigenschaften und Methoden wurden in Abschnitt 9.3.3 vorgestellt.
  • Zeile 10: Ein Befehl SQL Select wird über die Methode ExecuteReader desObjekts SqlCeCommand ausgeführt, das ein Objekt IDataReader zurückgibt, dessen Methoden und Eigenschaften in Abschnitt 9.3.3 vorgestellt wurden.
  • Zeile 12: Die Anzeige der Ergebnisse erfolgt über die folgende Methode AfficheReader:

        // Anzeige im Reader
        static void AfficheReader(IDataReader reader) {
            using (reader) {
                // Auswertung der Ergebnisse
                // -- Spalten
                StringBuilder ligne = new StringBuilder();
                int i;
                for (i = 0; i < reader.FieldCount - 1; i++) {
                    ligne.Append(reader.GetName(i)).Append(",");
                }
                ligne.Append(reader.GetName(i));
                Console.WriteLine("\n{0}\n{1}\n{2}\n", "".PadLeft(ligne.Length, '-'), ligne, "".PadLeft(ligne.Length, '-'));
                // -- Daten
                while (reader.Read()) {
                    // Auswertung der aktuellen Zeile
                    ligne = new StringBuilder();
                    for (i = 0; i < reader.FieldCount; i++) {
                        ligne.Append(reader[i].ToString()).Append(" ");
                    }
                    Console.WriteLine(ligne);
                }
            }
}
  • Zeile 2: Die Methode erhält ein Objekt IDataReader. Es ist zu beachten, dass hier eine Schnittstelle und keine spezifische Klasse verwendet wurde.
  • Zeile 3: Die Klausel using wird verwendet, um das Schließen des Objekts IDataReader automatisch zu verwalten.
  • Zeilen 8–10: Hier werden die Spaltennamen der Ergebnistabelle von Select angezeigt. Es handelt sich um die Spalten coli der Abfrage `select col1, col2, ... from table ...`
  • Zeilen 14–21: Die Ergebnistabelle wird durchlaufen und die Werte jeder Zeile der Tabelle werden ausgegeben.
  • Zeile 18: Der Typ der Spalte Nr. i im Ergebnis ist unbekannt, da die abgefragte Tabelle nicht bekannt ist. Daher kann die Syntax reader.GetXXX(i) – wobei XXX der Typ der Spalte Nr. i ist – nicht verwendet werden, da dieser Typ unbekannt ist. Man verwendet daher die Syntax reader.Item[i].ToString(), um die Darstellung der Spalte Nr. i als Zeichenkette zu erhalten. Die Syntax reader.Item[i].ToString() kann zu reader[i].ToString() abgekürzt werden.

9.3.8. Ausführung eines Aktualisierungsauftrags: INSERT, UPDATE, DELETE

Der Code der Methode ExecuteUpdate lautet wie folgt:


        // Ausführung einer Aktualisierungsanforderung
        static void ExecuteUpdate(string connectionString, string requête) {
            // Eventuelle Ausnahmen werden behandelt
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // Verbindung wird hergestellt
                    connexion.Open();
                    // führt sqlCommand mit Aktualisierungsanforderung aus
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // Anzeige des Ergebnisses
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // Fehlermeldung
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
}

Wir haben bereits erwähnt, dass sich die Ausführung eines Abfragebefehls Select nicht von der eines Aktualisierungsbefehls Update, Insert, Delete nur durch die verwendete Methode des Objekts SqlCeCommand unterscheidet: ExecuteReader für Select, ExecuteNonQuery für Update, Insert, Delete. Wir kommentieren im obigen Code nur diese letzte Methode aus:

  • Zeile 10: Der Befehl Update, Insert, Delete wird von der Methode ExecuteNonQuery des Objekts SqlCeCommand ausgeführt. Bei erfolgreichem Abschluss gibt diese Methode die Anzahl der aktualisierten (update), eingefügten (insert) oder gelöschten (delete) Zeilen zurück.
  • Zeile 12: Diese Anzahl von Zeilen wird auf dem Bildschirm angezeigt

Der Leser wird gebeten, sich ein Ausführungsbeispiel für diesen Code in Abschnitt 9.3.2 anzusehen.

9.4. Weitere Konnektoren ADO.NET

Der Code, den wir untersucht haben, ist proprietär: Er hängt vom Namensraum [System.Data.SqlServerCe] ab, der für den SGBD SQL Server Compact vorgesehen ist. Wir werden nun dasselbe Programm mit verschiedenen .NET-Konnektoren erstellen und sehen, was sich ändert.

9.4.1. Konnektor SQL Server 2005

Die verwendete Architektur sieht wie folgt aus:

Die Installation von SQL Server 2005 wird im Anhang unter Abschnitt 1.1 beschrieben.

Wir erstellen ein zweites Projekt in derselben Lösung wie zuvor und richten anschließend die Datenbank SQL Server 2005 ein. Der SGBD SQL Server 2005 muss vor den folgenden Schritten gestartet werden:

  • [1]: Ein neues Projekt in der aktuellen Lösung anlegen und es zum aktuellen Projekt machen.
  • [2]: Eine neue Verbindung erstellen
  • [3]: Verbindungstyp auswählen
  • [4]: SGBD auswählen SQL Server
  • [5]: Ergebnis der vorherigen Auswahl
  • [6]: Verwenden Sie die Schaltfläche [Browse], um anzugeben, wo die Datenbank SQL Server 2005 erstellt werden soll. Die Datenbank ist in einer .mdf-Datei gekapselt.
  • [7]: Wählen Sie das Stammverzeichnis des neuen Projekts aus und nennen Sie die Datenbank [dbarticles.mdf].
  • [8]: Windows-Authentifizierung verwenden.
  • [9]: Bestätigen Sie die Informationsseite
  • [11]: Die Datenbank SQL Server
  • [12]: Eine Tabelle erstellen. Diese entspricht der zuvor erstellten SQL Server Compact-Datenbank.
  • [13]: das Feld [id]
  • [14]: Das Feld [id] ist vom Typ Identity.
  • [15,16]: Das Feld [id] ist der Primärschlüssel
  • [17]: Die übrigen Felder der Tabelle
  • [18]: Geben Sie der Tabelle beim Speichern (Strg+S) den Namen [articles].

Nun müssen wir noch Daten in die Tabelle einfügen:

Wir fügen die Datenbank in das Projekt ein:

Die Projektreferenzen lauten wie folgt:

Die Konfigurationsdatei [App.config] lautet wie folgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="connectString1" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True;" />
     <add name="connectString2" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;Connect Timeout=30;" />
 </connectionStrings>
</configuration>
  • Zeile 4: Die Verbindungszeichenfolge zur Datenbank [dbarticles.mdf] mit Windows-Authentifizierung
  • Zeile 5: Die Verbindungszeichenfolge zur Datenbank [dbarticles.mdf] mit SQL-Server-Authentifizierung. [sa,msde] ist das Paar (Login, Passwort) des Administrators des Servers SQL Server, wie in Abschnitt 1.1 definiert.

Das Programm [Program.cs] verläuft wie folgt:


using System.Data.SqlClient;
...

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {
...
            // Auswertung der Konfigurationsdatei [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString;
            } catch (Exception e) {
...
            }
...
            // Einlesen und Ausführen der über die Tastatur eingegebenen Befehle SQL
...
        }

        // Ausführung einer Aktualisierungsanfrage
        static void ExecuteUpdate(string connectionString, string requête) {
            // Eventuelle Ausnahmen werden behandelt
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // Verbindung wird hergestellt
                    connexion.Open();
                    // Führt sqlCommand mit einer Aktualisierungsanfrage aus
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // Anzeige des Ergebnisses
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
....
            }
        }

        // Ausführung einer Select-Abfrage
        static void ExecuteSelect(string connectionString, string requête) {
            // Eventuelle Ausnahmen werden behandelt
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // Verbindung herstellen
                    connexion.Open();
                    // führt sqlCommand mit einer SELECT-Abfrage aus
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    SqlDataReader reader = sqlCommand.ExecuteReader();
                    // Auswertung der Ergebnisse
...
                }
            } catch (Exception ex) {
...
            }
        }
    }
}
  • Zeile 1: Der Namensraum [System.Data.SqlClient] enthält die Klassen zur Verwaltung einer SQL Server 2005-Datenbank
  • Zeile 24: Die Verbindung ist vom Typ SQLConnection
  • Zeile 28: Das Objekt, das die Befehle SQL kapselt, ist vom Typ SQLCommand
  • Zeile 47: Das Objekt, das das Ergebnis eines SQL-Select-Befehls kapselt, ist vom Typ SQLDataReader

Der Code ist identisch mit dem, der für den SGBD- und den SQL-Server Compact verwendet wird, abgesehen von den Klassennamen. Zur Ausführung kann (Zeile 11) eine der beiden in [App.config] definierten Verbindungszeichenfolgen verwendet werden.

9.4.2. Konnektor MySQL5

Die verwendete Architektur sieht wie folgt aus:

Die Installation von MySQL5 wird im Anhang unter Abschnitt 1.2 beschrieben, die des Konnektors Ado.Net unter Abschnitt 1.2.5.

Wir erstellen ein drittes Projekt in derselben Lösung wie zuvor und fügen ihm die erforderlichen Referenzen hinzu:

  • [1]: das neue Projekt
  • [2]: dem wir Referenzen hinzufügen
  • [3]: DLL, [MySQL.Data], den Konnektor Ado.Net von MySql5 sowie die von [System.Configuration], [4].

Wir legen nun die Datenbank [dbarticles] und ihre Tabelle [articles] an. Der SGBD MySQL5 muss gestartet werden. Außerdem wird der Client [Query Browser] gestartet (siehe Abschnitt 1.2.3).

  • [1]: In [Query Browser] mit der rechten Maustaste in den Bereich [Schemata] [2] klicken, um [3] ein neues Schema zu erstellen; dieser Begriff bezeichnet eine Datenbank.
  • [4]: Die Datenbank wird den Namen [dbarticles] tragen. In [5] ist sie zu sehen. Sie enthält derzeit noch keine Tabellen. Wir werden das folgende Skript SQL ausführen:
/* Auswahl der aktuellen Datenbank */
USE dbarticles;
/* Erstellung der Artikeltabelle */
CREATE TABLE ARTICLES (
    ID            INTEGER PRIMARY KEY AUTO_INCREMENT,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          DOUBLE PRECISION NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* Einfügen von Daten in die Tabelle */
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article1', 100, 10, 1);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article2', 200, 20, 2);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article3', 300, 30, 3);
/* Hinzufügen von Einschränkungen */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  • Zeile 1: Die Datenbank [dbarticles] wird zur aktuellen Datenbank. Die folgenden Befehle SQL werden auf dieser Datenbank ausgeführt.
  • Zeilen 4–10: Definition der Tabelle [ARTICLES]. Zu beachten ist, dass SQL Eigentümer von MySQL ist. Die Spaltentypen und die automatische Generierung des Primärschlüssels (Attribut AUTO_INCREMENT) unterscheiden sich von den bei den Tabellen SGBD und SQL in Server Compact und Express festgestellten Merkmalen.
  • Zeilen 12–14: Einfügen von drei Zeilen
  • Zeilen 16–21: Hinzufügen von Integritätsbeschränkungen für die Spalten.

Dieses Skript wird in [MySQL Query Browser] ausgeführt:

  • In [MySQL Query Browser] und [6] wird das Skript [7] geladen. Dies ist in [8] zu sehen. In [9] wird es ausgeführt.
  • In [10] wurde die Tabelle [articles] angelegt. Man doppelklickt darauf. Daraufhin erscheint das Fenster [11] mit der Abfrage [12], die bereit ist, von [13] ausgeführt zu werden. In [14] sehen wir das Ergebnis der Ausführung. Wir haben tatsächlich die drei erwarteten Zeilen. Es ist zu beachten, dass die Werte des Feldes [ID] automatisch generiert wurden (Attribut AUTO_INCREMENT des Feldes).

Nachdem die Datenbank nun bereit ist, können wir zur Entwicklung der Anwendung in Visual Studio zurückkehren.

In [1] befinden sich das Programm [Program.cs] und die Konfigurationsdatei [App.config]. Diese lautet wie folgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
 </connectionStrings>
</configuration>

In Zeile 4 lauten die Elemente der Verbindungszeichenfolge wie folgt:

  • Server: Name des Rechners, auf dem sich SGBD, MySQL, hier localhost und c.a.d befinden – also der Rechner, auf dem das Programm ausgeführt wird.
  • Database: Der Name der verwalteten Datenbank, hier dbarticles
  • Uid: der Benutzername, hier root
  • Pwd: sein Passwort, hier root. Diese beiden Angaben beziehen sich auf den in Abschnitt 1.2 angelegten Administrator.

Das Programm [Program.cs] ist bis auf die folgenden Details identisch mit dem der vorherigen Versionen:

espace de noms
MySql.Data.MySqlClient
classe Connection
MySqlConnection
classe Command
MySqlCommand
classe DataReader
MySqlDataReader

Das Programm verwendet die Verbindungszeichenfolge mit dem Namen dbArticlesMySql5 aus der Datei [App.config]. Die Ausführung liefert folgende Ergebnisse:

Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.3. Konnektor ODBC

Die verwendete Architektur sieht wie folgt aus:

Der Vorteil der Konnektoren ODBC besteht darin, dass sie den Anwendungen, die sie nutzen, eine Standardschnittstelle bieten. So wird die neue Anwendung mit einem einzigen Code in der Lage sein, mit jedem SGBD zu kommunizieren, das über einen ODBC-, c.a.d- oder die meisten SGBD-Konnektoren verfügt. Die Leistung der ODBC-Konnektoren ist geringer als die der „proprietären“ Konnektoren, die alle Eigenschaften eines bestimmten SGBD nutzen können. Im Gegenzug erhält man eine hohe Flexibilität der Anwendung: Man kann den SGBD wechseln, ohne den Code zu ändern.

Wir betrachten ein Beispiel, bei dem die Anwendung je nach der angegebenen Verbindungszeichenfolge entweder eine MySQL5-Datenbank oder eine SQL Server Express-Datenbank nutzt. Im Folgenden gehen wir davon aus, dass:

  • die Server „SGBD“, „SQL“ (Express-Server) und „MySQL5“ gestartet wurden
  • dass der Treiber ODBC für MySQL5 auf dem Rechner vorhanden ist (siehe Abschnitt 1.2.6). Der Treiber für den SQL Server 2005 ist standardmäßig vorhanden.
  • Die verwendeten Datenbanken sind die aus Abschnitt 9.4.2 für die Datenbank MySQL5 und die aus Abschnitt 9.4.1 für die Datenbank SQL Server Express.

Das neue Visual Studio-Projekt lautet wie folgt:

Oben wurde die in Abschnitt 9.4.1 erstellte Datenbank „SQL Server [dbarticles.mdf]“ in den Projektordner kopiert.

Die Konfigurationsdatei [App.config] lautet wie folgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;" />
     <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • Zeile 4: Die Verbindungszeichenfolge der Quelle „ODBC MySQL5“. Es handelt sich um eine bereits behandelte Zeichenfolge, in der ein neuer Parameter „Driver“ enthalten ist, der den zu verwendenden Treiber „ODBC“ definiert.
  • Zeile 5: Die Verbindungszeichenfolge der Quelle ODBC SQL Server Express. Es handelt sich um die bereits in einem früheren Beispiel verwendete Zeichenfolge, der der Parameter „Driver“ hinzugefügt wurde.

Das Programm [Program.cs] ist bis auf die folgenden Details identisch mit dem der vorherigen Versionen:

espace de noms
System.Data.Odbc
classe Connection
OdbcConnection
classe Command
OdbcCommand
classe DataReader
OdbcDataReader

Das Programm verwendet eine der beiden in der Datei [App.config] definierten Verbindungszeichenfolgen. Die Ausführung liefert folgende Ergebnisse:

Mit der Verbindungszeichenfolge [dbArticlesOdbcSqlServer2005]:

Chaîne de connexion à la base : [Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 500,0000 10 5
2 pompe 10,0000 10 2
3 arc 610,0000 4 1
4 flèches - lot de 6 100,0000 12 20
5 combinaison de plongée 300,0000 8 2
6 Bouteilles d'oxygène 120,0000 10 5

Mit der Verbindungszeichenfolge [dbArticlesOdbcMySql5]:

Chaîne de connexion à la base : [Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.4. Konnektor OLE DB

Die verwendete Architektur sieht wie folgt aus:

Wie die Konnektoren ODBC bieten auch die Konnektoren OLE und DB (Object Linking and Embedding DataBase) eine Standardschnittstelle für die Anwendungen, die sie nutzen. Die Treiber ODBC ermöglichen den Zugriff auf Datenbanken. Die Datenquellen für die Treiber OLE und DB sind vielfältiger: Datenbanken, E-Mail-Systeme, Verzeichnisse usw. Jede Datenquelle kann Gegenstand eines OLE-Treibers DB sein, wenn ein Hersteller dies beschließt. Auf diese Weise erhält man einen standardisierten Zugriff auf eine große Vielfalt an Daten.

Wir betrachten ein Beispiel, bei dem die Anwendung je nach der angegebenen Verbindungszeichenfolge entweder eine ACCESS-Datenbank oder eine SQL Server Express-Datenbank nutzt. Im Folgenden gehen wir davon aus, dass der SGBD SQL Server Express gestartet wurde und dass die verwendete Datenbank die aus dem vorherigen Beispiel ist.

Das neue Visual Studio-Projekt sieht wie folgt aus:

  • in [1]: Der für die Konnektoren OLE und DB erforderliche Namespace ist [System.Data.OleDb] und in der oben genannten Referenz [System.Data] enthalten. Die Datenbank „SQL Server [dbarticles.mdf]“ wurde aus dem vorherigen Projekt kopiert. Die Datenbank „[dbarticles.mdb]“ wurde mit Access erstellt.
  • In [2]: Wie die Datenbank SQL „Server“ verfügt auch die Datenbank ACCESS über die Eigenschaft [Copy to Output Directory=Copy Always], damit sie automatisch in den Ausführungsordner des Projekts kopiert wird.

Die Datenbank ACCESS [dbarticles.mdb] sieht wie folgt aus:

In [1] ist die Struktur der Tabelle [articles] und in [2] deren Inhalt enthalten.

Die Konfigurationsdatei [App.config] lautet wie folgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;"/>
     <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • Zeile 4: Die Verbindungszeichenfolge der Quelle OLE DB ACCESS. Darin findet sich der Parameter „Provider“, der den zu verwendenden Treiber OLE DB sowie den Pfad zur Datenbank definiert
  • Zeile 5: die Verbindungszeichenfolge zur OLE DB Server Express-Quelle.

Das Programm [Program.cs] entspricht bis auf die folgenden Details dem Programm der Vorgängerversionen:

espace de noms
System.Data.OleDb
classe Connection
OleDbConnection
classe Command
OleDbCommand
classe DataReader
OleDbDataReader

Das Programm verwendet eine der beiden in der Datei [App.config] definierten Verbindungszeichenfolgen. Die Ausführung liefert mit der Verbindungszeichenfolge [dbArticlesOleDbAccess] folgende Ergebnisse:

Chaîne de connexion à la base : [Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 1202 5 2
2 arc 5000 10 2
3 canoé 1502 12 6
4 fusil 3000 10 2
5 skis nautiques 1800 5 2
6 essai3 3 3 3
7 cachalot 200000 1 0
8 léopard 500000 1 1
9 panthère 800000 1 1

9.4.5. Generischer Konnektor

Die verwendete Architektur sieht wie folgt aus:

Wie die Konnektoren ODBC, OLE und DB bietet auch der generische Konnektor den Anwendungen, die ihn nutzen, eine Standardschnittstelle, verbessert jedoch die Leistung, ohne dabei an Flexibilität einzubüßen. Tatsächlich basiert der generische Konnektor auf den proprietären Konnektoren der Serie SGBD. Die Anwendung nutzt Klassen des generischen Konnektors. Diese Klassen dienen als Vermittler zwischen der Anwendung und dem proprietären Konnektor.

Wenn die Anwendung beispielsweise eine Verbindung zum generischen Konnektor anfordert, gibt dieser ihr eine Instanz von IDbConnection zurück, die in Abschnitt 9.3.3 beschriebene Verbindungsschnittstelle, die je nach Art der an sie gerichteten Anfrage durch eine Klasse MySQLConnection oder SQLConnection implementiert wird. Man sagt, dass der generische Konnektor über Factory-Klassen verfügt: Man verwendet eine Factory-Klasse, um ihn aufzufordern, Objekte zu erstellen und Referenzen (Zeiger) darauf zurückzugeben. Daher auch sein Name (Factory = Fabrik, Fabrik zur Erzeugung von Objekten).

Es gibt keinen generischen Konnektor für alle SGBD (Stand: April 2008). Um herauszufinden, welche auf einem Rechner installiert sind, kann man das folgende Programm verwenden:


using System;
using System.Data;
using System.Data.Common;

namespace Chap7 {
    class Providers {
        public static void Main() {
            DataTable dt = DbProviderFactories.GetFactoryClasses();
            foreach (DataColumn col in dt.Columns) {
                Console.Write("{0}|", col.ColumnName);
            }
            Console.WriteLine("\n".PadRight(40, '-'));
            foreach (DataRow row in dt.Rows) {
                foreach (object item in row.ItemArray) {
                    Console.Write("{0}|", item);
                }
                Console.WriteLine("\n".PadRight(40, '-'));
            }
        }
    }
}
  • Zeile 8: Die statische Methode [DbProviderFactories.GetFactoryClasses()] gibt die Liste der installierten generischen Konnektoren in Form einer im Speicher abgelegten Datenbanktabelle (DataTable) zurück.
  • Zeilen 9–11: Zeigen die Spaltennamen der Tabelle dt an:
    • dt.Columns ist die Liste der Spalten der Tabelle. Eine Spalte C hat den Typ DataColumn
    • [DataColumn]. ColumnName ist der Name der Spalte
  • Zeilen 13–18: zeigen die Zeilen der Tabelle an. dt:
    • dt.Rows ist die Liste der Zeilen der Tabelle. Eine Zeile L hat den Typ DataRow
    • [DataRow].ItemArray ist ein Array von Objekten, wobei jedes Objekt eine Spalte der Zeile darstellt

Das Ergebnis der Ausführung auf meinem Rechner lautet wie folgt:

Name|Description|InvariantName|AssemblyQualifiedName|
---------------------------------------
Odbc Data Provider|.Net Framework Data Provider for Odbc|System.Data.Odbc|System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OleDb Data Provider|.Net Framework Data Provider for OleDb|System.Data.OleDb|System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OracleClient Data Provider|.Net Framework Data Provider for Oracle|System.Data.OracleClient|System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
SqlClient Data Provider|.Net Framework Data Provider for SqlServer|System.Data.SqlClient|System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
Microsoft SQL Server Compact Data Provider|.NET Framework Data Provider for Microsoft SQL Server Compact|System.Data.SqlServerCe.3.5|System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91|
---------------------------------------
MySQL Data Provider|.Net Framework Data Provider for MySQL|MySql.Data.MySqlClient|MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=5.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d|
  • Zeile 1: Die Tabelle hat vier Spalten. Die ersten drei sind für uns hier am nützlichsten.

Die folgende Anzeige zeigt, dass folgende generische Konnektoren zur Verfügung stehen:

Name
ID
Odbc Data Provider
System.Data.Odbc
OleDb Data Provider
System.Data.OleDb
OracleClient Data Provider
System.Data.OracleClient
SqlClient Data Provider
System.Data.SqlClient
Microsoft SQL Server Compact Data Provider
System.Data.SqlServerCe.3.5
MySQL Data Provider
MySql.Data.MySqlClient

Ein generischer Konnektor ist in einem C#-Programm über seine Kennung zugänglich.

Wir betrachten ein Beispiel, in dem die Anwendung die verschiedenen Datenbanken nutzt, die wir bisher erstellt haben. Die Anwendung erhält zwei Parameter:

  • Ein erster Parameter gibt den verwendeten Typ „SGBD“ an, damit die richtige Klassenbibliothek verwendet wird
  • Der zweite Parameter gibt die zu verwaltende Datenbank über eine Verbindungszeichenfolge an.

Das neue Visual Studio-Projekt sieht wie folgt aus:

  • in [1]: Der für generische Konnektoren erforderliche Namespace ist [System.Data.common], der in der Referenz [System.Data] enthalten ist.

Die Konfigurationsdatei [App.config] lautet wie folgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
        <add name="dbArticlesSqlServer" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
        <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;Option=3;" />
        <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;Persist Security Info=True"/>
    </connectionStrings>
    <appSettings>
        <add key="factorySqlServerCe" value="System.Data.SqlServerCe.3.5"/>
        <add key="factoryMySql" value="MySql.Data.MySqlClient"/>
        <add key="factorySqlServer" value="System.Data.SqlClient"/>
        <add key="factoryOdbc" value="System.Data.Odbc"/>
        <add key="factoryOleDb" value="System.Data.OleDb"/>
    </appSettings>
</configuration>
  • Zeilen 3–11: Die Verbindungszeichenfolgen der verschiedenen verwendeten Datenbanken.
  • Zeilen 13–17: Die Namen der zu verwendenden generischen Konnektoren

Das Programm [Program.cs] lautet wie folgt:


...
using System.Data.Common;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // Konsolenanwendung – führt über die Tastatur eingegebene SQL-Abfragen aus
            // in einer Datenbank, deren Verbindungszeichenfolge sowie der Name des zugehörigen SGBD-Konnektors aus einer Konfigurationsdatei abgerufen werden

            // Überprüfung der Parameter
            if (args.Length != 2) {
                Console.WriteLine("Syntaxe : pg factory connectionString");
                return;
            }

            // Auswertung der Konfigurationsdatei
            string factory = null;
            string connectionString = null;
            DbProviderFactory connecteur = null;
            try {
                // Factory
                factory = ConfigurationManager.AppSettings[args[0]];
                // Verbindungskette
                connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString;
                // Es wird ein generischer Stecker für das SGBD abgerufen
                connecteur = DbProviderFactories.GetFactory(factory);
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // Anzeigen
            Console.WriteLine("Provider factory : [{0}]\n", factory);
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

...
                // Ausführung der Abfrage
                if (champs[0] == "select") {
                    ExecuteSelect(connecteur,connectionString, requête);
                } else
                    ExecuteUpdate(connecteur, connectionString, requête);
            }
        }

        // Ausführung einer Aktualisierungsabfrage
        static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requête) {
            // Eventuelle Ausnahmen werden behandelt
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // Verbindungskonfiguration
                    connexion.ConnectionString = connectionString;
                    // Verbindung wird hergestellt
                    connexion.Open();
                    // Befehlskonfiguration 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // Abfrage ausführen
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // Anzeige des Ergebnisses
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // Fehlermeldung
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // Ausführung einer SELECT-Abfrage
        static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requête) {
            // Eventuelle Ausnahmen werden behandelt
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // Verbindungskonfiguration
                    connexion.ConnectionString = connectionString;
                    // Verbindung wird hergestellt
                    connexion.Open();
                    // Befehlskonfiguration 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // Abfrage ausführen
                    DbDataReader reader = sqlCommand.ExecuteReader();
                    // Anzeige der Ergebnisse
...
                }
            } catch (Exception ex) {
                // Fehlermeldung
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
    }
}
  • Zeilen 12–14: Die Anwendung erhält zwei Parameter: den Namen des generischen Konnektors sowie die Verbindungszeichenfolge zur Datenbank in Form von Schlüsseln aus der Datei [App.config].
  • Zeilen 23, 25: Aus der Datei [App.config] werden der Name des generischen Konnektors sowie die Verbindungszeichenfolge abgerufen.
  • Zeile 27: Der generische Konnektor wird instanziiert. Ab diesem Zeitpunkt ist er einem bestimmten SGBD zugeordnet.
  • Zeilen 39–43: Die Ausführung des über die Tastatur eingegebenen Befehls SQL wird an zwei Methoden delegiert, denen Folgendes übergeben wird:
    • die auszuführende Abfrage
    • die Verbindungszeichenfolge, die die Datenbank identifiziert, auf der die Abfrage ausgeführt wird
    • den generischen Konnektor, der die Klassen identifiziert, die für die Kommunikation mit dem SGBD verwendet werden, der die Datenbank verwaltet.
  • Zeilen 50–54: Mit der Methode CreateConnection (Zeile 50) des generischen Konnektors wird eine Verbindung hergestellt und anschließend mit der Verbindungszeichenfolge der zu verwaltenden Datenbank konfiguriert (Zeile 52). Anschließend wird die Verbindung geöffnet (Zeile 54).
  • Zeilen 56–58: Das für die Ausführung des Befehls SQL erforderliche Objekt Command wird mit der Methode CreateCommand des generischen Konnektors angelegt. Anschließend wird es mit dem Text des auszuführenden Auftrags SQL (Zeile 57) und der Verbindung, über die dieser ausgeführt werden soll (Zeile 58), konfiguriert.
  • Zeile 60: Der Aktualisierungsbefehl SQL wird ausgeführt
  • Zeilen 74–87: Hier findet sich ein ähnlicher Code. Die Neuerung findet sich in Zeile 84. Das durch die Ausführung des Befehls Select erhaltene Objekt Reader ist vom Typ DbDataReader, der wie die Objekte OleDbDataReader, OdbcDataReader, … verwendet wird, die wir bereits kennengelernt haben.

Hier sind einige Beispiele für die Ausführung.

Mit der Basis MySQL5:

Wir öffnen die Eigenschaftenseite des Projekts [1] und wählen die Registerkarte [Debug] [2] aus. In [3] den Konnektorschlüssel aus Zeile 14 von [App.config]. In [4] der Schlüssel der Verbindungszeichenfolge aus Zeile 6 von [App.config]. Die Ergebnisse der Ausführung lauten wie folgt:

Provider factory : [MySql.Data.MySqlClient]
Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

Mit der Datenbank SQL Server Compact:

In [1] ist der Connector-Schlüssel in Zeile 13 von [App.config]. In [2] der Schlüssel der Verbindungszeichenfolge aus Zeile 4 von [App.config]. Die Ergebnisse der Ausführung lauten wie folgt:

Provider factory : [System.Data.SqlServerCe.3.5]
Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

Der Leser wird gebeten, die anderen Datenbanken zu testen.

9.4.6. Welchen Konnektor soll man wählen?

Kehren wir zur Architektur einer datenbankgestützten Anwendung zurück:

Wir haben verschiedene Arten von Konnektoren betrachtet:

  • Proprietäre Konnektoren sind am leistungsfähigsten, machen die Schicht [dao] jedoch von proprietären Klassen abhängig. Ein Wechsel des SGBD erfordert eine Änderung der Schicht [dao].
  • Die Konnektoren ODBC, OLE und DB ermöglichen die Arbeit mit mehreren Datenbanken, ohne die Schicht [dao] ändern zu müssen. Sie sind weniger leistungsfähig als die proprietären Konnektoren.
  • Der generische Konnektor stützt sich auf die proprietären Konnektoren und bietet gleichzeitig eine Standardschnittstelle zur Schicht [dao].

Es scheint also, als sei der generische Konnektor der ideale Konnektor. In der Praxis gelingt es dem generischen Konnektor jedoch nicht, alle Besonderheiten eines SGBD hinter einer Standardschnittstelle zu verbergen. Im folgenden Abschnitt werden wir uns mit dem Konzept der parametrisierten Abfrage befassen. Mit dem SQL-Server sieht eine parametrisierte Abfrage wie folgt aus:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Mit MySQL5 würde dieselbe Abfrage wie folgt geschrieben:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Es besteht also ein Unterschied in der Syntax. Die in Abschnitt 9.3.3 beschriebene Eigenschaft der Schnittstelle IDbCommand, die sich auf die Parameter bezieht, lautet wie folgt:

Parameters
die Liste der Parameter eines konfigurierten Befehls SQL. Der Befehl „update articles set prix=prix*1.1 where id=@id“ hat den Parameter @id.

Die Eigenschaft Parameters ist vom Typ IDataParameterCollection, einer Schnittstelle. Sie repräsentiert die Gesamtheit der Parameter des Befehls SQL CommandText. Die Eigenschaft Parameters verfügt über eine Methode Add zum Hinzufügen von Parametern vom Typ IDataParameter, bei dem es sich wiederum um eine Schnittstelle handelt. Diese weist folgende Eigenschaften auf:

  • ParameterName: Name des Parameters
  • DbType: der Typ SQL des Parameters
  • Value: Der dem Parameter zugewiesene Wert
  • ...

Der Typ IDataParameter eignet sich gut für die Parameter der Anordnung SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

da dort benannte Parameter vorkommen. Die Eigenschaft ParameterName kann verwendet werden.

Der Typ IDataParameter ist für den Auftrag SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

da die Parameter nicht benannt sind. In diesem Fall wird die Reihenfolge berücksichtigt, in der die Parameter in die Sammlung [IDbCommand.Parameters] eingefügt wurden. In diesem Beispiel müssen die 4 Parameter in der Reihenfolge nom, prix, stockactuel, stockminimum eingefügt werden. Bei einer Abfrage mit benannten Parametern spielt die Reihenfolge, in der die Parameter hinzugefügt werden, keine Rolle. Letztendlich kann der Entwickler den Wert „SGBD“, den er bei der Initialisierung der Parameter einer parametrisierten Abfrage verwendet, nicht vollständig außer Acht lassen. Dies ist eine der derzeitigen Einschränkungen des generischen Konnektors.

Es gibt frameworks, die diese Einschränkungen überwinden und darüber hinaus neue Funktionen in die [dao]-Schicht einbringen:

Ein Framework ist eine Sammlung von Klassenbibliotheken, die darauf abzielen, eine bestimmte Art der Anwendungsarchitektur zu vereinfachen. Es gibt mehrere Frameworks, die das Schreiben von [dao]-Schichten ermöglichen, die sowohl leistungsstark als auch unempfindlich gegenüber Änderungen an SGBD sind:

  • Spring.Net [http://www.springframework.net/], das bereits in diesem Dokument vorgestellt wurde, bietet das Äquivalent des untersuchten generischen Konnektors ohne dessen Einschränkungen sowie verschiedene Funktionen, die den Zugriff auf die Daten vereinfachen. Es gibt eine Java-Version.
  • iBatis.Net [http://ibatis.apache.org] ist älter und umfangreicher als Spring.Net. Es gibt eine Java-Version.
  • NHibernate [http://www.hibernate.org/] ist eine Portierung der in der Java-Welt sehr bekannten Java-Version von Hibernate. NHibernate ermöglicht es der Schicht [dao], mit SGBD zu kommunizieren, ohne Befehle von SQL zu senden. Die Schicht [dao] arbeitet mit Hibernate-Objekten. Eine Abfragesprache HBL (Hibernate Query Language) ermöglicht es, Abfragen an die von Hibernate verwalteten Objekte zu stellen. Diese Objekte senden die Befehle SQL. Hibernate kann sich an die proprietären SQL-Befehle der SGBD-Befehle anpassen.
  • LINQ (INtegrated-Abfragesprache), integriert in Version 3.5 .NET und verfügbar in C# 2008. LINQ tritt in die Fußstapfen von NHibernate, doch derzeit (Mai 2008) wird nur der SGBD SQL-Server unterstützt. Dies dürfte sich im Laufe der Zeit ändern. LINQ geht über NHibernate hinaus: Seine Abfragesprache ermöglicht es, drei verschiedene Arten von Datenquellen standardmäßig abzufragen:
    • Objektsammlungen (LINQ to Objects)
    • eine XML-Datei (LINQ to XML)
    • eine Datenbank (LINQ to SQL)

Diese Frameworks werden in diesem Dokument nicht behandelt. Es wird jedoch dringend empfohlen, sie in professionellen Anwendungen zu verwenden.

9.5. Parametrisierte Abfragen

Im vorigen Abschnitt haben wir parametrisierte Abfragen angesprochen. Wir stellen sie hier anhand eines Beispiels für den SGBD SQL Server Compact vor. Das Projekt sieht wie folgt aus

  • in [1], das Projekt. Es werden nur [App.config], [Article.cs] und [Parametres.cs] verwendet. Zu beachten ist auch die Basis SQL Server sowie [dbarticles.sdf].
  • In [2] ist das Projekt so konfiguriert, dass es [Parametres.cs]
  • In [3] sind die Projektverweise

Die Konfigurationsdatei [App.config] definiert die Verbindungszeichenfolge zur Datenbank:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
    </connectionStrings>
</configuration>

Die Datei [Article.cs] definiert eine Klasse [Article]. Ein Objekt Article wird verwendet, um die Informationen einer Zeile der Tabelle ARTICLES aus der Datenbank [dbarticles.sdf] zu kapseln:


namespace Chap7 {
    class Article {
        // Eigenschaften
        public int Id { get; set; }
        public string Nom { get; set; }
        public decimal Prix { get; set; }
        public int StockActuel { get; set; }
        public int StockMinimum { get; set; }

        // Konstruktoren
        public Article() { 
        }

        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }

    }
}

Die Anwendung [Parametres.cs] führt die parametrisierten Abfragen aus:


using System;
using System.Data.SqlServerCe;
using System.Text;
using System.Data;
using System.Configuration;

namespace Chap7 {
    class Parametres {
        static void Main(string[] args) {

            // Auswertung der Konfigurationsdatei
            string connectionString = null;
            try {
                // Verbindungszeichenfolge
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // Anzeigen
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // Erstellung einer Artikeltabelle
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i-1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // Eventuelle Ausnahmen werden behandelt
            try {

                // Vorhandene Artikel aus der Datenbank löschen
                ExecuteUpdate(connectionString, "delete from articles");

                // Anzeige der Artikel aus der Tabelle
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // Die Artikeltabelle wird in die Datenbank eingefügt
                InsertArticles(connectionString, articles);

                // Die Artikel aus der Tabelle werden angezeigt
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // Fehlermeldung
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // Einfügen einer Artikeltabelle
        static void InsertArticles(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // Verbindung wird hergestellt
                connexion.Open();
                // Konfiguration des Befehls
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom",SqlDbType.NVarChar,30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // Auftrag wird zusammengestellt
                sqlCommand.Prepare();
                // Einfügen der Zeilen
                for (int i = 0; i < articles.Length; i++) {
                    // Initialisierung der Parameter
                    sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                    sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                    sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                    sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                    // Abfrageausführung
                    sqlCommand.ExecuteNonQuery();
                }
            }
        }

        // Ausführung einer Aktualisierungsabfrage
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // Ausführung einer SELECT-Abfrage
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // Anzeige im Reader
        static void AfficheReader(IDataReader reader) {
...
    }
}

Neu im Vergleich zu dem zuvor Behandelten ist die Prozedur [InsertArticles] in den Zeilen 51–75:

  • Zeile 51: Die Prozedur erhält zwei Parameter:
    • die Verbindungszeichenfolge connectionString, die es der Prozedur ermöglicht, eine Verbindung zur Datenbank herzustellen
    • ein Array von Objekten Article, die zur Tabelle Articles der Datenbank hinzugefügt werden müssen
  • Zeile 56: Die Abfrage zum Einfügen eines Objekts [Article]. Sie hat vier Parameter:
    • @nom: der Name des Artikels
    • @prix: sein Preis
    • @sa: sein aktueller Lagerbestand
    • @sm: der Mindestbestand

Die Syntax dieser parametrisierten Abfrage ist eine proprietäre Syntax von SQL Server Compact. Im vorigen Absatz haben wir gesehen, dass die Syntax bei MySQL5 wie folgt aussehen würde:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Bei SQL Server Compact muss jedem Parameter das Zeichen @ vorangestellt werden. Die Namen der Parameter können frei gewählt werden.

  • Zeilen 58–61: Hier werden die Eigenschaften der vier Parameter definiert und nacheinander zur Parameterliste des Objekts SqlCeCommand hinzugefügt, das den auszuführenden Auftrag SQL kapselt.

Hier wird die Methode [SqlCeCommand].Parameters.Add verwendet, die über sechs Signaturen verfügt. Wir verwenden die beiden folgenden:

Add(string parameterName, SQLDbType type)

fügt den Parameter mit dem Namen parameterName hinzu und konfiguriert ihn. Dieser Name muss einer der Namen der konfigurierten parametrisierten Abfrage sein: (@name, ...). type bezeichnet den Typ SQL der Spalte, auf die sich der Parameter bezieht. Es stehen zahlreiche Typen zur Verfügung, darunter die folgenden:

Typ SQL
Typ C#
Kommentar
BigInt
Int64
 
DateTime
DateTime
 
Decimal
Decimal
 
Float
Doppelt
 
Int
Int32
 
Money
Dezimal
 
NChar
Zeichenkette
Zeichenkette mit fester Länge
NVarChar
Zeichenkette
Zeichenkette mit variabler Länge
Real
Single
 

Add(string parameterName, SQLDbType type, int size)

Der dritte Parameter „size“ legt die Spaltenbreite fest. Diese Information ist nur für bestimmte Typen von Nutzen, z. B. den Typ SQL oder den Typ NVarChar.

  • Zeile 63: Die parametrisierte Abfrage wird kompiliert. Man spricht auch davon, dass sie vorbereitet wird, daher der Name der Methode. Dieser Vorgang ist nicht zwingend erforderlich. Er dient der Leistungsoptimierung. Wenn ein SGBD einen Befehl SQL ausführt, führt er vor der Ausführung bestimmte Optimierungsschritte durch. Eine parametrisierte Abfrage ist dafür vorgesehen, mehrmals mit unterschiedlichen Parametern ausgeführt zu werden. Der Text der Abfrage selbst ändert sich dabei nicht. Die Optimierungsarbeit muss daher nur einmal durchgeführt werden. Bestimmte SGBD verfügen über die Möglichkeit, parametrisierte Abfragen „vorzubereiten“ oder zu „kompilieren“. Für diese Abfrage wird dann ein Ausführungsplan definiert. Dies ist die bereits erwähnte Optimierungsphase. Nach der Kompilierung wird die Abfrage wiederholt ausgeführt, wobei jedes Mal neue Parameter zum Einsatz kommen, der Ausführungsplan jedoch derselbe bleibt.

Die Kompilierung ist nicht der einzige Vorteil parametrisierter Abfragen. Nehmen wir noch einmal die untersuchte Abfrage:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Man könnte den Text der Abfrage programmgesteuert zusammenstellen:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('"+nom+"',"+prix+","+sa+","+sm+")";

Wenn oben (name,preis,sa,sm) den Wert („article1“, 100, 10, 1) hat, lautet die vorherige Abfrage:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('article1',100,10,1)";

Wenn nun (Name, Preis, sa, sm) den Wert („Artikel1“, 100, 10, 1) hat, lautet die vorherige Abfrage:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('l'article1',100,10,1)";

und wird aufgrund des Apostrophs im Namen „l'article1“ syntaktisch falsch. Wenn „nom“ aus einer Benutzereingabe stammt, bedeutet dies, dass wir prüfen müssen, ob die Eingabe keine Apostrophe enthält, und falls doch, diese neutralisieren müssen. Diese Neutralisierung hängt von SGBD ab. Der Vorteil der vorbereiteten Abfrage besteht darin, dass sie diese Arbeit selbst übernimmt. Allein diese Erleichterung rechtfertigt bereits die Verwendung einer vorbereiteten Abfrage.

  • Zeilen 65–73: Die Artikel der Tabelle werden nacheinander eingefügt
  • Zeilen 67–70: Jeder der vier Parameter der Abfrage erhält seinen Wert über seine Eigenschaft Value.
  • Zeile 72: Die nun vollständige Einfügeabfrage wird wie gewohnt ausgeführt.

Hier ein Ausführungsbeispiel:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------


------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

117 article1 100 10 1
118 article2 200 20 2
119 article3 300 30 3
120 article4 400 40 4
121 article5 500 50 5
  • Zeile 3: Meldung nach dem Löschen aller Zeilen der Tabelle
  • Zeilen 5–7: zeigen, dass die Tabelle leer ist
  • Zeilen 10–18: zeigen die Tabelle nach dem Einfügen der 5 Artikel

9.6. Transactions

9.6.1. Allgemeines

Eine Transaktion ist eine Abfolge von Befehlen SQL, die „atomar“ ausgeführt wird:

  • Entweder sind alle Operationen erfolgreich
  • oder eine davon schlägt fehl, woraufhin alle vorhergehenden rückgängig gemacht werden

Letztendlich wurden die Vorgänge einer Transaktion entweder alle erfolgreich durchgeführt oder gar keiner. Wenn der Benutzer selbst die Kontrolle über die Transaktion hat, bestätigt er eine Transaktion mit einem Befehl COMMIT oder bricht sie mit einem Befehl ROLLBACK ab.

In unseren vorangegangenen Beispielen haben wir keine Transaktion verwendet. Und doch gab es eine, denn in einem SGBD wird ein Befehl SQL immer innerhalb einer Transaktion ausgeführt. Wenn der Client .NET selbst keine explizite Transaktion startet, verwendet der SGBD eine implizite Transaktion. Dabei gibt es zwei häufige Fälle:

  1. Jeder einzelne Befehl SQL ist Gegenstand einer Transaktion, die von SGBD vor dem Befehl initiiert und anschließend abgeschlossen wird. Man spricht hier vom Autocommit-Modus. Es ist also so, als würde der Client .NET für jeden Befehl SQL eine Transaktion durchführen.
  2. SGBD befindet sich nicht im Autocommit-Modus und startet eine implizite Transaktion mit dem ersten Befehl SQL, den der Client .NET außerhalb einer Transaktion sendet, und überlässt es dem Client, diese zu schließen. Alle vom Client .NET gesendeten Befehle SQL sind dann Teil der impliziten Transaktion. Diese kann durch verschiedene Ereignisse beendet werden: Der Client schließt die Verbindung, startet eine neue Transaktion usw. – man befindet sich dann jedoch in einer von SGBD abhängigen Situation. Dieser Modus sollte vermieden werden.

Der Standardmodus wird in der Regel durch die Konfiguration von SGBD festgelegt. Einige SGBD befinden sich standardmäßig im Autocommit-Modus, andere nicht. SQLServer Compact befindet sich standardmäßig im Autocommit-Modus.

Die Befehle SQL der verschiedenen Benutzer werden gleichzeitig in parallel laufenden Transaktionen ausgeführt. Die von einer Transaktion durchgeführten Operationen können sich auf die einer anderen Transaktion auswirken. Man unterscheidet vier Isolationsstufen zwischen den Transaktionen der verschiedenen Benutzer:

  • Uncommitted Read
  • Committed Read
  • Repeatable Read
  • Serialisierbar

Uncommitted Read

Dieser Isolationsmodus wird auch als „Dirty Read“ bezeichnet. Hier ein Beispiel dafür, was in diesem Modus passieren kann:

  1. Ein Benutzer mit dem Namen U1 startet eine Transaktion für die Tabelle T
  2. Ein Benutzer U2 startet eine Transaktion auf derselben Tabelle T
  3. Der Benutzer U1 ändert Zeilen in der Tabelle T, bestätigt diese jedoch noch nicht
  4. Der Benutzer U2 „sieht“ diese Änderungen und trifft Entscheidungen auf der Grundlage dessen, was er sieht
  5. Der Benutzer bricht seine Transaktion mit einem ROLLBACK ab

Man sieht, dass der Benutzer U2 in Schritt 4 eine Entscheidung auf der Grundlage von Daten getroffen hat, die sich später als falsch herausstellen werden.

Committed Read

Dieser Isolationsmodus vermeidet die oben beschriebene Gefahr. In diesem Modus wird der Benutzer U2 in Schritt 4 die vom Benutzer U1 an der Tabelle T vorgenommenen Änderungen nicht „sehen“. Er wird sie erst sehen, nachdem U1 seine Transaktion mit „COMMIT“ abgeschlossen hat.

In diesem Modus, der auch als „Unrepeatable Read“ bezeichnet wird, können dennoch folgende Situationen auftreten:

  1. Ein Benutzer U1 beginnt eine Transaktion auf einer Tabelle T
  2. Ein Benutzer U2 startet eine Transaktion auf derselben Tabelle T
  3. Der Benutzer U2 führt einen SELECT aus, um den Durchschnitt einer Spalte C der Zeilen von T zu ermitteln, die eine bestimmte Bedingung erfüllen
  4. Der Benutzer U1 ändert (UPDATE) bestimmte Werte in der Spalte C von T und bestätigt sie (COMMIT)
  5. Der Benutzer U2 führt denselben Vorgang SELECT wie in Schritt 3 erneut durch. Er wird feststellen, dass sich der Durchschnitt der Spalte C aufgrund der von U1 vorgenommenen Änderungen geändert hat.

Nun sieht der Benutzer U2 nur die von U1 „bestätigten“ Änderungen. Doch während er sich noch in derselben Transaktion befindet, liefern zwei identische Operationen (3 und 5) unterschiedliche Ergebnisse. Der Begriff „Unrepeatable Read“ bezeichnet diese Situation. Das ist ärgerlich für jemanden, der sich ein stabiles Abbild der Tabelle T wünscht.

Repeatable Read

In diesem Isolationsmodus ist einem Benutzer garantiert, dass er bei seinen Lesevorgängen aus der Datenbank dieselben Ergebnisse erhält, solange er in derselben Transaktion bleibt. Er arbeitet mit einer Momentaufnahme, in der Änderungen, die durch andere Transaktionen vorgenommen wurden – selbst wenn diese bestätigt wurden –, niemals berücksichtigt werden. Er sieht diese erst, wenn er selbst seine Transaktion mit einem COMMIT oder ROLLBACK abschließt.

Dieser Isolationsmodus ist jedoch noch nicht perfekt. Nach dem oben genannten Schritt 3 sind die vom Benutzer U2 abgefragten Zeilen gesperrt. Bei Vorgang 4 kann der Benutzer U1 die Werte in Spalte C dieser Zeilen nicht ändern (UPDATE). Er kann jedoch Zeilen hinzufügen (INSERT). Wenn einige der hinzugefügten Zeilen die in Schritt 3 geprüfte Bedingung erfüllen, ergibt Schritt 5 aufgrund der hinzugefügten Zeilen einen anderen Durchschnittswert als den in Schritt 3 ermittelten. Diese Zeilen werden manchmal als „Geisterzeilen“ bezeichnet.

Um dieses neue Problem zu lösen, muss auf die Isolationsstufe „Serializable“ umgestellt werden.

Serializable

In diesem Isolationsmodus sind die Transaktionen vollständig voneinander abgeschottet. Er stellt sicher, dass das Ergebnis zweier gleichzeitig durchgeführter Transaktionen dasselbe ist, als wären sie nacheinander ausgeführt worden. Um dieses Ergebnis zu erreichen, wird der Benutzer U1 bei Operation 4 daran gehindert, Zeilen hinzuzufügen, die das Ergebnis von SELECT des Benutzers U1 verändern würden. Eine Fehlermeldung weist ihn darauf hin, dass das Einfügen nicht möglich ist. Dies wird erst möglich, wenn der Benutzer U2 seine Transaktion bestätigt hat.

Die vier Transaktionsisolationsstufen SQL sind nicht in allen SGBD verfügbar. Die Standard-Isolationsstufe ist in der Regel „Committed Read“. Die gewünschte Isolationsstufe für eine Transaktion kann bei der Erstellung einer expliziten Transaktion durch einen Kunden .NET explizit angegeben werden.

9.6.2. Die Transaktionsverwaltung API

Eine Verbindung implementiert die in Abschnitt 9.3.3 vorgestellte Schnittstelle IDbConnection. Diese Schnittstelle verfügt über die folgende Methode:

BeginTransaction
M
startet eine Transaktion.

Diese Methode hat zwei Signaturen:

  1. IDbTransaction BeginTransaction(): Startet eine Transaktion und gibt das Objekt IDbTransaction zurück, mit dem die Transaktion gesteuert werden kann
  2. IDbTransaction BeginTransaction(IsolationLevel level): legt zusätzlich die gewünschte Sicherheitsstufe für die Transaktion fest. level nimmt seine Werte aus der folgenden Aufzählung:
ReadUncommitted
Die Transaktion kann Daten lesen, die von einer anderen Transaktion geschrieben wurden, die diese noch nicht bestätigt hat – zu vermeiden
ReadCommitted
Die Transaktion kann keine Daten lesen, die von einer anderen Transaktion geschrieben wurden und von ihr noch nicht bestätigt wurden. Die zweimal hintereinander in der Transaktion gelesenen Daten können sich jedoch ändern (non-repeatable reads), da eine andere Transaktion sie in der Zwischenzeit möglicherweise geändert hat (die gelesenen Zeilen sind nicht gesperrt – nur die aktualisierten Zeilen sind gesperrt). Außerdem könnte eine andere Transaktion Zeilen hinzugefügt haben (Geisterzeilen), die beim zweiten Lesevorgang berücksichtigt werden.
RepeatableRead
Die von der Transaktion gelesenen Zeilen werden ebenso wie die aktualisierten Zeilen gesperrt. Dadurch wird verhindert, dass eine andere Transaktion sie ändert. Das Hinzufügen von Zeilen wird dadurch jedoch nicht verhindert.
Serializable
Die von der Transaktion genutzten Tabellen werden gesperrt, wodurch das Hinzufügen neuer Zeilen durch eine andere Transaktion verhindert wird. Es ist, als ob die Transaktion allein wäre. Dies beeinträchtigt die Leistung, da die Transaktionen nicht mehr parallel ablaufen.
Snapshot
Die Transaktion arbeitet mit einer zum Zeitpunkt T erstellten Kopie der Daten. Wird verwendet, wenn die Transaktion schreibgeschützt ist. Liefert das gleiche Ergebnis wie serializable, vermeidet jedoch deren Kosten.

Sobald die Transaktion gestartet ist, wird sie vom Objekt vom Typ IDbTransaction gesteuert, einer Schnittstelle, von der wir die folgenden Eigenschaften P und Methoden M verwenden werden:

Name
Typ
Rolle
Connection
P
die Verbindung IDbConnection, die die Transaktion unterstützt
Commit
M
die Transaktion validiert – die Ergebnisse der in der Transaktion erteilten Aufträge SQL werden in die Datenbank kopiert.
Rollback
M
macht die Transaktion ungültig – die Ergebnisse der in der Transaktion erteilten Befehle SQL werden nicht in die Datenbank kopiert.

9.6.3. Das Beispielprogramm

Wir greifen das vorherige Projekt wieder auf und widmen uns nun dem Programm [Transactions.cs]:

  • in [1], das Projekt.
  • In [2] ist das Projekt so konfiguriert, dass [Transactions.cs] ausgeführt wird

Der Code von [Transactions.cs] lautet wie folgt:


using System;
using System.Configuration;
using System.Data;
using System.Data.SqlServerCe;
using System.Text;

namespace Chap7 {
    class Transactions {
        static void Main(string[] args) {

            // Auswertung der Konfigurationsdatei
            string connectionString = null;
            try {
                // Verbindungszeichenfolge
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // Anzeigen
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // Erstellung einer Tabelle mit 2 Artikeln gleichen Namens
            Article[] articles = new Article[2];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article", i * 100, i * 10, i);
            }
            // Eventuelle Ausnahmen werden behandelt
            try {
                Console.WriteLine("Insertion sans transaction...");
                // Die Artikeltabelle wird zunächst ohne Transaktion in die Datenbank eingefügt
                ExecuteUpdate(connectionString, "delete from articles");
                try {
                    InsertArticlesOutOfTransaction(connectionString, articles);
                } catch (Exception ex) {
                    // Fehlermeldung
                    Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
                }
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // Der Vorgang wird wiederholt, diesmal jedoch in einer Transaktion
                Console.WriteLine("\n\nInsertion dans une transaction...");
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // Fehlermeldung
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // Einfügen einer Artikeltabelle ohne Transaktion
        static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) {
....
        }

        // Einfügen einer Artikeltabelle innerhalb einer Transaktion
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
....
        }

        // Ausführung einer Aktualisierungsabfrage
        static void ExecuteUpdate(string connectionString, string requête) {
....
        }

        // Ausführung einer Select-Abfrage
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // Reader-Anzeige
        static void AfficheReader(IDataReader reader) {
...
            }
        }
    }
}
  • Zeilen 12–19: Die Verbindungszeichenfolge zur Datenbank SQLServer wird aus [App.config] gelesen
  • Zeilen 25–28: Es wird ein Array mit zwei Objekten Article erstellt. Diese beiden Artikel haben denselben Namen „Artikel“. Die Datenbank [dbarticles.sdf] weist jedoch eine Eindeutigkeitsbeschränkung für ihre Spalte [nom] auf (siehe Abschnitt 9.3.1). Daher können diese beiden Artikel nicht gleichzeitig in der Datenbank vorhanden sein. Die beiden Artikel mit dem Namen „article“ werden in die Tabelle articles eingefügt. Es wird also ein Problem auftreten: c.a.d – eine Ausnahme, die von SGBD ausgelöst und von dessen Konnektor ADO.NET weitergeleitet wird. Um die Auswirkungen der Transaktion zu veranschaulichen, werden die beiden Artikel in zwei verschiedenen Umgebungen eingefügt:
    • zunächst außerhalb einer Transaktion. Dabei ist zu beachten, dass SQLServer Compact in diesem Fall im Autocommit-Modus arbeitet, c.a.d. Jeder Befehl SQL wird in eine implizite Transaktion eingefügt. Der erste Artikel wird eingefügt. Der zweite nicht.
    • Anschließend in eine explizite Transaktion, die beide Einfügungen umfasst. Da die zweite Einfügung fehlschlägt, wird die erste rückgängig gemacht. Letztendlich wird keine Einfügung vorgenommen.
  • Zeile 33: Die Tabelle articles wird geleert
  • Zeile 35: Einfügen der beiden Artikel ohne explizite Transaktion. Da bekannt ist, dass das zweite Einfügen eine Ausnahme auslöst, wird diese durch ein try/catch-Block abgefangen
  • Zeile 46: Anzeige der Tabelle articles
  • Zeilen 44–46: Die gleiche Abfolge wird wiederholt, diesmal jedoch wird für die Einfügungen eine explizite Transaktion verwendet. Die auftretende Ausnahme wird hier von der Methode InsertArticlesInTransaction abgefangen.
  • Zeilen 54–56: Die Methode InsertArticlesOutOfTransaction entspricht der Methode InsertArticles aus dem zuvor behandelten Programm [Parametres.cs].
  • Zeilen 64–66: Die Methode ExecuteUpdate ist dieselbe wie zuvor. Der Befehl SQL wird in einer impliziten Transaktion ausgeführt. Dies ist hier möglich, da bekannt ist, dass SQLServer Compact in diesem Fall im Autocommit-Modus arbeitet.
  • Zeilen 69–71: Gleiches gilt für die Methode ExecuteSelect.

Die Methode InsertArticlesInTransaction lautet wie folgt:


// Einfügen einer Artikeltabelle in eine Transaktion
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // Verbindung herstellen
                connexion.Open();
                // Konfiguration des Befehls
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // Auftragskompilierung
                sqlCommand.Prepare();
                // Transaktion
                SqlCeTransaction transaction = null;
                try {
                    // Transaktion starten
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                    // Der Befehl SQL muss in dieser Transaktion ausgeführt werden
                    sqlCommand.Transaction = transaction;
                    // Einfügen der Zeilen
                    for (int i = 0; i < articles.Length; i++) {
                        // Initialisierung der Parameter
                        sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                        sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                        sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                        sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                        // Abfrage ausführen
                        sqlCommand.ExecuteNonQuery();
                    }
                    // Die Transaktion wird bestätigt
                    transaction.Commit();
                    Console.WriteLine("transaction validée...");
                } catch {
                    // Transaktion rückgängig machen
                    if (transaction != null)transaction.Rollback();
                    Console.WriteLine("transaction invalidée...");
                }
            }
        }

Wir gehen nur auf die Unterschiede zur Methode InsertArticles des zuvor behandelten Programms [Parametres.cs] ein:

  • Zeile 16: Eine Transaktion SqlCeTransaction wird deklariert.
  • Zeilen 17, 35: Der Try/Catch-Block zur Behandlung der Ausnahme, die nach dem zweiten Einfügevorgang auftritt
  • Zeile 19: Die Transaktion wird angelegt. Sie gehört zur aktuellen Verbindung.
  • Zeile 21: Der parametrisierte Befehl SQL wird in die Transaktion aufgenommen
  • Zeilen 23–31: Die Einfügungen werden durchgeführt
  • Zeile 33: Alles ist gut verlaufen – die Transaktion wird bestätigt – die Einfügungen werden endgültig in die Datenbank übernommen.
  • Zeile 37: Es ist ein Problem aufgetreten. Die Transaktion wird rückgängig gemacht, sofern sie existierte.

Die Ausführung liefert folgende Ergebnisse:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Insertion sans transaction...
Il y a eu 0 ligne(s) modifiée(s)
Erreur d'accès à la base de données (A duplicate value cannot be inserted into a unique index. [ Table name = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ])

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

126 article 100 10 1


Insertion dans une transaction...
Il y a eu 1 ligne(s) modifiée(s)
transaction invalidée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------
  • Zeile 4: ausgegeben von ExecuteUpdate („delete from articles“) – die Tabelle enthielt keine Zeilen
  • Zeile 5: Die durch den zweiten Einfügevorgang ausgelöste Ausnahme. Die Meldung weist darauf hin, dass die Einschränkung UQ__ARTICLES__0000000000000010 nicht überprüft wurde. Weitere Informationen erhalten Sie, wenn Sie die Eigenschaften der Datenbank einsehen:
  • In [1] in der Ansicht [Database Explorer] von Visual Studio wurde eine Verbindung [2] zur Datenbank [dbarticles.sdf] erstellt. Diese hat den Index UQ__ARTICLES__0000000000000010. Durch einen Rechtsklick auf diesen Index gelangt man zu dessen Eigenschaften (Index properties)
  • In [3,4] sieht man, dass der Index UQ__ARTICLES__0000000000000010 einer Eindeutigkeitsbeschränkung für die Spalte [NOM] entspricht
  • Zeilen 7–11: Anzeige der Tabelle articles nach den beiden Einfügungen. Sie ist nicht leer: Der erste Artikel wurde eingefügt.
  • Zeile 15: Ausgegeben von ExecuteUpdate („delete from articles“) – es gab eine Zeile in der Tabelle
  • Zeile 16: Meldung, die von InsertArticlesInTransaction angezeigt wird, wenn die Transaktion fehlschlägt.
  • Zeilen 18–20: Zeigen, dass keine Einfügung vorgenommen wurde. Das Rollback der Transaktion hat die erste Einfügung rückgängig gemacht.

9.7. Die Methode ExecuteScalar

9.7.1. Unter den Methoden der in Abschnitt 9.3.3 beschriebenen Schnittstelle IDbCommand befand sich die folgende Methode:

ExecuteScalar
M
um einen Befehl SQL auszuführen, wobei Select nur ein einziges Ergebnis liefert, wie in: select count(*) from articles.

Hier zeigen wir ein Anwendungsbeispiel für diese Methode. Kehren wir zum Projekt zurück:

  • in [1], dem Projekt.
  • In [2] ist das Projekt so konfiguriert, dass es [ExecuteScalar.cs] ausführt

Das Programm [ExecuteScalar.cs] lautet wie folgt:


...
namespace Chap7 {
    class Scalar {
        static void Main(string[] args) {

            // Auswertung der Konfigurationsdatei
            string connectionString = null;
...

            // Anzeigen
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // Erstellung einer Tabelle mit 5 Artikeln
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // Eventuelle Ausnahmen werden behandelt
            try {
                // Die Artikeltabelle wird in eine Transaktion eingefügt
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
                // Berechnung des Durchschnittspreises der Artikel
                decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles");
                Console.WriteLine("Prix moyen des articles={0}", prixMoyen);
                // oder die Anzahl der Artikel
                int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles");
                Console.WriteLine("Nombre d'articles={0}", nbArticles);
            } catch (Exception ex) {
                // Fehlermeldung
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // Einfügen einer Artikeltabelle in eine Transaktion
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
...
        }


        // Ausführung einer Aktualisierungsanfrage
        static object ExecuteScalar(string connectionString, string requête) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // Verbindung herstellen
                connexion.Open();
                // Abfrage ausführen
                return new SqlCeCommand(requête, connexion).ExecuteScalar();
            }
        }

        // Ausführung einer Aktualisierungsabfrage
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // Ausführung einer Select-Abfrage
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // Reader-Anzeige
        static void AfficheReader(IDataReader reader) {
...
        }
    }
}
  • Zeilen 14–17: Erstellung eines Arrays mit 5 Artikeln
  • Zeile 22: Die Tabelle articles wird geleert
  • Zeile 23: Sie wird mit den 5 Artikeln gefüllt
  • Zeile 24: Die Tabelle wird angezeigt
  • Zeile 26: Abfrage des Durchschnittspreises der Artikel
  • Zeile 29: Abfrage der Anzahl der Artikel
  • Zeile 49: Verwendung der Methode [IDbCommand].ExecuteScalar(), um jeden dieser Werte zu berechnen.

Die Ergebnisse der Ausführung lauten wie folgt:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)
transaction validée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

145 article1 100 10 1
146 article2 200 20 2
147 article3 300 30 3
148 article4 400 40 4
149 article5 500 50 5
Prix moyen des articles=300
Nombre d'articles=5

Die Zeilen 15 und 16 zeigen die beiden von der Methode ExecuteScalar zurückgegebenen Werte.

9.8. Beispielanwendung – Version 7

Wir greifen die Beispielanwendung IMPOTS wieder auf. Die letzte Version wurde in Abschnitt 7.6 behandelt. Es handelte sich um die folgende dreischichtige Anwendung:

  • Die Schicht [ui] war eine grafische Benutzeroberfläche ([A]), und die Schicht [dao] bezog ihre Daten aus einer Textdatei ([B]).
  • Die Instanziierung der Schichten und ihre Integration in die Anwendung wurden von Spring übernommen.

Wir ändern die Schicht [dao] so, dass sie ihre Daten aus einer Datenbank abruft.

9.8.1. Die Datenbank

Der Inhalt der vorherigen Textdatei [B] wird in eine Datenbank MySQL5 übernommen. Wir zeigen, wie das funktioniert:

  • [1]: MySQL Administrator wurde gestartet
  • [2,3]: Klicken Sie im Bereich [Schemata] mit der rechten Maustaste und wählen Sie die Option [Create Schema], um eine neue Datenbank anzulegen
  • [4]: Die Datenbank erhält den Namen [bdimpots]
  • [5]: Sie wurde zu den Datenbanken im Bereich [Schemata] hinzugefügt.
  • [6,7]: Klicken Sie mit der rechten Maustaste auf die Tabelle und wählen Sie die Option [Create New Table], um eine Tabelle zu erstellen
  • [8]: Die Tabelle erhält den Namen [tranches]. Sie enthält die Spalten von [id, limite, coeffR, coeffN].
  • [9,10]: [id] ist der Primärschlüssel vom Typ INTEGER und hat das Attribut AUTO_INCREMENT [10]: SGBD ist der Schlüssel, der diese Spalte beim Hinzufügen von Zeilen ausfüllt.
  • Die Spalten [limite, coeffR, coeffN] sind vom Typ DOUBLE.
  • [11,12]: Die neue Tabelle wird auf der Registerkarte „[Schema Tables]“ der Datenbank angezeigt.
  • [13,14]: Um Daten in die Tabelle einzufügen
  • [15]: [Query Browser] wurde gestartet
  • [16]: Die Daten für die Spalten [limite, coeffR, coeffN] wurden eingegeben und validiert. Die Spalte [id] wurde durch SGBD gefüllt. Die Validierung erfolgte mit [17].
  • immer noch in [Query Browser] [18] wird die Abfrage [20] mit [19] ausgeführt. Diese Abfrage legt einen Benutzer „admimpots“ mit dem Passwort „mdpimpots“ an und gewährt ihm alle Berechtigungen (grant all privileges) für alle Objekte der Datenbank bdimpots (on bdimpots.*). Dadurch können wir in der Datenbank [bdimpots] mit dem Benutzer [admimpots] statt mit dem Administrator [root] arbeiten.

9.8.2. Die Visual Studio-Lösung

Wir werden den für Version 5 der Beispielanwendung beschriebenen Ansatz befolgen (siehe Abschnitt 6.4). Wir werden schrittweise die folgende Visual Studio-Lösung erstellen:

  • in [1]: Die Lösung ImpotsV7 besteht aus drei Projekten, jeweils eines für jede der drei Schichten der Anwendung
  • in [2]: das Projekt [dao] der Schicht [dao], das fortan eine Datenbank nutzen wird
  • in [3]: das Projekt [metier] der Schicht [metier]. Hier übernehmen wir die in Abschnitt 6.4.4 beschriebene Schicht [metier] aus Version 5.
  • in [4]: das Projekt [ui] der Ebene [ui]. Wir greifen hier auf die Ebene [ui] der Version 6 zurück, die in Abschnitt 7.6 beschrieben ist.

Wir stützen uns auf den bisherigen Entwicklungsstand, um zwei bereits geschriebene Schichten wiederzuverwenden, nämlich die Schichten [ui] und [metier]. Dies wird durch die gewählte Schichtenarchitektur ermöglicht. Wir benötigen jedoch die Quellcodes der Schichten [ui] und [metier]. Es reicht nämlich nicht aus, sich mit den DLL der Schichten zu begnügen. Als in Version 5 die DLL der Schicht [metier] erstellt wurde, hatte sie eine Abhängigkeit von der DLL der Schicht [dao]. Diese Abhängigkeit wurde fest in der DLL der Schicht [metier] verankert (Name der DLL der Schicht [dao], Version, Identitäts-Token, ...). Somit akzeptiert die DLL der Version 5 [ImpotsV5-metier.dll] nur die Zusammenarbeit mit der DLL und der [ImpotsV5-dao.dll], mit denen sie kompiliert wurde. Wenn man die DLL der Schicht [dao] ändert, muss die Schicht [metier] neu kompiliert werden, um eine neue DLL zu erstellen. Das Gleiche gilt für die Ebene [ui]. Die Schichten [ui] und [metier] werden daher nicht geändert, sondern neu kompiliert, um mit der DLL der neuen Schicht [dao] zusammenzuarbeiten.

9.8.3. Die Schicht [dao]

Die Projektreferenzen (siehe [1] im Projekt)

  • nunit.framework: für den Test NUnit
  • System.Configuration: zur Auswertung der Konfigurationsdatei [App.config]
  • System.Data: da eine Datenbank ausgewertet wird.

Die Entitäten (siehe [2] im Projekt)

Die Klassen [TrancheImpot] und [ImpotException] stammen aus früheren Versionen.

Die Schicht [dao] (siehe [3] im Projekt)

Die Schnittstelle [IImpotDao] hat sich nicht geändert:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // Steuerklassen
        TrancheImpot[] TranchesImpot{get;}
    }
}

Die Implementierungsklasse [DataBaseImpot] dieser Schnittstelle lautet wie folgt:


using System;
using System.Collections.Generic;
using System.Data.Common;
using Entites;

namespace Dao {
    public class DataBaseImpot : IImpotDao {
        // Steuerklassen
        private TrancheImpot[] tranchesImpot;
        public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } }

        // Hersteller
        public DataBaseImpot(string factory, string connectionString, string requête) {
            // Fabrik: die Fabrik des Ziels SGBD
            // connectionString: die Verbindungszeichenfolge zur Datenbank der Steuerklassen
            // Eventuelle Ausnahmen werden behandelt
            try {
                // Es wird ein generischer Konnektor für SGBD abgerufen
                DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory);
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // Verbindungskonfiguration
                    connexion.ConnectionString = connectionString;
                    // Verbindung wird hergestellt
                    connexion.Open();
                    // Befehlskonfiguration 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // Abfrage ausführen
                    List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>();
                    using (DbDataReader reader = sqlCommand.ExecuteReader()) {
                        while (reader.Read()) {
                            // Erstellung einer neuen Steuerklasse
                            listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) });
                        }
                    }
                    // Steuerklassen werden in die Instanz aufgenommen
                    tranchesImpot = listTrancheImpot.ToArray();
                }
            } catch (Exception ex) {
                // Die Ausnahme wird in einen Typ ImpotException gekapselt
                throw new ImpotException("Erreur de lecture des tranches d'impôt", ex) { Code = 101 };
            }

        }
    }
}
  • Zeile 7: Die Klasse [DataBaseImpot] implementiert die Schnittstelle [IImpotDao].
  • Zeile 10: Die Implementierung der Methode [TranchesImpot] der Schnittstelle. Sie gibt lediglich einen Verweis auf das Array der Steuerklassen aus Zeile 9 zurück. Dieses Array wird vom Konstruktor der Klasse erstellt.
  • Zeile 13: Der Konstruktor. Er verwendet einen generischen Konnektor (siehe Abschnitt 9.4.5), um die Datenbank der Steuerklassen abzufragen. Der Konstruktor erhält drei Parameter:
    • den Namen der „Factory“, bei der er die Klassen anfordert, um eine Verbindung zur Datenbank herzustellen, Befehle SQL auszuführen und das Ergebnis einer SELECT-Abfrage auszuwerten.
    • die Verbindungszeichenfolge, die er für die Verbindung zur Datenbank verwenden muss
    • den Befehl SQL SELECT, den er ausführen muss, um die Steuerklassen abzurufen.
  • Zeile 19: Fordert einen Konnektor bei der „Factory“ an
  • Zeile 20: Stellt eine Verbindung mit diesem Konnektor her. Die Verbindung wird hergestellt, ist aber noch nicht betriebsbereit
  • Zeile 22: Die Verbindungszeichenfolge für die Verbindung wird initialisiert. Nun kann eine Verbindung hergestellt werden.
  • Zeile 24: Es wird eine Verbindung hergestellt
  • Zeile 26: Fordert vom Konnektor ein Objekt [DbCommand] an, um den Auftrag SQL auszuführen
  • Zeile 27: Legt den auszuführenden Auftrag SQL fest
  • Zeile 28: Legt die Verbindung fest, über die der Befehl ausgeführt werden soll
  • Zeile 30: Es wird eine leere Liste [listTrancheImpot] mit Objekten vom Typ [TrancheImpot] erstellt.
  • Zeile 31: Der Befehl SQL „Select“ wird ausgeführt
  • Zeilen 32–35: Das aus dem „Select“-Befehl resultierende Objekt vom Typ [DbDataReader] wird ausgewertet. Jede Zeile der Ergebnistabelle des „Select“-Befehls dient dazu, ein Objekt vom Typ [TrancheImpot] zu instanziieren, das der Liste [listTrancheImpot] hinzugefügt wird.
  • Zeile 38: Die Liste der Objekte vom Typ [TrancheImpot] wird in das Array in Zeile 9 übertragen.
  • Zeilen 40–43: Eine eventuelle Ausnahme wird in einen Typ [ImpotException] gekapselt und erhält den Fehlercode 101 (beliebig).

Der Test [Test1] (siehe [4] im Projekt)

Die Klasse [Test1] gibt lediglich die Steuerklassen auf dem Bildschirm aus. Es handelt sich um dieselbe Klasse, die bereits in Version 5 (Abschnitt 6.4.3) verwendet wurde, mit Ausnahme der Anweisung, die die Schicht [dao] instanziiert (Zeile 14).


using System;
using Dao;
using Entites;
using System.Configuration;

namespace Tests {
    class Test1 {
        static void Main() {

            // Die Schicht wird angelegt [dao]
            IImpotDao dao = null;
            try {
                // Erstellung der Schicht [dao]
                dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
            } catch (ImpotException e) {
                // Fehleranzeige
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // Programm beenden
                Environment.Exit(1);
            }
            // Steuerklassen werden angezeigt
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
        }
    }
}

In Zeile 14 wird die folgende Konfigurationsdatei [App.config] verwendet:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbImpotsMySql5" connectionString="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;" />
    </connectionStrings>
    <appSettings>
        <add key="requete" value="select limite, coeffr, coeffn from tranches"/>
        <add key="factoryMySql5" value="MySql.Data.MySqlClient"/>
    </appSettings>
</configuration>
  • Zeile 4: Die Verbindungszeichenfolge zur Datenbank MySQL5. Es ist zu beachten, dass der Benutzer [admimpots] die Verbindung herstellt.
  • Zeile 8: die „Factory“ für die Arbeit mit SGBD und MySQL5
  • Zeile 7: Die Select-Abfrage SQL zum Abrufen der Steuerklassen.

Das Projekt ist so konfiguriert, dass [Test1.cs] ausgeführt wird:

Image

Die Testausführung liefert folgende Ergebnisse:

1
2
3
4
5
6
7
4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

Der Test NUnit [NUnit1] (siehe [4] im Projekt)

Der Unit-Test [NUnit1] ist derselbe, der bereits in Version 5 (Abschnitt 6.4.3) verwendet wurde, mit Ausnahme der Anweisung, die die Schicht [dao] instanziiert (Zeile 16).


using System;
using System.Configuration;
using Dao;
using Entites;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // Layer [dao] zu testen
        private IImpotDao dao;

        // Hersteller
        public NUnit1() {
            // Initialisierung der Ebene [dao]
            dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
        }

        // Test
        [Test]
        public void ShowTranchesImpot(){
            // Steuerklassen werden angezeigt
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
            // einige Tests
            Expect(tranchesImpot.Length,EqualTo(7));
            Expect(tranchesImpot[2].Limite,EqualTo(14753).Within(1e-6));
            Expect(tranchesImpot[2].CoeffR, EqualTo(0.191).Within(1e-6));
            Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92).Within(1e-6));
        }
    }
}

Um diesen Unit-Test auszuführen, muss das Projekt vom Typ [Class Library] sein:

  • in [1]: Der Projekttyp wurde geändert
  • in [2]: Das generierte DLL wird [ImpotsV7-dao.dll] heißen
  • in [3]: Nach der Generierung (F6) des Projekts enthält der Ordner [dao/bin/Release] die Dateien DLL und [ImpotsV7-dao.dll]. Außerdem enthält er die Konfigurationsdatei [App.config], die in [nom DLL].config umbenannt wurde. Dies ist in Visual Studio Standard.

Die Datei „DLL“ und „[ImpotsV7-dao.dll]“ werden anschließend in das Framework „NUnit“ geladen und ausgeführt:

  • in [1]: Die Tests waren erfolgreich. Wir betrachten die Schicht [dao] nun als betriebsbereit. Ihre DLL enthält alle Klassen des Projekts, einschließlich der Testklassen. Diese sind überflüssig. Wir erstellen die DLL neu, um die Testklassen daraus auszuschließen.
  • zu [2]: Der Ordner [tests] wird aus dem Projekt ausgeschlossen
  • zu [3]: das neue Projekt. Dieses wird durch F6 neu generiert, um eine neue DLL zu erzeugen. Diese DLL wird von den Ebenen [metier] und [ui] der Anwendung verwendet.

9.8.4. Die Schicht [metier]

  • in [1] wurde das Projekt [metier] zum aktiven Projekt der Lösung
  • in [2]: die Projektreferenzen. Zu beachten ist die Referenz auf DLL der zuvor erstellten Ebene [dao]. Dieses Verfahren zum Hinzufügen von Referenzen wurde in Version 5 in Abschnitt 6.4.4 beschrieben.
  • in [3]: die Ebene [metier]. Es handelt sich um die in Version 5 beschriebene Ebene, die in Abschnitt 6.4.4 erläutert wurde.

Das Projekt [metier] ist so konfiguriert, dass es eine DLL generiert:

  • [1]: Das Projekt ist vom Typ „Klassenbibliothek“
  • [2]: Bei der Generierung des Projekts entstehen die Dateien DLL, [ImpotsV7-metier.dll] und [3].

Das Projekt wurde generiert (F6).

9.8.5. Die Ebene [ui]

  • in [1] wurde das Projekt [ui] zum aktiven Projekt der Lösung
  • in [2]: die Projektreferenzen. Zu beachten sind die Referenzen auf DLL in den Ebenen [dao] und [metier].
  • in [3]: die Ebene [ui]. Es handelt sich um die in Abschnitt 7.6 beschriebene Version 6.
  • In [4] entspricht die Konfigurationsdatei [App.config] derjenigen der Version 6. Sie unterscheidet sich davon lediglich in der Art und Weise, wie die Schicht [dao] von Spring instanziiert wird:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • Zeilen 11–25: die Spring-Konfiguration
  • Zeilen 15–24: die von Spring instanziierten Objekte
  • Zeilen 16–20: Instanziierung der Schicht [dao]
  • Zeile 16: Die Schicht [dao] wird von der Klasse [Dao.DataBaseImpot] instanziiert, die sich in DLL [ImpotsV7-Dao] befindet
  • Zeilen 17–19: Die drei Parameter (Factory der verwendeten Klasse SGBD, Verbindungszeichenfolge, Abfrage SQL), die dem Konstruktor der Klasse [Dao.DataBaseImpot] übergeben werden müssen
  • Zeilen 21–23: Instanziierung der Schicht [metier]. Es handelt sich um dieselbe Konfiguration wie in Version 6.

Tests

Das Projekt [ui] ist wie folgt konfiguriert:

  • [1]: Das Projekt ist vom Typ „Windows-Anwendung“
  • [2]: Bei der Projektgenerierung wird die ausführbare Datei [ImpotsV7-ui.exe] erstellt

Ein Ausführungsbeispiel finden Sie unter [3].

9.8.6. Datenbank ändern

Die oben genannte Schicht [dao] wurde mit einem generischen Konnektor und einer Basis MySQL5 erstellt. Wir möchten hier auf eine Basis SQL Server Compact umsteigen, um zu zeigen, dass sich lediglich die Konfiguration ändert.

Die „ “-Datenbank SQL Server Compact sieht wie folgt aus:

  • [1]: Die Datenbank „[dbimpots.sdf]“ in der Ansicht „[DataBase Explorer]“ von Visual Studio „[2]“. Sie wurde ohne Passwort angelegt.
  • [3]: Die Tabelle [data], die die Daten enthält. Die Namen der Tabelle und der Spalten wurden bewusst anders gewählt als bei der Datenbank MySQL5, um erneut zu verdeutlichen, wie wichtig es ist, solche Details in der Konfigurationsdatei statt im Code festzuhalten.
  • [4]: Die Spalte [id] ist der Primärschlüssel und hat das Attribut Identity: Es ist die Spalte SGBD, die ihr die Werte zuweist.
  • [5]: Der Inhalt der Tabelle [data].
  • [6]: Die Datenbank [dbimpots.sdf] wurde in den Projektordner [ui] verschoben und in dieses Projekt integriert.
  • [7]: Die Datenbank [dbimpots.sdf] wird in den Ausführungsordner des Projekts kopiert.

Die Konfigurationsdatei [App.config] für die neue Datenbank lautet wie folgt:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <!--
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            -->
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • Zeilen 23–27: Die Konfiguration der Schicht [dao] zur Nutzung der Datenbank [dbimpots.sdf].

Die Ergebnisse der Ausführung sind identisch mit den vorherigen. Es ist anzumerken, dass die Verwendung eines generischen Konnektors von Vorteil ist, um die Schicht [dao] unempfindlich gegenüber der Änderung von SGBD zu machen. Wir haben jedoch gesehen, dass dieser Konnektor nicht für alle Situationen geeignet ist, insbesondere nicht für solche, in denen parametrisierte Abfragen verwendet werden. In solchen Fällen gibt es andere Lösungen, wie die bereits erwähnten Frameworks von Drittanbietern für den Datenzugriff (Spring, iBatis, NHibernate, LINQ, ...).

9.9. Weiterführende Informationen …

  • LINQ wird in zahlreichen Werken vorgestellt, insbesondere in dem Buch: „C# 3.0 in a Nutshell“ von Joseph und Ben Albahari, erschienen bei O’Reilly, das bereits in der Einleitung dieses Dokuments erwähnt wurde.
  • iBatis wird in dem Buch „iBatis in Action“ von Clinton Begin (Manning Verlag) vorgestellt
  • „Nhibernate in Action“ vom Manning-Verlag ist für Juli 2008 geplant

Für Spring, iBatis und NHibernate stehen Referenzhandbücher auf den Websites der jeweiligen Frameworks zur Verfügung.