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

Inhaltsverzeichnis
Vorwort zur 6. Auflage
1 Allgemeine Einführung in .NET
2 Grundlagen der Sprache C#
3 Das Klassendesign
4 Vererbung, Polymorphie und Interfaces
5 Delegates und Ereignisse
6 Strukturen und Enumerationen
7 Fehlerbehandlung und Debugging
8 Auflistungsklassen (Collections)
9 Generics – Generische Datentypen
10 Weitere C#-Sprachfeatures
11 LINQ
12 Arbeiten mit Dateien und Streams
13 Binäre Serialisierung
14 XML
15 Multithreading und die Task Parallel Library (TPL)
16 Einige wichtige .NET-Klassen
17 Projektmanagement und Visual Studio 2012
18 Einführung in die WPF und XAML
19 WPF-Layout-Container
20 Fenster in der WPF
21 WPF-Steuerelemente
22 Elementbindungen
23 Konzepte von WPF
24 Datenbindung
25 Weitere Möglichkeiten der Datenbindung
26 Dependency Properties
27 Ereignisse in der WPF
28 WPF-Commands
29 Benutzerdefinierte Controls
30 2D-Grafik
31 ADO.NET – Verbindungsorientierte Objekte
32 ADO.NET – Das Command-Objekt
33 ADO.NET – Der SqlDataAdapter
34 ADO.NET – Daten im lokalen Speicher
35 ADO.NET – Aktualisieren der Datenbank
36 Stark typisierte DataSets
37 Einführung in das ADO.NET Entity Framework
38 Datenabfragen des Entity Data Models (EDM)
39 Entitätsaktualisierung und Zustandsverwaltung
40 Konflikte behandeln
41 Plain Old CLR Objects (POCOs)
Stichwort

Download:
- Beispiele, ca. 62,4 MB

Jetzt Buch bestellen
Ihre Meinung?

Spacer
Visual C# 2012 von Andreas Kühnel
Das umfassende Handbuch
Buch: Visual C# 2012

Visual C# 2012
Rheinwerk Computing
1402 S., 6., aktualisierte und erweiterte Auflage 2013, geb., mit DVD
49,90 Euro, ISBN 978-3-8362-1997-6
Pfeil 35 ADO.NET – Aktualisieren der Datenbank
Pfeil 35.1 Aktualisieren mit dem »CommandBuilder«
Pfeil 35.1.1 Die von »SqlCommandBuilder« generierten Aktualisierungsstatements
Pfeil 35.1.2 Konfliktsteuerung in einer Mehrbenutzerumgebung
Pfeil 35.1.3 Die Eigenschaft »ConflictOption« des »SqlCommandBuilders«
Pfeil 35.1.4 Die Eigenschaft »SetAllValues«
Pfeil 35.2 Manuell gesteuerte Aktualisierung
Pfeil 35.2.1 Eigene Aktualisierungslogik
Pfeil 35.2.2 Das Beispielprogramm
Pfeil 35.3 Konfliktanalyse
Pfeil 35.3.1 Den Benutzer über fehlgeschlagene Aktualisierungen informieren
Pfeil 35.3.2 Konfliktverursachende Datenzeilen bei der Datenbank abfragen
Pfeil 35.4 Neue Autoinkrementwerte abrufen

35 ADO.NET – Aktualisieren der DatenbankZur nächsten Überschrift


Rheinwerk Computing - Zum Seitenanfang

35.1 Aktualisieren mit dem »CommandBuilder«Zur nächsten ÜberschriftZur vorigen Überschrift

Eine DataTable können Sie mit Daten aus jeder Datenquelle füllen. Handelt es sich dabei um eine Datenbank und können die Benutzer die Daten auch ändern, müssen die Änderungen zu einem bestimmten Zeitpunkt an die Datenbank übermittelt werden. Des Öfteren habe ich bereits die Update-Methode des DataAdapters erwähnt, die eine Verbindung zu der Datenbank aufbaut, um deren Datenbestand zu aktualisieren. Vielleicht haben Sie auch schon die Update-Methode getestet, nachdem Sie Zeilen Ihres DataSets geändert hatten. Sie werden dabei bestimmt einen Laufzeitfehler erhalten haben. Sehen Sie sich dazu das folgende Beispiel an, in dem eine neue Datenzeile hinzugefügt und eine vorhandene geändert wird. Nach Abschluss der Änderungen wird die Methode Update des SqlDataAdapters aufgerufen.

// Beispiel: ..\Kapitel 35\CommandBuilderSample
class Program {
static void Main(string[] args) {
SqlConnection con = new SqlConnection();
con.ConnectionString = "...";
SqlCommand cmd = new SqlCommand();
cmd.Connection = con;
cmd.CommandText = "SELECT ProductID, ProductName, " +
"UnitsInStock, Discontinued FROM Products";
DataSet ds = new DataSet();
SqlDataAdapter da = new SqlDataAdapter(cmd);
da.FillSchema(ds, SchemaType.Source);
ds.Tables[0].Columns["ProductID"].AutoIncrementSeed = -1;
ds.Tables[0].Columns["ProductID"].AutoIncrementStep = -1;
da.Fill(ds);
// Neue Datenzeile hinzufügen
DataRow newRow = ds.Tables[0].NewRow();
newRow["ProductName"] = "Camembert";
newRow["UnitsInStock"] = 100;
newRow["Discontinued"] = false;
ds.Tables[0].Rows.Add(newRow);
// Datenzeile ändern
DataRow[] editRow = ds.Tables[0].Select("ProductName='Tofu'");
if (editRow.Length == 1) {
editRow[0].BeginEdit();
editRow[0]["UnitsInStock"] = 1000;
editRow[0].EndEdit();
}
else
Console.WriteLine("Datenzeile 'Tofu' nicht gefunden.");
// Datenbank aktualisieren
int count = da.Update(ds);
Console.WriteLine("{0} Datenzeilen aktualisiert", count);
Console.ReadLine();
}
}

Listing 35.1 Aktualisieren einer Datenbank (verursacht einen Fehler)

Wo liegt aber nun die Ursache des in der Anweisung, die die Methode Update aufruft, auftretenden Laufzeitfehlers?

Denken wir einmal daran, wie die Abfolge ist, bis der SqlDataAdapter eine Auswahlabfrage an die Datenbank schickt. Wir hatten ein SqlCommand-Objekt erzeugt und diesem das SELECT-Statement übergeben. Bei der Instanziierung haben wir dem SqlDataAdapter das SqlCommand-Objekt über den Konstruktoraufruf bekannt gegeben. Der SqlDataAdapter speichert das in seiner Eigenschaft SelectCommand.

Der SqlDataAdapter hat aber noch drei weitere Eigenschaften, die nach einem SqlCommand-Objekt verlangen:

  • InsertCommand
  • DeleteCommand
  • UpdateCommand

So wie über SelectCommand die vom SqlDataAdapter abzusetzende Auswahlabfrage bekannt ist, benötigt der Adapter auch noch SqlCommand-Objekte, die die SQL-Statements INSERT, DELETE und UPDATE beschreiben.

Erfreulicherweise stellt der SqlDataAdapter nicht nach festgeschriebenen Regeln automatisch Aktualisierungsstatements bereit. Dieses Verhalten gibt uns jedoch die Möglichkeit, selbst Einfluss auf die Aktualisierung zu nehmen. Darauf werden wir später noch genauer eingehen. Die Folge ist jedenfalls, dass die Eigenschaften Insert-, Update- und DeleteCommand zunächst den Inhalt null haben.

Für das Erzeugen von SQL-Aktualisierungsstatements bietet uns ADO.NET die Klasse SqlCommandBuilder. Übergeben Sie bei der Instanziierung dieser Klasse dem Konstruktor die Referenz auf den SqlDataAdapter.

SqlCommandBuilder cmb = new SqlCommandBuilder(da);

Da der SqlCommandBuilder nun das SqlDataAdapter-Objekt kennt, weiß er, wie die SELECT-Auswahlabfrage aussieht. Auf dieser Grundlage erzeugt SqlCommandBuilder die SQL-Befehle INSERT, DELETE und UPDATE, verpackt sie in eine Zeichenfolge und weist sie jeweils einem neuen SqlCommand-Objekt zu. Die drei SqlCommand-Objekte werden den Eigenschaften UpdateCommand, InsertCommand und DeleteCommand des SqlDataAdapters übergeben. Unabhängig davon, ob im DataSet eine Zeile gelöscht, hinzugefügt oder editiert worden ist, wird der SqlDataAdapter mit den vom SqlCommandBuilder erzeugten Kommandos die Originaldatenbank aktualisieren.

Kommen wir zu dem eingangs gezeigten Beispiel zurück. Wenn Sie vor dem Aufruf von Update ein SqlCommandBuilder-Objekt erzeugen und dessen Konstruktor die Instanz des SqlDataAdapters übergeben, wird die Aktualisierung erfolgreich sein.

[...]
SqlCommandBuilder cmb = new SqlCommandBuilder(da);
da.Update(ds);

Rheinwerk Computing - Zum Seitenanfang

35.1.1 Die von »SqlCommandBuilder« generierten AktualisierungsstatementsZur nächsten ÜberschriftZur vorigen Überschrift

SqlCommandBuilder erzeugt Aktualisierungscode auf Grundlage des SELECT-Statements. Doch wie sieht die Aktualisierungslogik exakt aus? Wir wollen uns das nun ansehen. Grundlage dazu bildet die Abfrage:

SELECT ProductID, ProductName, UnitsInStock FROM Products

Sie können sich die Aktualisierungsstatements ausgeben lassen, indem Sie die Methoden GetUpdateCommand, GetInsertCommand oder GetDeleteCommand des SqlCommandBuilders aufrufen. Alle liefern ein Objekt vom Typ SqlCommand, über dessen Eigenschaft CommandText Sie das jeweilige SQL-Statement abfragen können. Es genügt, wenn wir uns nur eines der drei ansehen.

UPDATE [Products]
SET [ProductName] = @p1, [UnitsInStock] = @p2
WHERE (([ProductID] = @p3) AND ([ProductName] = @p4) AND
((@p5 = 1 AND [UnitsInStock] IS NULL) OR ([UnitsInStock] = @p6)))

Listing 35.2 Vom CommandBuilder generiertes SQL-UPDATE-Statement

Sie erkennen, dass hinter der WHERE-Klausel alle Spalten der SELECT-Abfrage als Suchkriterium nach dem zu editierenden Datensatz aufgeführt sind. Die Parameter @p3 bis @p6 werden mit den Daten gefüllt, die unter DataRowVersion.Original aus dem Dataset bezogen werden, @p1 bis @p3 erhalten die Daten aus DataRowVersion.Current. In gleicher Weise werden auch die INSERT- und DELETE-Anweisungen vom SqlCommandBuilder generiert.


Rheinwerk Computing - Zum Seitenanfang

35.1.2 Konfliktsteuerung in einer MehrbenutzerumgebungZur nächsten ÜberschriftZur vorigen Überschrift

Meistens werden Datenbanken in einem Netzwerk betrieben. Das ist ein ganz wesentlicher Gesichtspunkt bei der Bereitstellung der SQL-Aktualisierungsstatements, denn in solchen Umgebungen müssen wir nun noch die Möglichkeit betrachten, dass ein zweiter User gleichzeitig mit denselben Daten arbeitet. Dann stellt sich auch sofort die Frage, was passiert, wenn versucht wird, eine Datenzeile zu aktualisieren, die ein anderer Benutzer zwischenzeitlich geändert hat. Möglicherweise tritt dabei ein Konflikt auf. Ob ein Konflikt auftritt, hängt ganz entscheidend davon ab, wie die WHERE-Klausel des Aktualisierungsstatements formuliert ist. Dabei müssen wir mehrere Fälle betrachten, die wir nun theoretisch untersuchen wollen.

Die WHERE-Klausel enthält alle Spalten

Betrachten wir sofort ein Beispiel, wenn alle Spalten der SELECT-Abfrage in der WHERE-Klausel angegeben sind, und nehmen wir an, Anwender A und Anwender B rufen praktisch gleichzeitig dieselbe Datenzeile in der Produkttabelle auf. Ändert Anwender A die Spalte ProductName, könnte das UPDATE-Statement beispielsweise wie folgt aussehen:

UPDATE Products
SET ProductName="Kuchen", UnitsInStock=18
WHERE ProductID=1 AND ProductName="Chai" AND
UnitsInStock=18

In der WHERE-Klausel sind den Spaltenangaben genau die Werte zugeordnet, die Anwender A aus der Datenbank bezogen hat. Anwender A aktualisiert erfolgreich, weil die Datenzeile, die von der WHERE-Klausel beschrieben wird, in der Datenbank gefunden wird.

Danach versucht Anwender B seine Aktualisierung der Datenbank mitzuteilen. Ob er dabei die Spalte ProductName geändert hat oder UnitsInStock, spielt keine Rolle. Entscheidend ist, dass die WHERE-Klausel die Spalten ProductID, UnitsInStock und ProductName enthält.

UPDATE Products
SET ProductName="Senf", UnitsInStock=56
WHERE ProductID=1 AND ProductName="Chai" AND
UnitsInStock=18

Der Aktualisierungsversuch wird scheitern. Schade, aber ein Datensatz mit dem Primärschlüssel 1 und den Spalteninhalten ProductName = Chai und UnitsInStock = 18 wird nicht mehr gefunden, weil ein Anwender den Produktnamen vorher geändert hat.

Immer dann, wenn beim Absetzen eines UPDATE- oder DELETE-Statements in der WHERE-Klausel eine Datenzeile beschrieben wird, die nicht in der Datenbank gefunden wird, haben wir es mit einem Konflikt zu tun.

Bei diesem Szenario »gewinnt« immer der Anwender, der als Erster seine Änderungen an die Datenbank übermittelt. Der Anwender, der seine Änderungen später zur Datenbank schickt, hat das Nachsehen. Sein Aktualisierungsversuch misslingt. Es kommt zu einem Parallelitätskonflikt. Dieses Szenario wird auch als First-in-wins bezeichnet.

Die WHERE-Klausel enthält nur die Primärschlüsselspalte

Betrachten wir nun einen anderen Fall. Wieder helfen uns die beiden fiktiven Anwender A und B dabei, den Sachverhalt zu verstehen. Beide Anwender rufen praktisch gleichzeitig dieselbe Datenzeile ab und nehmen Änderungen an einer der Spalten vor. Anwender A aktualisiert die Originaldatenbank zuerst, beispielsweise die Spalte ProductName des ersten Datensatzes:

UPDATE Products
SET ProductName="Marmorkuchen", UnitsInStock=56
WHERE ProductID=1

Anwender B übermittelt seine Änderung, nachdem Anwender A den ersten Datensatz geändert hat. Nehmen wir an, Anwender B hat den Inhalt in der Spalte UnitsInStock editiert, so könnte sein vollständiges Aktualisierungsstatement wie folgt lauten:

UPDATE Products
SET ProductName="Chai", UnitsInStock=56
WHERE ProductID=1

Die Aktualisierung wird erfolgreich sein, wenn der Datensatz mit der angegebenen ProductID in der Tabelle gefunden wird. Die Änderungen von Anwender A sieht Anwender B nicht; er wird vielleicht auch niemals erfahren, welche Daten Anwender A geändert hat, denn er überschreibt die Änderung von Anwender A in der Spalte ProductName mit dem alten Wert. Dieses Szenario, bei dem die letzte Änderung grundsätzlich immer erfolgreich an die Datenbank übermittelt werden kann, wird als Last-in-wins bezeichnet.

Die Identifizierung der zu ändernden Datenzeile nur anhand der Primärschlüsselspalte ist folglich denkbar ungeeignet, wenn Sie vermeiden müssen, dass Anwender B unwissentlich geänderte Daten überschreibt. Können Sie davon ausgehen, dass die letzte Aktualisierung zweifelsfrei diejenige mit den »besten« Daten ist, sollten Sie sich für diese Variante entscheiden.

Natürlich wird es auch hier zu einem Konfliktfall kommen, sollte Anwender A dieselbe Datenzeile nicht geändert, sondern bereits gelöscht haben.

Weitere Szenarien

Die beiden zuvor beschriebenen Szenarien stellen Grenzfälle dar. Zwischen diesen beiden gibt es, abhängig von der zugrunde liegenden Tabelle, unzählige weitere Optionen. Gehen wir beispielsweise wieder davon aus, dass zwei Benutzer mit

SELECT ProductID, ProductName, UnitsInStock FROM Products

Datenzeilen abrufen. Stellen wir uns weiter vor, dass der Artikelbestand UnitsInStock durchaus überschrieben werden darf, aber eine Änderung der Spalte ProductName nicht. Unabhängig davon, mit welchem UPDATE-Statement Benutzer A die Datenbank aktualisiert hat, muss unser fiktiver Benutzer B das folgende UPDATE-Statement zur Datenbank schicken:

UPDATE Products
SET ProductName="Kuchen", UnitsInStock=18
WHERE ProductID=1 AND ProductName="Chai"

Hat Anwender A nur den Lagerbestand UnitsInStock geändert, wird der Datensatz gefunden, und die in der SET-Klausel angegebenen Werte werden eingetragen. Dabei werden die neuen Daten des Anwenders A durch die alten Daten überschrieben, weil die Spalte UnitsInStock in der SET-Klausel von Anwender B enthalten ist.

Hat Anwender A jedoch den Artikelbezeichner ProductName editiert, kommt es zu einem Konflikt.

Selbstverständlich könnte man sich auch vorstellen, dass die SET-Klausel nur die veränderten Spaltenwerte beschreibt und die WHERE-Klausel neben der Angabe der Primärschlüsselspalte nur die Spalten, die als konfliktverursachend eingestuft werden und darüber hinaus auch verändert worden sind. Bezogen auf unser letztes Beispiel sollte das UPDATE-Statement wie folgt aussehen:

UPDATE Products
SET ProductName="Kuchen"
WHERE ProductID=1 AND ProductName="Chai"

Jetzt wird die Spalte UnitsInStock nicht mehr in der SET-Klausel angeführt. Falls Anwender A diese Spalte geändert hat, ist seine Aktualisierung weiterhin gültig, und in der Datenzeile wird nur der Produktbezeichner editiert.

Sollten die beiden Extremszenarien Last-in-wins und First-in-wins nicht Ihren Anforderungen an die Aktualisierung entsprechen, öffnet sich ein weites Feld der Möglichkeiten, das mit der Komplexität einer Tabelle größer wird. Hier bedarf es sicherlich einer gründlichen Analyse, was im Einzelfall als Konflikt zu betrachten ist.


Rheinwerk Computing - Zum Seitenanfang

35.1.3 Die Eigenschaft »ConflictOption« des »SqlCommandBuilders«Zur nächsten ÜberschriftZur vorigen Überschrift

Grundsätzlich ist das Aktualisieren einer Datenquelle mit dem SqlCommandBuilder-Objekt sehr einfach. Aber diese Einfachheit hat ihren Preis, denn wir müssen uns mit den Charakteristiken des Objekts abfinden und haben nur wenig Einfluss darauf, wie die Daten zurückgeschrieben werden. Der SqlCommandBuilder generiert, wie Sie weiter oben gesehen haben, Abfragen, die zur Identifikation einer Datenzeile in der Tabelle einer Datenbank alle Spalten einschließen, die mit SELECT abgefragt worden sind. Er bildet demnach per Vorgabe das First-in-wins-Szenario ab. Eine Änderung der Daten in der Datenbank führt zu der Ausnahme DBConcurrencyException, wenn ein anderer User eine dieser Spalten genau in dem Zeitraum verändert hat, in dem die ursprünglichen Daten für die Zeile abgerufen und neue Werte für die Zeile übermittelt werden.

Dieses Verhalten ist nicht immer wünschenswert. Daher stellt Ihnen der SqlCommandBuilder mit der Eigenschaft ConflictOption eine Möglichkeit zur Verfügung, das Aktualisierungsverhalten zu beeinflussen. Die Eigenschaft ist vom Typ der gleichnamigen Enumeration, deren Mitglieder Sie Tabelle 35.1 entnehmen können.

Tabelle 35.1 Die Enumeration »ConflictOption«

Konstante Beschreibung

CompareAllSearchableValues

UPDATE- und DELETE-Anweisungen schließen alle Spalten aus der Tabelle, nach denen gesucht werden kann, in die WHERE-Klausel ein. Das ist der Standard.

CompareRowVersion

Wenn in der Tabelle eine Timestamps-Spalte vorhanden ist, wird sie in der WHERE-Klausel für alle generierten UPDATE-Anweisungen verwendet.

OverwriteChanges

Alle UPDATE- und DELETE-Anweisungen enthalten nur die Spalten des Primärschlüssels in der WHERE-Klausel.

Der Wert ConflictOption.CompareAllSearchableValues ist der Standardwert. In diesem Szenario wird immer die erste Änderung in einer Datenzeile zum Erfolg, die dann folgende Änderung zu einem Konflikt führen. Dieses Szenario entspricht dem First-in-wins-Szenario. Das ist die Vorgabe.

Mit ConflictOption.OverwriteChanges teilen Sie dem SqlCommandBuilder mit, nur die Primärschlüsselspalte(n) in die WHERE-Klausel einzubeziehen. Das hat zur Folge, dass die Änderungen des ersten Benutzers von den nachfolgenden Änderungen überschrieben werden. Diese Einstellung bildet das Last-in-wins-Szenario ab.

Ein Timestamp ist ein automatisch generierter, eindeutiger 8-Byte-Wert. Mit Hilfe der Timestamp-Spalte einer Zeile können Sie sehr einfach ermitteln, ob sich ein Wert in der Datenzeile geändert hat, seit er eingelesen wurde. Der Timestamp-Wert wird bei jeder Aktualisierung geändert. Ist er beim Absetzen des UPDATE-Statements identisch, liegt keine andere zwischenzeitliche Aktualisierung vor. Mit ConflictOption.CompareRowVersion weisen Sie den SqlCommandBuilder an, in der WHERE-Klausel nur die Primärschlüsselspalte(n) und die Timestamp-Spalte aufzunehmen.


Rheinwerk Computing - Zum Seitenanfang

35.1.4 Die Eigenschaft »SetAllValues«Zur vorigen Überschrift

Betrachten Sie noch einmal das Beispiel mit Benutzer A und Benutzer B. Es wäre denkbar, dass weder das Last-in-wins- noch das First-in-wins-Szenario die Forderung passend erfüllt.

Vielleicht soll auch jede Änderung an einer Datenzeile akzeptiert werden, solange dieselbe Spalte nicht von einem anderen User verändert worden ist. Das bedeutet, dass in der WHERE-Klausel neben dem Primärschlüssel auch die jeweils geänderte Spalte mit ihrem Ursprungswert angegeben werden muss. Benutzer A müsste in einem solchen Fall das folgende UPDATE absetzen:

UPDATE Products
SET ProductName = "Cheese"
WHERE ProductID = 55 AND ProductName = "Käse"

Ändert Benutzer B unter gleichen Voraussetzungen die Spalte UnitsInStock, wird diese Spalte seinem UPDATE-Statement hinzugefügt:

UPDATE Products
SET UnitsInStock = 2
WHERE ProductID = 55 AND UnitsInStock = 13

Die Aktualisierung wird erfolgreich sein. Mehr noch, die betroffene Datenzeile in der Datenbank wird beide Änderungen aufweisen.

Setzen Sie die Eigenschaft SetAllValues des SqlCommandBuilders auf false, werden neben der Primärschlüsselspalte nur die Spalten der WHERE-Klausel als Suchkriterium hinzugefügt, deren Inhalte sich verändert haben. Das entspricht genau dem gezeigten Muster.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück
  Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Visual C# 2012

Visual C# 2012
Jetzt Buch bestellen


 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Rheinwerk-Shop: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Rheinwerk-Shop: Windows Presentation Foundation






 Windows Presentation
 Foundation


Zum Rheinwerk-Shop: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Rheinwerk-Shop: C++ Handbuch






 C++ Handbuch


Zum Rheinwerk-Shop: C/C++






 C/C++


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo





Copyright © Rheinwerk Verlag GmbH 2013
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