Mehrschichtige Anwendungen

Lies diesen Artikel und viele weitere mit einem kostenlosen, einwöchigen Testzugang.

Access ist – rein architektonisch betrachtet – auf das Anlegen monolithischer Anwendungen mit Objekten mit starker Abhängigkeit ausgelegt. Formulare und Berichte sind starr mit den zu Grunde liegenden Datenherkünften verbunden und der Code liegt jeweils im passenden Formular- oder Berichtsmodul oder, falls nötig, in einem Standard- oder Klassenmodul. Wie man auch mit Access mehrschichtige Anwendungen baut, zeigt dieser Beitrag.

Im Gegensatz zu objektorientierten Programmiersprachen gibt es in Access-Anwendungen keine einzelnen Dateien, die unterschiedliche Klassen repräsentieren, sondern nur eine einzige Datei mit der Endung .mdb, die alle Objekte enthält.

Wenn Sie hier eine mehrschichtige Anwendung bauen möchten und dies von richtigen objektorientierten Programmiersprachen gewohnt sind, müssen Sie von dem Gedanken Abschied nehmen, jede Klasse fein säuberlich in einer eigenen Datei zu speichern.

Davon abgesehen funktioniert das Programmieren mehrschichtiger Anwendungen aber genauso wie in anderen objektorientierten Programmiersprachen – nur dass Sie auf Eigenschaften wie Vererbung und Polymorphie verzichten müssen und alle Klassen in der .mdb-Datei enthalten sind.

Ein Grund, eine mehrschichtige Anwendung zu erstellen, ist die starke Verzahnung von Benutzeroberfläche und Anwendungslogik. Das gilt vor allem für den in Formularen und Berichten enthaltenen Code, der nicht nur die Benutzeroberfläche steuert, sondern auch die Businesslogik und die Funktionen für den Datenzugriff enthält. Letztere sind dabei noch tiefer verborgen, nämlich in den passenden Formular-, Berichts- und Steuerelementeigenschaften, die für die Bindung an die entsprechenden Tabellen und Abfragen sorgen. Teilweise befindet sich die Businesslogik sogar direkt in den Tabellen – etwa in Form der Gültigkeitsregeln und der übrigen Integritätsmechanismen wie der referentiellen Integrität und der darin enthaltenen Lösch- und Aktualisierungsweitergabe.

Wegen dieser Aufteilung kann die Pflege solcher Anwendungen sehr zeitintensiv werden: Wenn Sie etwa eine Meldung, die auf einen falschen Datentyp bei der Eingabe in ein Formularfeld hinweist, ändern oder entfernen möchten, sind Sie unter Umständen lange unterwegs, da sich der Auslöser in verschiedenen Ereignissen des Formulars oder auch im Tabellenentwurf befinden kann.

Ganz klar: Wer eine konsistente Vorgehensweise bei der Programmierung von Anwendungen an den Tag legt, der wird auch genau wissen, wo in seiner Anwendung sich welche Funktionen befinden. Aber erstens kann es ja auch sein, dass sich mal jemand anders um die Wartung oder Weiterentwicklung der Anwendung kümmern muss, und zweitens entwickeln Sie sich selbst ja auch weiter und erkennen nach einer gewissen Zeit die funktionale Struktur früherer Anwendungen nicht mehr wieder. Und Access ist natürlich zunächst einmal dafür ausgelegt, auf schnellstem Wege Anwendungen für den Zugriff auf und die Verwaltung von Daten zu entwickeln. Wenn eine Anwendung allerdings ein gewisses Maß an Komplexität überschritten hat und man für kleine Ônderungen beinahe genauso lange braucht, als wenn man die halbe Anwendung neu programmieren würde, sollte man über alternative Vorgehensweisen nachdenken.

Diese liegen beispielsweise in der Verwendung eines mehrschichtigen Datenmodells. Solche Modelle gibt es in mehreren Varianten mit unterschiedlicher Interpretation.

Im Folgenden lernen Sie ein Modell kennen, das je nach Sichtweise aus drei oder vier Schichten besteht: Der Benutzeroberfläche (GUI-Schicht), der Business-Schicht, der Datenzugriffsschicht und den Daten. Manch einer betrachtet die Datenzugriffsschicht und die Daten als Einheit, andere sehen zwei Schichten darin. Im Rahmen dieses Beitrags werden Datenzugriffsschicht und Daten als zwei Schichten betrachtet.

Beispiel für den mehrschichtigen Datenzugriff

Als Beispiel für den mehrschichtigen Datenzugriff dienen eine Tabelle namens tblPersonal (siehe Bild 1) und ein Formular namens frmPersonal (siehe Bild 2). Dabei verwenden wir aus Gründen der Übersicht nur einige der in der Tabelle enthaltenen Felder.

pic001.tif

Bild 1: Diese Tabelle dient als Datenherkunft des Formulars.

pic002.tif

Bild 2: Das Formular zum Anzeigen der Daten ist ungebunden.

Das Formular ist komplett ungebunden und enthält die folgenden Steuerelemente:

  • cboSchnellsuche: dient der Auswahl von Mitarbeitern
  • txtPersonalID: schreibgeschützt
  • txtVorname, txtNachname, txtPosition, txtAnrede, txtEinstellung: Textfelder der Tabelle
  • cmdOK: Schaltfläche zum Schließen des Formulars und zum Speichern des aktuellen Inhalts
  • cmdLoeschen: Schaltfläche zum Löschen des aktuellen Datensatzes
  • cmdNeu: Schaltfläche zum Anlegen eines neuen Datensatzes

Die GUI-Schicht

Die GUI-Schicht – also die Benutzeroberfläche – bildet für das nachfolgende Beispiel das Formular frmPersonal aus Bild 2. Die GUI-Schicht enthält nur Methoden für den Zugriff auf die Business-Schicht, auf keinen Fall kann sie direkt auf eine der darunter liegenden Schichten zugreifen. Das ist bei herkömmlichen Access-Anwendungen der Fall: Hier werden Datenherkunft und Steuerelemente direkt an die Datenschicht gebunden. Andersherum kann keine der anderen Schichten auf die GUI-Schicht zugreifen – das ist eine der Hauptprämissen bei der Entwicklung mehrschichtiger Anwendungen. Sie minimieren damit die Abhängigkeit, indem Sie dafür sorgen, dass diese lediglich einseitig ist.

Beim hier verwendeten Schichtenmodell greift die GUI-Schicht ausschließlich auf die Objekte der Business-Schicht zu: auf die Datenobjekte mit den anzuzeigenden Informationen und auf das Controller-Objekt, das die Funktionen zum Füllen der Datenobjekte, zum Speichern der per GUI-Schicht geänderten Inhalte der Datenobjekte, zum Speichern neuer Datenobjekte und zum Löschen bestehender Datenobjekte bereitstellt.

Die Business-Schicht

Die Business-Schicht enthält zwei Typen von Objekten: Der erste Typ repräsentiert die in den Datensätzen der Tabellen enthaltenen Daten (Daten-Objekte), der zweite enthält die Steuermechanismen für den Transfer der Daten zwischen der Datenzugriffsschicht und der GUI-Schicht (Controller-Objekte).

Genau genommen ist das nicht ganz richtig: Die Controller-Objekte steuern zwar die Objekte der Datenzugriffsschicht und andere Objekte der Business-Schicht, aber die Kooperation zwischen der GUI-Schicht und den Controller-Objekten geht immer von der GUI-Schicht aus. Wie bereits erwähnt – die oberen Schichten können zwar auf die unteren zugreifen, aber niemals umgekehrt.

Und da auch nie eine Schicht übersprungen werden darf, muss die GUI-Schicht immer über die Business-Schicht auf die Datenzugriffsschicht zugreifen, die dann die gewünschten Daten nach oben reicht.

Wie viele und welche Objekte sich in der Business-Schicht befinden, hängt von der Art der enthaltenen Daten und der Benutzeroberfläche ab. Sie werden vermutlich für jede Tabelle, die objektartige Daten enthält, ein eigenes Objekt erstellen. Außerdem müssen Sie entscheiden, ob Sie ein Controller-Objekt pro Element der Benutzeroberfläche oder vielleicht sogar ein großes Controller-Objekt verwenden. Übersichtlicher dürfte ein Objekt pro Formular sein.

Die Datenzugriffsschicht

Die Datenzugriffsschicht enthält Datenzugriffsobjekte. Zu jedem Datenobjekt gibt es ein Datenzugriffsobjekt, das verschiedene Operationen ausführen kann:

  • Erzeugen eines Datenobjekts auf Basis eines Datensatzes der zu Grunde liegenden Tabelle
  • Erzeugen eines Recordsets mit Datensätzen als Suchergebnis mit vorgegebenen Kriterien
  • Aktualisieren eines Datensatzes in der Datenbank auf Basis der in einem Datenobjekt enthaltenen Daten
  • Anlegen eines neuen Datensatzes in der Datenbank
  • Löschen eines Datensatzes aus der Datenbank

Damit entkoppelt die Datenzugriffsschicht die Business-Schicht von den Daten. Der Vorteil liegt darin, dass Sie ohne Probleme die Datenquelle wechseln können – etwa, um von einem Access-Backend auf einen SQL-Server umzusteigen oder vielleicht sogar um eine XML-Datei als Datenquelle zu verwenden.

Sie müssen lediglich die Klassen der Datenzugriffsschicht anpassen – die GUI-Schicht und die Business-Schicht bleiben von einem Wechsel der Datenquelle unberührt.

Im Beispiel erfahren Sie, wie die fünf Operationen eines Datenzugriffsobjekts aussehen. Man benötigt je ein Datenzugriffsobjekt pro Businessobjekt der Business-Schicht.

Das Beispiel verwendet lediglich ein Datenzugriffsobjekt für den Zugriff auf die Datenbank per DAO. Sie können alternativ ein Datenzugriffsobjekt mit den gleichen Methoden, aber anderen Anweisungen für den Zugriff auf die Daten verwenden, um etwa die ADODB-Bibliothek statt der DAO-Bibliothek zu verwenden. Oder Sie erstellen ein drittes Datenzugriffsobjekt, das den Zugriff auf eine XML-Datei über das mit der Bibliothek MSXML gelieferte Document Object Model ermöglicht. Wenn Sie bezüglich des Datenzugriffs derart flexibel sein möchten, empfiehlt sich die Verwendung einer Schnittstelle.

Die Datenschicht

Die Datenschicht enthält die eigentlichen Daten. Im vorliegenden Beispiel ist das eine Tabelle in einer Access-Datenbank, es kann sich aber auch um eine Tabelle in einer SQL-Server-Datenbank oder um eine XML-Datei handeln.

Diese Flexibilität erhalten Sie durch die Aufteilung der Anwendung auf verschiedene Schichten – um etwa auf eine XML-Datei statt auf eine Access-Datenbank zuzugreifen, müssten Sie nur die Objekte der Datenzugriffsschicht anpassen. Die Benutzeroberfläche und die Business-Schicht bleiben unangetastet.

Beispielanwendung

Nach der Theorie zurück zum Beispiel: Das Formular aus Bild 2 soll nun also über die einzelnen Schichten auf die in der Datenbank gespeicherten Daten zugreifen, diese anzeigen, neue Datensätze anlegen oder bestehende Datensätze ändern oder löschen. Beginnen wir also einfach mal am Anfang: den Zustand beim Öffnen des Formulars.

Person-Objekt erstellen

Während der Arbeit mit dem Formular werden die Daten der Mitarbeiter, wie bereits erwähnt, nicht direkt aus der Tabelle ins Formular transportiert, sondern über ein spezielles Objekt, das im Prinzip mit einem Datensatz einer Tabelle vergleichbar ist, mit dem Unterschied, dass es statt Felder Eigenschaften aufweist und dass Sie seine Methoden selbst definieren können beziehungsweise müssen. Da Sie dieses Objekt auf Basis einer Klasse erzeugen und die Beschreibung dieser Klasse die folgenden Erläuterungen unnötig behindern würde, legen Sie diese Klasse schnell vorab an. Die Klasse heißt clsPerson und enthält den Code aus Listing 1. Dazu gehören lediglich die Membervariablen, die die Eigenschaften speichern, sowie zu jeder Membervariablen eine Property Get– und eine Property Let-Methode. Wenn Sie sich noch nicht mit der Programmierung von Klassen auskennen, finden Sie in [1] weitere Informationen.

Listing 1: Code der Klasse clsPerson

Option Compare Database
Option Explicit
Dim mPersonalID As Long
Dim mVorname As String
Dim mNachname As String
Dim mPosition As String
Dim mAnrede As String
Dim mEinstellung As Date
Public Property Get PersonalID() As Long
    PersonalID = mPersonalID
End Property
Public Property Let PersonalID(lngPersonalID As Long)
    mPersonalID = lngPersonalID
End Property
Public Property Get Vorname() As String
    Vorname = mVorname
End Property
Public Property Let Vorname(strVorname As String)
    mVorname = strVorname
End Property
Public Property Get Nachname() As String
    Nachname = mNachname
End Property
Public Property Let Nachname(strNachname As String)
    mNachname = strNachname
End Property
Public Property Get Position() As String
    Position = mPosition
End Property
Public Property Let Position(strPosition As String)
    mPosition = strPosition
End Property
Public Property Get Anrede() As String
    Anrede = mAnrede
End Property
Public Property Let Anrede(strAnrede As String)
    mAnrede = strAnrede
End Property
Public Property Get Einstellung() As Date
    Einstellung = mEinstellung
End Property
Public Property Let Einstellung(strEinstellung As Date)
    mEinstellung = strEinstellung
End Property

Initialisieren des Formulars

Direkt nach dem Öffnen soll das Formular keinen Datensatz anzeigen. Lediglich das Kombinationsfeld cboSchnellsuche soll alle enthaltenen Personen zur Auswahl anbieten. Das Füllen dieses Steuerelements ist dann auch die erste Funktion, die programmiert und auf mehrere Schichten aufgeteilt werden soll.

Der Beginn sieht unspektakulär aus: Die beim Öffnen des Formulars ausgelöste Routine initialisiert das im Kopf des Moduls deklarierte Controller-Objekt und ruft die Prozedur cboSchnellsucheAktualisieren auf. Das Controller-Objekt befindet sich im Übrigen in einer eigenen Klasse, die Sie weiter unten erstellen. Den Code aus Listing 2 speichern Sie im Klassenmodul des Formulars frmPersonal. Außerdem müssen Sie noch die Eigenschaft Beim Öffnen auf den Wert [Ereignisprozedur] einstellen.

Listing 2: Initialisieren des Formulars

Dim objController As clsController
Private Sub Form_Open(Cancel As Integer)
    Set objController = New clsController
    cboSchnellsucheAktualisieren
End Sub

Die Prozedur cboSchnellsucheAktualisieren soll das Kombinationsfeld mit den in der Tabelle tblPersonal enthaltenen Daten füllen. Unter Access brauchen Sie dafür keine einzige Codezeile, sondern weisen einfach der Eigenschaft Datensatzherkunft eine passende Abfrage zu. Bei der Programmierung einer mehrschichtigen Anwendung wird das Ganze hingegen schon mächtig interessant – Sie füllen dazu zunächst in der Datenzugriffsschicht ein Collection-Objekt, das dann nach oben gereicht und von der GUI-Schicht in das Kombinationsfeld gefüllt wird.

Fangen Sie also ganz unten an und erstellen Sie eine Klasse namens clsPersonal_Datenzugriffsobjekt. Dieser fügen Sie eine Methode namens Find hinzu, die nichts anderes erledigen soll, als eine Collection mit allen in der Tabelle tblPersonal enthaltenen Mitarbeitern zurückzuliefern. Um späteren Anwendungszwecken vorzubeugen, fügen Sie dieser direkt einen optionalen Parameter namens varSearch hinzu, dem Sie eine Zeichenkette mit einer WHERE-Klausel übergeben können – dazu jedoch später mehr. Die komplette Routine finden Sie in Listing 3. Sie durchläuft in einer Do While-Schleife alle in der Tabelle gespeicherten Datensätze und schreibt diese in jeweils ein eigenes Objekt des Typs clsPerson. Jedes dieser Objekte landet dann in der Collection objPersonen. Wichtig ist hierbei, dass beim Anfügen der Objekte an die Collection der Primärschlüsselwert des Datensatzes mit dem zweiten Parameter der Add-Methode der Collection als Schlüssel des aktuellen Elements angehängt wird. Anderenfalls können Sie die Elemente der Collection später nicht mehr identifizieren. Die Collection dient im Übrigen als Rückgabewert der Funktion Find an die aufrufende Instanz.

Listing 3: Die Find-Methode des Datenzugriffsobjekts clsPerson_Datenzugriffsobjekt

Public Function Find(Optional varSearch As Variant) As Collection
    ''On Error GoTo Find_Err
    Dim db As DAO.Database
    Dim rst As DAO.Recordset
    Dim strSQL As String
    Dim objPerson As clsPerson
    Dim objPersonen As Collection
    Set db = CurrentDb
    strSQL = "SELECT * FROM tblPersonal"
    If Not IsMissing(varSearch) Then
        strSQL = strSQL & " WHERE " & varSearch
    End If
    Set rst = db.OpenRecordset(strSQL, dbOpenDynaset)
    Set objPersonen = New Collection
    Do While Not rst.EOF
        Set objPerson = New clsPerson
        With objPerson
            .PersonalID = rst!PersonalID
            .Vorname = Nz(rst!Vorname, "")
            .Nachname = Nz(rst!Nachname, "")
            .Position = Nz(rst!Position, "")
            .Anrede = Nz(rst!Anrede, "")
            .Einstellung = Nz(rst!Einstellung, "")
        End With
        objPersonen.Add objPerson, CStr(objPerson.PersonalID)
        rst.MoveNext
    Loop
    Set Find = objPersonen
Find_Exit:
    On Error Resume Next
    Set db = Nothing
    Exit Function
Find_Err:
    GoTo Find_Exit
End Function

Wer ruft nun diese Funktion auf und wohin geht die Collection Nun, dafür ist das Controller-Objekt zuständig. Dieses legen Sie ebenfalls zunächst in Form einer eigenen Klasse namens clsController an.

Für den Aufruf der Find-Methode ist in diesem Fall die kleine Methode GetPersons zuständig. Diese Methode liefert ein Collection-Objekt zurück, das in dem Objekt objPersonen gespeichert wird. Die Methode sieht wie in Listing 4 aus.

Listing 4: Die Methode GetPersons der Klasse clsController

Public Function GetPersons() As Collection
    Set objPersonen = objPerson_Datenzugriffsobjekt.Find
    Set GetPersons = objPersonen
End Function

Nun kennt diese Methode das Objekt objPerson_Datenzugriffsobjekt aber noch gar nicht. Das wird ihm indirekt wiederum von der darüberliegenden Schicht, nämlich der GUI-Schicht mitgeteilt. Indirekt deshalb, weil diese das Controller-Objekt instanziert und dadurch das im Controller-Objekt enthaltene Ereignis Class_Initialize ausgelöst wird. Damit dies alles geschieht, fügen Sie auch noch die Deklarationen und die Prozedur Class_Initialize aus Listing 5 zur Klasse clsController hinzu.

Listing 5: Initialisieren der Klasse clsController der Business-Schicht

Dim objPerson_Datenzugriffsobjekt As clsPerson_Datenzugriffsobjekt
Dim objPersonen As Collection
Private Sub Class_Initialize()
    Set objPerson_Datenzugriffsobjekt = New clsPerson_Datenzugriffsobjekt
End Sub

Nun endlich können Sie auch der GUI-Schicht die für das Füllen des Kombinationsfelds notwendige Funktionalität hinzufügen. Diese manifestiert sich in Form der Routine cboSchnellsucheAktualisieren und kann nicht nur beim Öffnen des Formulars, sondern auch durch andere Ereignisse aufgerufen werden. Die Routine aus Listing 6 deklariert zunächst ein Objekt des Typs clsPerson und eine Collection. Die oben vorgestellte Methode GetPersons des Controller-Objekts liefert (über die Find-Methode des Datenzugriffsobjekts) die Collection mit den einzelnen Personen-Objekten. Diese durchläuft die Routine in einer For Each-Schleife und trägt den Vor- und den Nachnamen in eine Semikola-separierte Zeichenkette ein. Diese muss die Routine nun nur noch der Datensatzherkunft des Kombinationsfelds zur Schnellauswahl zuweisen – fertig! Natürlich müssen Sie die Eigenschaft Herkunftstyp des Kombinationsfelds noch auf Wertliste sowie die beiden Eigenschaften Spaltenanzahl und Spaltenbreite auf 2 und 0cm einstellen.

Listing 6: Zuweisen einer Datensatzgruppe mit allen Personen an das Kombinationsfeld zur Schnellsuche

Private Sub cboSchnellsucheAktualisieren()
    Dim objPerson As clsPerson
    Dim objPersonen As Collection
    Dim str As String
    Set objPersonen = objController.GetPersons
    If Not objPersonen Is Nothing Then
        For Each objPerson In objPersonen
            str = str & objPerson.PersonalID & ";" & objPerson.Nachname _
                & ", " & objPerson.Vorname & ";"
        Next objPerson
        Me.cboSchnellsuche.RowSource = str
    Else
        MsgBox "Personenliste konnte nicht geladen werden."
    End If
End Sub

Wenn Sie nun das Formular in der Formularansicht öffnen und das Kombinationsfeld aufklappen, sollte sich etwa ein Bild wie in Bild 3 präsentieren. Glückwunsch! Sie haben Ihr erstes Kombinationsfeld mittels objektorientierter, mehrschichtiger Datenbankentwicklung gefüllt. Zugegebenermaßen ist der Aufwand nicht gerade gering, aber die fehlende Datenbindung und die gewünschte Flexibilität erfordern diesen Weg. Nun geht es aber schnell weiter – immerhin soll der ausgewählte Datensatz ja auch noch komplett im Formular angezeigt werden.

pic003.tif

Bild 3: Kombinationsfeld mit Daten aus einer Collection von Personen-Objekten

Auswählen und anzeigen eines Datensatzes

Beim Füllen des Formulars kommt wiederum die Klasse clsPerson ins Spiel. Sie dient als Transportmittel der Daten aus der immer noch in der Klasse objController befindlichen Collection objPersonen.

Ja, genau: Nach der Auswahl des anzuzeigenden Datensatzes aus dem Kombinationsfeld greift die Anwendung nicht etwa über die Zwischenschichten auf die Datenschicht zu, sondern bezieht die Informationen aus der im Controller zwischengespeicherten Collection, die alle im Kombinationsfeld auswählbaren Personen in Form von Objekten des Typs clsPerson enthält. Die Ereigniseigenschaft Nach Aktualisierung löst dabei über die Prozedur aus Listing 7 die Routine in Listing 8 aus.

Listing 7: Diese Prozedur ruft die eigentliche Routine zum Laden eines Mitarbeiters auf.

Private Sub cboSchnellsuche_AfterUpdate()
    MitarbeiterLaden
End Sub

Listing 8: Diese Routine wird nach der Auswahl eines Eintrags des Kombinationsfelds aufgerufen.

Private Sub MitarbeiterLaden()
    Dim objPerson As clsPerson
    Set objPerson = objController.LoadPerson(Me!cboSchnellsuche)
    objController.SavePerson Nz(Me!txtPersonalID, 0), Me!txtVorname, _
         Me!txtNachname, Me!txtPosition, Me!txtAnrede, Me!txtEinstellung
    With objPerson
        Me!txtPersonalID = .PersonalID
        Me!txtVorname = .Vorname
        Me!txtNachname = .Nachname
        Me!txtPosition = .Position
        Me!txtAnrede = .Anrede
        Me!txtEinstellung = .Einstellung
    End With
End Sub

Und das sieht so aus: Nach dem Deklarieren der Objektvariablen objPerson wird diese mit Hilfe der Methode LoadPerson des Controller-Objekts (siehe weiter unten) gefüllt. Als Parameter wird dabei das gebundene Feld des Kombinationsfeldes übergeben, das die PersonalID des anzuzeigenden Mitarbeiters enthält (s. Listing 9).

Listing 9: Weiterdelegieren des Ladens von Personendaten in das entsprechende Objekt

Public Function LoadPerson(lngPersonalID As Long) As clsPerson
    Dim objPerson As clsPerson
    Set objPerson = objPersonen(CStr(lngPersonalID))
    If objPerson Is Nothing Then
        Set LoadPerson = objPerson_Datenzugriffsobjekt.Read(lngPersonalID)
    Else
        Set LoadPerson = objPerson
    End If
End Function

Zum Füllen der Eigenschaften mit den Inhalten der Felder des Datensatzes mit der gesuchten PersonalID ist die Methode LoadPerson des Controller-Objekts zuständig.

Diese Methode deklariert zunächst ein Objekt des Typs clsPersonen und weist diesem das Objekt aus der Collection objPersonen mit dem passenden Wert der Eigenschaft PersonID zu, der in der Collection als Schlüsselwert eines jeden Elements gespeichert ist.

Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

Testzugang

eine Woche kostenlosen Zugriff auf diesen und mehr als 1.000 weitere Artikel

diesen und alle anderen Artikel mit dem Jahresabo

Schreibe einen Kommentar