Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort zur 5. Auflage
1 Allgemeine Einführung in .NET
2 Grundlagen der Sprache C#
3 Klassendesign
4 Vererbung, Polymorphie und Interfaces
5 Delegates und Ereignisse
6 Weitere .NET-Datentypen
7 Weitere Möglichkeiten von C#
8 Auflistungsklassen (Collections)
9 Fehlerbehandlung und Debugging
10 LINQ to Objects
11 Multithreading und die Task Parallel Library (TPL)
12 Arbeiten mit Dateien und Streams
13 Binäre Serialisierung
14 Einige wichtige .NET-Klassen
15 Projektmanagement und Visual Studio 2010
16 XML
17 WPF – Die Grundlagen
18 WPF-Containerelemente
19 WPF-Steuerelemente
20 Konzepte der WPF
21 Datenbindung
22 2D-Grafik
23 ADO.NET – verbindungsorientierte Objekte
24 ADO.NET – Das Command-Objekt
25 ADO.NET – Der SqlDataAdapter
26 ADO.NET – Daten im lokalen Speicher
27 ADO.NET – Aktualisieren der Datenbank
28 Stark typisierte DataSets
29 LINQ to SQL
30 Weitergabe von Anwendungen
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Visual C# 2010 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2010

Visual C# 2010
geb., mit DVD
1295 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1552-7
Pfeil 24 ADO.NET – Das Command-Objekt
Pfeil 24.1 Das »SqlCommand«-Objekt
Pfeil 24.1.1 Erzeugen eines »SqlCommand«-Objekts
Pfeil 24.1.2 Die Methode »CreateCommand« des »Connection«-Objekts
Pfeil 24.1.3 Ausführen des »SqlCommand«-Objekts
Pfeil 24.1.4 Die Eigenschaft »CommandTimeout« des »SqlCommand«-Objekts
Pfeil 24.2 Aktionsabfragen absetzen
Pfeil 24.2.1 Datensätze hinzufügen
Pfeil 24.2.2 Datensätze löschen
Pfeil 24.2.3 Datensätze ändern
Pfeil 24.2.4 Abfragen, die genau ein Ergebnis liefern
Pfeil 24.3 Das »SqlDataReader«-Objekt
Pfeil 24.3.1 Datensätze einlesen
Pfeil 24.3.2 Schließen des »SqlDataReader«-Objekts
Pfeil 24.3.3 MARS (Multiple Active Resultsets)
Pfeil 24.3.4 Batch-Abfragen mit »NextResult« durchlaufen
Pfeil 24.3.5 Schema eines »SqlDataReader«-Objekts untersuchen
Pfeil 24.4 Parametrisierte Abfragen
Pfeil 24.4.1 Parametrisierte Abfragen mit dem SqlClient-Datenprovider
Pfeil 24.4.2 Die Klasse »SqlParameter«
Pfeil 24.4.3 Asynchrone Abfragen
Pfeil 24.4.4 Gespeicherte Prozeduren (Stored Procedures)


Galileo Computing - Zum Seitenanfang

24.3 Das »SqlDataReader«-Objekt Zur nächsten ÜberschriftZur vorigen Überschrift

Mit der Methode ExecuteNonQuery des SqlCommand-Objekts können Sie Datensätze in der Originaldatenbank manipulieren und mit ExecuteScalar ein einzelnes Abfrageergebnis abrufen. Möchte man sich die Datensätze einer Tabelle in einer Anwendung anzeigen lassen, wird die Methode ExecuteReader des SqlCommand-Objekts aufgerufen.


public SqlDataReader ExecuteReader()

Das Ergebnis des Methodenaufrufs ist ein Objekt vom Typ SqlDataReader. Dieses ähnelt den anderen Reader-Objekten des .NET-Frameworks (TextReader, StreamReader usw.). Ein SqlDataReader-Objekt liest aus einer Ergebnisliste, die schreibgeschützt ist und sich in einem serverseitigen Puffer befindet, also auf der Seite der Datenbank. Sie sollten daher den Ratschlag beherzigen, die Ergebnisliste so schnell wie möglich abzurufen, damit die beanspruchten Ressourcen wieder freigegeben werden.

In einer von einem SqlDataReader-Objekt bereitgestellten Datensatzliste kann immer nur zum folgenden Datensatz navigiert werden. Eine beliebige Navigation in der Ergebnisliste ist nicht möglich – ebensowenig wie das Ändern der gelieferten Daten. Damit hat ein SqlDataReader nur sehr eingeschränkte Funktionalität. Dieses Manko wird andererseits durch die sehr gute Performance wettgemacht – das ist die Stärke des SqlDataReaders.

Das Erzeugen eines DataReader-Objekts funktioniert nur über den Aufruf der Methode ExecuteReader auf die SqlCommand-Referenz, denn die Klasse SqlDataReader weist keinen öffentlichen Konstruktor auf.


SqlDataReader reader = cmd.ExecuteReader();


Galileo Computing - Zum Seitenanfang

24.3.1 Datensätze einlesen Zur nächsten ÜberschriftZur vorigen Überschrift

Im folgenden Beispielprogramm wird ein SqlDataReader dazu benutzt, alle Artikel zusammen mit ihrem Preis nach dem Preis sortiert auszugeben.


// ------------------------------------------------------------------
// Beispiel: ...\Kapitel 24\DataReaderSample
// ------------------------------------------------------------------
SqlConnection con = new SqlConnection("...");
string strSQL = "SELECT ProductName, Unitprice " +
                "FROM Products " +
                "ORDER BY[UnitPrice]";
SqlCommand cmd = new SqlCommand(strSQL, con);
con.Open();
SqlDataReader reader = cmd.ExecuteReader();    
while (reader.Read())
  Console.WriteLine("{0,-35}{1}", reader["ProductName"], reader["UnitPrice"]);
reader.Close();
con.Close();

Zuerst wird die Zeichenfolge des SELECT-Statements definiert, die im nächsten Schritt zusammen mit der Referenz auf das SqlConnection-Objekt dazu dient, ein SqlCommand-Objekt zu erzeugen. Auf dem SqlCommand-Objekt wird nach dem Öffnen der Verbindung die Methode ExecuteReader ausgeführt. Der Rückgabewert wird in der Objektvariablen reader vom Typ SqlDataReader gespeichert.

SqlDataReader liefert alle Datensätze, die der Reihe nach durchlaufen werden müssen. Um auf die Datensätze zuzugreifen, gibt es nur eine Möglichkeit: die Methode Read des DataReader-Objekts.


public override bool Read()

Jeder Aufruf von Read legt die Position des SqlDataReaders neu fest. Die Ausgangsposition vor dem ersten Read-Aufruf ist vor dem ersten Datensatz. Nach dem Aufruf von Read ist der Rückgabewert true, falls noch eine weitere Datenzeile abgerufen werden kann. Ist der Rückgabewert false, ist kein weiterer Datensatz mehr verfügbar. Damit eignet sich Read, um die Datensatzliste in einer while-Schleife zu durchlaufen.

Beabsichtigen Sie, wiederholt die Datensätze im SqlDataReader auszuwerten, müssen Sie die Methode ExecuteReader erneut aufrufen.

Auswerten der einzelnen Spalten in DataReader

Mit Read wird die Position des SqlDataReaders auf die folgende Datenzeile verschoben. In unserem Beispiel hat jede Datenzeile zwei Feldinformationen, nämlich die der Spalten ProductName und UnitPrice. Die einzelnen Spalten einer Abfrage werden in einer Auflistung geführt, auf die Sie über den Index des SqlDataReader-Objekts zugreifen können:


reader[0]

Sie können auch den Spaltenbezeichner angeben, also:


reader["ProductName"]

Diese Angaben sind gleichwertig. Bezüglich der Performance gibt es jedoch einen Unterschied. Geben Sie den Spaltennamen an, muss das SqlDataReader-Objekt zuerst die Spalte in der Auflistung suchen – und das bei jeder Datenzeile.


while (reader.Read())
  Console.WriteLine("{0,-35}{1}", reader["ProductName"], reader["UnitPrice"]);

Um die Leistung Ihrer Anwendung zu steigern, sollten Sie daher den Index der betreffenden Spalte angeben:


while(reader.Read())
  Console.WriteLine("{0,-35}{1}",reader[0], reader[1]);

Ist Ihnen nur der Spaltenbezeichner, jedoch nicht der dazugehörige Index bekannt, haben Sie mit der Methode GetOrdinal der Klasse DataReader unter Angabe des Spaltenbezeichners die Möglichkeit, vor dem Aufruf von Read den Index zu ermitteln:


int intName = reader.GetOrdinal("ProductName");
int intPrice = reader.GetOrdinal("UnitPrice");
while(reader.Read())
   Console.WriteLine("{0,-20}{1,-20}{2,-20}",
                 reader[intName], reader[intPrice]);

Spalten mit den typspezifischen Methoden abrufen

Mit dem Indexer der Methode ExecuteReader werden die Spaltenwerte vom Typ Object zurückgegeben. Das hat Leistungseinbußen zur Folge, weil der tatsächliche Typ erst in Object umgewandelt werden muss. Anstatt über den Indexer die Daten auszuwerten, können Sie auch eine der vielen GetXxx-Methoden anwenden, die für die wichtigsten .NET-Datentypen bereitgestellt werden, beispielsweise GetString, GetInt32 oder GetBoolean. Sie müssen nur die passende Methode aus einer (langen) Liste auswählen und beim Aufruf die Ordinalzahl der entsprechenden Spalte übergeben. Wählen Sie eine nicht typgerechte Methode aus, kommt es zur Ausnahme InvalidCastException.


SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) {
  Console.WriteLine(reader.GetString(0));
  Console.WriteLine(reader.GetString(1));
}

Auch wenn der Programmieraufwand größer ist, zur Laufzeit werden Sie dafür mit einem besseren Leistungsverhalten belohnt.

NULL-Werte behandeln

Spalten einer Tabelle können, sofern sie zugelassen sind, NULL-Werte enthalten. In der Tabelle Products betrifft das zum Beispiel die Spalte UnitPrice. Rufen Sie die Datenwerte über eine der typisierten Methoden ab und ist der Spaltenwert NULL, führt das zu einer Ausnahme.

Um diesem Problem zu begegnen, können Sie mit der Methode IsDBNull des SqlDataReaders prüfen, ob die entsprechende Spalte einen gültigen Wert oder NULL enthält.


SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) {
  Console.WriteLine(reader.GetString(0));
  if(! reader.IsDBNull(1))
    Console.WriteLine(reader.GetString(1));
}


Galileo Computing - Zum Seitenanfang

24.3.2 Schließen des »SqlDataReader«-Objekts Zur nächsten ÜberschriftZur vorigen Überschrift

Der SqlDataReader blockiert standardmäßig das SqlConnection-Objekt. Solange SqlDataReader durch den Aufruf von ExecuteReader geöffnet ist, können keine anderen Aktionen auf Basis der Verbindung durchgeführt werden, auch nicht das Öffnen eines zweiten SqlDataReader-Objekts. Daher sollte die Sperre so schnell wie möglich mit


reader.Close();

aufgehoben werden.


Galileo Computing - Zum Seitenanfang

24.3.3 MARS (Multiple Active Resultsets) Zur nächsten ÜberschriftZur vorigen Überschrift

Der SQL Server hat ein Feature, das es gestattet, mehrere Anforderungen auf einer Verbindung auszuführen. Damit wird eine Verbindung nicht mehr blockiert, wenn diese einem geöffneten SqlDataReader zugeordnet ist. Diese Technik von SQL Server wird als Multiple Active Resultsets, kurz MARS, bezeichnet. MARS ist per Vorgabe deaktiviert und muss zuvor aktiviert werden, um es zu nutzen. Sie aktivieren MARS entweder durch Ergänzen der Verbindungszeichenfolge um


MultipleActiveResultSets=True;

oder durch Setzen der gleichnamigen Eigenschaft im SqlConnectionStringBuilder.

MARS bietet sich an, wenn auf Basis der Ergebnismenge eines SqlDataReaders eine untergeordnete Tabellenabfrage gestartet werden soll. Das folgende Beispiel demonstriert dies. Dazu soll zu jedem Artikel auch der dazugehörige Lieferant ausgegeben werden. Damit stehen die beiden Tabellen Products und Suppliers im Mittelpunkt unserer Betrachtung. Sie stehen in einer 1:n-Beziehung zueinander.

Für jede Tabelle werden ein SqlCommand-Objekt sowie ein SqlDataReader-Objekt benötigt. Das erste DataReader-Objekt durchläuft die Artikeltabelle. Mit der in der Spalte SupplierID enthaltenen ID des Lieferanten wird eine untergeordnete Ergebnisliste – die der Tabelle Suppliers – durchlaufen. Hier wird die ID des Lieferanten gesucht und dessen Firmenbezeichnung zusätzlich zum Artikel ausgegeben.


// ------------------------------------------------------------------
// Beispiel: ...\Kapitel 24\MarsSample
// ------------------------------------------------------------------
SqlConnection con = new SqlConnection(
                    " ...;MultipleActiveResultSets=true");
string textProducts =
       "SELECT ProductName, UnitsInStock, SupplierID " +
       "FROM Products";
string textSupplier =
       "SELECT CompanyName " +
       "FROM Suppliers " +
       "WHERE SupplierID=@SupplierID";
// SqlCommand-Objekte erzeugen
SqlCommand cmdProducts, cmdSupplier;
cmdProducts = new SqlCommand(textProducts, con);
cmdSupplier = new SqlCommand(textSupplier, con);
SqlParameter param = cmdSupplier.Parameters.Add(
                    "@SupplierID", SqlDbType.Int); 
// Verbindung öffnen
con.Open();
SqlDataReader readerProducts = cmdProducts.ExecuteReader();
// Einlesen und Ausgabe der Datenzeilen an der Konsole
while (readerProducts.Read()) {
  Console.Write("{0,-35}{1,-6}",
  readerProducts["ProductName"], readerProducts["UnitsInStock"]);
  param.Value = readerProducts["SupplierID"];
  SqlDataReader readerSupplier = cmdSupplier.ExecuteReader();
  while (readerSupplier.Read()) {
    Console.WriteLine(readerSupplier["Companyname"]);
  }
  readerSupplier.Close();
  Console.WriteLine(new string('-', 80));
}
readerProducts.Close();
con.Close();

Der Vorteil von MARS wird in diesem Beispiel deutlich: Es genügt eine Verbindung, um mit den beiden SqlDataReader-Objekten zu operieren. Selbstverständlich kann die dem Programmcode zugrunde liegende Forderung auch ohne die Nutzung von MARS erfüllt werden. Allerdings wären dazu zwei Verbindungen notwendig, die einen gewissen Overhead verursachen.

Ein SQL-Statement kann eine parametrisierte Abfrage beschreiben. SqlCommand-Objekte unterstützen parametrisierte Abfragen durch eine Parameterliste. Weiter unten werden wir uns den parametrisierten Abfragen im Detail widmen.


Galileo Computing - Zum Seitenanfang

24.3.4 Batch-Abfragen mit »NextResult« durchlaufen Zur nächsten ÜberschriftZur vorigen Überschrift

Wenn Sie mehrere Abfragen hintereinander absetzen müssen, können Sie eine Batch-Abfrage ausführen. Allerdings werden Batch-Abfragen nicht von allen Datenbanken unterstützt, der SQL Server gehört aber dazu.

Nehmen wir an, Sie benötigen alle Datensätze sowohl der Tabelle Orders als auch der Tabelle Customers. Um eine syntaktisch korrekte Batch-Abfrage zu formulieren, werden die beiden SELECT-Statements innerhalb einer Zeichenfolge durch ein Semikolon getrennt angegeben:


SELECT * FROM Orders;SELECT * FROM Customers

Der Vorteil einer Batch-Abfrage ist, dass Sie die Methode ExecuteReader nicht zweimal aufrufen und nach dem ersten Aufruf den SqlDataReader schließen müssen. Selbstverständlich sind Batch-Abfragen nicht nur auf zwei SELECT-Anweisungen beschränkt, es können beliebig viele festlegt werden.

Das von einer Batch-Abfrage gefüllte SqlDataReader-Objekt enthält nach dem Aufruf der ExecuteReader-Methode mehrere Ergebnislisten. Um zwischen diesen zu wechseln, verwendet man die Methode NextResult. Die Funktionsweise ähnelt der von Read. Sie liefert true, wenn eine Datensatzliste durchlaufen wurde und sich noch eine weitere im DataReader befindet.


do {
  while(dr.Read())
    Console.WriteLine("{0}{1}{2}", dr[0], dr[1], dr[2]);
  Console.WriteLine();
} while(dr.NextResult());

Die Überprüfung mit NextResult muss in jedem Fall im Schleifenfuß erfolgen. Eine Prüfung im Schleifenkopf hätte zur Folge, dass die erste Datensatzliste überhaupt nicht durchlaufen wird.

Gemischte Batch-Abfragen

Manchmal ist es erforderlich, eine Batch-Abfrage zu definieren, die sich aus einer oder mehreren Auswahl- und Aktionsabfragen zusammensetzt. Vielleicht möchten Sie eine SELECT-, eine DELETE- sowie eine UPDATE-Abfrage in einer Batch-Abfrage behandeln? Kein Problem. Erstellen Sie eine solche Abfrage genauso wie jede andere, also beispielsweise mit:


SELECT * FROM Products;
UPDATE Products SET ProductName='Senfsauce' WHERE ProductName='Chai'

In dieser Weise gemischte Abfragen rufen Sie ebenfalls mit der Methode ExecuteReader auf.


Galileo Computing - Zum Seitenanfang

24.3.5 Schema eines »SqlDataReader«-Objekts untersuchen topZur vorigen Überschrift

Das Haupteinsatzgebiet des SqlDataReader-Objekts ist sicherlich die Abfrage von Daten. Darüber hinaus weist dieser Typ aber auch weitere Fähigkeiten auf. Im Einzelnen handelt es sich dabei um die folgenden:

  • Abrufen der Schemadaten der Spalten mit der Methode GetSchemaTable. Die gelieferten Informationen beschreiben unter anderem, ob eine Spalte Primärschlüsselspalte ist, ob sie schreibgeschützt ist, ob der Spaltenwert innerhalb der Tabelle eindeutig ist oder ob die Spalte einen NULL-Wert zulässt.
  • Der Name einer bestimmten Spalte lässt sich mit der Methode GetName ermitteln.
  • Die Ordinalposition einer Spalte lässt sich anhand des Spaltenbezeichners ermitteln. Die Methode GetOrdinal liefert den entsprechenden Index.

Die Methode »GetSchemaTable«

Der Rückgabetyp der Methode GetSchemaTable ist ein Objekt vom Typ DataTable. An dieser Stelle wollen wir diesen Typ nicht weiter betrachten. Es genügt am Anfang, zu wissen, dass sich ein DataTable-Objekt aus Datenzeilen und Spalten zusammensetzt, ähnlich einer Excel-Tabelle.

Dieser Tabelle liegt ein SELECT-Statement zugrunde, das mit ExecuteReader gegen die Datenbank ausgeführt wird. ExecuteReader haben wir bisher nur parameterlos kennengelernt; es akzeptiert aber auch einen Übergabeparameter vom Typ der Enumeration CommandBehavior. Das Member CommandBehavior.SchemaOnly gibt vor, dass die Abfrage nur Spalteninformationen zurückliefert.


SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly);

Auf der SqlDataReader-Referenz kann man anschließend die Methode GetSchemaTable aufrufen. Das ist vorteilhaft, denn die übermittelten Metadaten werden nun für alle Spalten, die im SELECT-Statement angegeben sind, in der Tabelle eingetragen. Dabei wird für jede im SELECT-Statement angegebene Spalte der Originaltabelle eine Datenzeile geschrieben.


DataTable table = reader.GetSchemaTable();

Die Spalten in der Schema-Tabelle werden durch festgelegte Bezeichner in einer bestimmten Reihenfolge ausgegeben. Die erste Spalte ist immer ColumnName, die zweite ColumnOrdinal, die dritte ColumnSize. Insgesamt werden 28 Spalten zur Auswertung bereitgestellt. Falls Sie nähere Informationen benötigen, sehen Sie sich in der .NET-Dokumentation die Hilfe zur Methode GetSchemaTable an.

Das folgende Beispiel untersucht die Spalten ProductID, ProductName und UnitsInStock der Tabelle Products. Es soll dabei genügen, nur die ersten vier Metainformationen zu ermitteln.


// ------------------------------------------------------------------
// Beispiel: ...\Kapitel 24\GetSchemaTableSample
// ------------------------------------------------------------------
SqlConnection con = new SqlConnection("...");
string strSQL = "SELECT ProductID, ProductName, " +
                "UnitsInStock FROM Products";
SqlCommand cmd = new SqlCommand(strSQL, con);
con.Open();
// Schema-Informationen einlesen
SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly); 
// Schema-Tabelle erstellen
DataTable table = reader.GetSchemaTable();
// Ausgabe der Schema-Tabelle
for(int col = 0; col < 4; col++)
  Console.Write("{0,-15}", table.Columns[col].ColumnName);
Console.WriteLine("\n" + new string('-', 60));
for(int i = 0; i < table.Rows.Count; i++) {
  for(int j = 0; j < 4; j++) {
    Console.Write("{0,-15}", table.Rows[i][j]);
  }
  Console.WriteLine();
}

Die resultierende Konsolenausgabe sehen Sie in Abbildung 24.1.

Abbildung 24.1 Ausgabe des Beispiels »GetSchemaTableSample«

Bezeichner einer Spalte ermitteln

Möchten Sie den Bezeichner einer bestimmten Spalte in der Ergebnisliste ermitteln, rufen Sie die Methode GetName des SqlDataReader-Objekts auf und übergeben dabei den Index der betreffenden Spalte in der Ergebnisliste. Der Rückgabewert ist eine Zeichenfolge.


Console.WriteLine(reader.GetName(3));

Index einer Spalte ermitteln

Ist der Index einer namentlich bekannten Spalte in der Ergebnisliste nicht bekannt, können Sie diesen mit GetOrdinal unter Angabe des Spaltenbezeichners ermitteln.


Console.WriteLine(reader.GetOrdinal("UnitPrice"));

Datentyp einer Spalte ermitteln

Sie können sowohl den .NET-Datentyp als auch den Datenbank-Datentyp eines bestimmten Feldes im SqlDataReader abfragen. Interessieren Sie sich für den .NET-Datentyp, rufen Sie die Methode GetFieldType des DataReaders auf, ansonsten GetDataTypeName.


Console.WriteLine(reader.GetFieldType(4));
Console.WriteLine(reader.GetDataTypeName(0));

Beide Methoden erwarten den Ordinalwert der betreffenden Spalte.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Visual C# 2010

Visual C# 2010
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Katalog: Windows Presentation Foundation






 Windows Presentation
 Foundation


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


Zum Katalog: C/C++






 C/C++


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2010
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern