Die nächste Lernzusammenfassung, heute zum Thema Datenbanksysteme, das sich um relationale Datenbanken, deren Formalisierung, Modellierung und SQL drehen wird.
Inhalt:
1 – Grundlagen
2 – Relationale Algebra
3 – Relationenkalkül
4 – Joins und weitere Operationen
5 – Views
6 – E/R-Modell
7 – Normalisierung
8 – Transaktionen
1 – Grundlagen
Relationale Datenbanken bestehen aus Relationen. Relationen kann man als Tabelle sehen, als Teilmenge des kartesischen Produkts von Domains oder als Ausprägung eines Relationenschemas. Domains sind in dem Fall die Spalten einer Tabelle bzw. Attribute und geben einen Wertebereich bzw. Typ vor. Als Grad oder auch Stelligkeit bezeichnet man die Anzahl der Domains bzw. Spalten. Die Zeilen werden Tupeln genannt.
Die Reihenfolge der Tupeln ist egal. Für die Reihenfolge der Attribute gilt das nicht, da sie durch ein Schema vorgegeben ist. Eine Ausnahme hiervon ist das ungeordnete Relationenschema, bei dem die Domain über ihren Namen referenziert wird (z.B. tupel3(domain2) = ‘Eintrag’).
Um Tupeln referenzieren zu können, benötigt man einen eindeutigen Identifikator, auch Schlüssel genannt. Ein Schlüssel ist wie folgt definiert:
Eine Teilmenge S der Attribute eines Relationenschemas R heißt Schlüssel, wenn gilt:
1. Eindeutigkeit: Keine Ausprägung von R kann zwei verschiedene Tupel enthalten, die sich in allen Attributen von S gleichen.
2. Minimalität: Keine echte Teilmenge von S erfüllt bereits die Bedingung der Eindeutigkeit.
Ein Schlüssel, der nur eindeutig ist, heißt Superschlüssel.
Oder mathematischer:
Sei r eine Relation über dem Relationenschema R und S eine Auswahl von Attributen und Teilmenge von R und t[S] ein Tupel t eingeschränkt auf die Attribute aus S (also alle anderen Attribute gestrichen). Dann muss für einen Schlüssel gelten:
1. Eindeutigkeit: Für alle möglichen Ausprägungen r und t1, t2 Element von r gilt: t1 =/= t2 => t1[S] =/= t2[S].
2. Minimalität: Für alle Attributmengen S und T, die die Eindeutigkeit erfüllen, gilt: Wenn T eine Teilmenge von S ist, dann muss T = S sein.
Es stellt sich natürlich die Frage, warum man unbedingt Datenbanken braucht. Man könnte ja stattdessen auch Dateien benutzen. Dateien haben allerdings zahlreiche Nachteile:
- Bei einer Änderung des Dateiformates (z.B. neue Attribute) müssen alte Dateien konvertiert werden und nutzende Programme angepasst werden (logische Datenabhängigkeit).
- Daten werden anhand ihrer Position adressiert => Die Suche nach bestimmten Attributwerten muss im Programm kodiert werden (physische Datenabhängigkeit).
- Die Redundanz bei der Datenhaltung führt zu Änderungsanomalien (später mehr dazu), es müssen viele Dateien nach Einträgen durchsucht werden.
- Es gibt eine Schnittstellenproblematik, die zur Unübersichtlichkeit bei mehrfachen Zugriffen führt, und allgemein keine Zugriffssynchronisation.
- Es gibt keinen ausreichenden Schutz vor Datenverlust bei Systemabstürzen oder anderen Defekten.
- Es gibt nur eine unflexible Zugriffskontrolle, der Datenschutz ist mangelhaft.
Deswegen verwendet man bei größeren geordneten Datenmengen ein Datenbanksystem, gerade wenn man spezielle Anforderungen hat. Diese können sein…
- Integration: Einheitliche Verwaltung aller von Anwendungen benötigten Daten, redundanzfreie Datenhaltung
- Operationen: O. zur Speicherung, Recherche und Manipulation der Daten müssen vorhanden sein
- Data Dictionary: Ein Katalog erlaubt Zugriffe auf die Beschreibung der Daten
- Benutzersichten bzw. Views: Für unterschiedliche Anwendungen unterschiedliche Sichten auf den Bestand
- Konsistenzüberwachung: DBMS überwacht die Korrektheit der Daten bei Änderungen
- Zugriffskontrolle: Ausschluss unautorisierter Zugriffe
- Transaktionen: Zus.fassung einer Folge von Änderungsoperationen zu einer Einheit, deren Effekt bei Erfolg permanent in die Datenbank gespeichert wird
- Synchronisation: Vermeidung von unbeabsichtigten gegenseitigen Beeinflussungen bei mehreren gleichzeitigen Datenbanknutzern
- Datensicherung: Nach System- oder Medienfehlern wird die Wiederherstellung ermöglicht (Rekonstruktion des Zustands der letzten erfolgreichen Transaktion)
Ein Datenbanksystem kann man in 2 oder 3 Ebenen unterteilen.
Das 2-Ebenen-Modell kennt eine Intensionale und eine Extensionale Ebene. Das Datenbankschema wird als Intensionale Ebene bezeichnet. Es beschreibt den möglichen Inhalt, Struktur- und Typinformationen (Metadaten) und das Datenmodell. Änderungen sind möglich, aber selten. Die Extensionale Ebene ist die Ausprägung der Datenbank. Dabei handelt es sich um den tatsächlichen Inhalt, Objektinformationen und Attributwerte. Die Struktur ist durch das Schema vorgegeben. Änderungen kommen häufig vor.
Das 3-Ebenen-Modell teilt das Datenbanksystem in externe, konzeptionelle und interne Ebene. Die externe Ebene ist die Sammlung der individuellen Sichten aller Nutzer (bzw. externer Schemata) und zeigt, was der Nutzer sehen will, und zeigt nicht, was er nicht sehen soll. Die konzeptionelle Ebene ist die logische Gesamtsicht aller Daten, die mit einer Data Definition Language (DDL) beschrieben wird (Schema). Es handelt sich also um das Datenmodell des Datenbanksystems. Die interne Ebene bezieht sich auf die physische Speicherung der Datensätze, also auf das interne Schema (sprich Indexstrukturen etc.).
Eine DDL dient zu Deklarationen zur Beschreibung des Schemas, also bei relationalen Datenbanken z.B. zum Anlegen und Löschen von Tabellen, Festlegen von Integritätsbedingungen, etc.. Eine DML (Data Manipulation Language) bietet hingegen Anweisungen zum Arbeiten mit den Daten in der Datenbank.
Zu unterscheiden sind zwei Arten der Datenunabhängigkeit, die oben schon erwähnt wurden:
1. Logische Datenunabhängigkeit: Die Datenbank wird von Änderungen der Anwenderschnittstellen entkoppelt.
2. Physische Datenunabhängigkeit: Die Anwendungen sind von Änderungen des internen Schemas nicht betroffen.
2 – Relationale Algebra
Die relationale Algebra ist die mathematische Formalisierung von Tabellen und darauf möglichen Operationen. Sie ist abgeschlossen, d.h. Operationsergebnisse sind wiederum Elemente der Menge. Grundoperationen der relationen Algebra sind Vereinigung, Differenz, Kartesisches Produkt, Selektion (Auswahl von Zeilen) und Projektion (Auswahl von Spalten).Der Durchschnitt lässt sich als Kombination von Differenzen modellieren (A n B = A-(A-B) ). Joins sind Kombinationen von Selektion und Kreuzprodukt. Die auftretende Duplikatelimination ist kostenintensiv, der Aufwand beträgt O(n²) bzw. nach Sortieren O(n log n). Eine weitere Operation ist der Quotient, der die Umkehrung des Kartesischen Produkts darstellt.
SQL-Anfragen entsprechen den Grundoperationen. SELECT steht für eine Projektion, FROM für das Kreuzprodukt, WHERE für eine Selektion, UNION erlaubt Vereinigung mit Duplikatelimination und EXCEPT die Differenz. Zwar sind in SQL im Gegensatz zur Relationalen Algebra die Operatoren nicht beliebig schachtelbar, trotzdem sind aber alle algebraischen Ausdrücke darstellbar. Man nennt es deswegen relational vollständig.
3 – Relationenkalkül
Während die Relationale Algebra beschreibt, wie man zum Ergebnis kommt (prozedural), zeigt das Relationenkalkül, was das Ergebnis sein soll (deklarativ). Es gibt zwei Kalküle, nämlich das Tupelkalkül (mit angegebenem Schema) und das Bereichskalkül.
Das Tupelkalkül fokussiert sich auf die Ergebnistupel und beschreibt ihre Eigenschaften. Das sieht beispielsweise so aus:
Schema(t) = Schema(Städte) ~ (Name, Einwohner, Land)
{t | Städte(t) ∧ t.Land = Bayern ∧ t.SEinw ≥ 500.000}
Hier würden, die entsprechenden Relationen vorausgesetzt, die Tupel ausgewählt, die Großstädte in Bayern beinhalten. Das Tupelkalkül ähnelt nicht zufällig den aus der Mathematik bekannten Formulierungen zu Eigenschaften von Mengen. Die einzig freie Variable ist t, alle anderen Variablen müssen gebunden werden.
Das Bereichskalkül geht einen etwas anderen Weg und beschreibt den Bereich, in dem das Ergebnis liegt. Wieder ein anschauliches Beispiel:
{x3 | ∃ x2: (Städte(Passau, x2, x3) ) }
Hier wird abgefragt, in welchem Land Passau liegt. Die Anfrage besagt, dass x3 (also das Land) gesucht ist und irgendein x2 existiert, sodass bei einem x1 = Passau entsprechende Tupel in der Relation Städte existieren, die zurückgegeben werden sollen. Angefragte Elemente werden nicht beim Existenzquantor aufgeführt, Fremdschlüssel nur einmalig.
4 – Joins und weitere Operationen
Nach dem kurzen Ausflug in die Kalküle nun zu weiteren Operationen in SQL (Structured Query Language). Dazu zählen Joins, Gruppierungsoperationen, Aggregatfunktionen, Sortierung und Quantoren.
Ein Join ist die Vermengung von zwei Relationen. Es gibt verschiedene Arten von Joins:
- Inner Join: entspricht dem kartesischen Produkt eingeschränkt auf die Join-Bedingung, wobei die Tupeln ohne Joinpartner verloren gehen.. In SQL: SELECT * FROM a INNER JOIN b ON a.id = b.id
- Outer Join: Tupeln ohne Joinpartner bleiben erhalten und werden mit NULL-Werten aufgefüllt.
- Left bzw. Right Outer Join: linke bzw. rechte Relation bleibt verlustfrei
- Natural Join: Join über identische Attributnamen (im obigen Beispiel “id”)
- Cross Join: Kartesisches Produkt.
Gruppierungsoperationen werden mit GROUP BY eingeleitet, wobei alle Attribute der SELECT-Operation enthalten sein müssen. Einschränken kann man die Gruppierung mit HAVING, wobei nur Attribute aus der Gruppierung oder Aggregatfunktionen verwendet werden dürfen.
Sortieren ist möglich mit ORDER BY, bei Strings alphabetisch, bei Zahlen nach deren natürlicher Reihenfolge.
Aggregatfunktionen sind z.B. count(a), um die Vorkommen von a zu zählen, max(a), um das Maximum aller a zu bestimmen oder sum(a), um alle a zu summieren. Man kann sie entweder in der SELECT- oder in der HAVING-Anweisung verwenden, aber nicht in WHERE.
Quantoren sind ALL, SOME und ANY. Bei ALL müssen alle Elemente die spezifizierte Bedingung erfüllen, bei SOME einige und bei ANY irgendein Element. Quantoren finde ich nicht so intuitiv wie eine herkömmliche Formulierung, evtl. mittels Subqueries. Mann kann gottseidank auf sie verzichten.
Tabellen erstmal erstellen kann man mit CREATE TABLE name ( Attributdefinitionen ), siehe zusammenfassend hier. Eine Attributdefinition hat die Form name typ integritätsbedingung. Löschen kann man sie wieder mit DROP TABLE name. Neue Datensätze kann man einfügen mit dem Befehl INSERT INTO name (spalte1, spalte2, …) VALUES (wert1, wert2, …). Die Tabelle ändern kann mittels ALTER TABLE name ADD attr varchar(30), um z.B. ein neues Attribut hinzuzufügen, ALTER TABLE name DROP attr, um es zu löschen, oder ALTER TABLE name MODIFY attr float(3), um das Attribut zu ändern. Möglich ist eine Änderung auch mittels UPDATE name SET spalte1 = wert1 WHERE bedingung.
5 – Views
Views dienen als abstrahierte Benutzersichten auf die Datenbank. Im Grunde sind Views nichts anderes als Relationen, die eine bestimmte Ansicht auf in der Datenbank vorhandene Relationen geben. Für den Benutzer sehen sie aus wie eine Relation, sind aber nicht real, sondern aus anderen Relationen berechnet. Der Befehl dazu ist (mit angefügtem Beispiel):
CREATE VIEW name AS SELECT s.ort, s.einwohner, l.name, l.einwohner FROM Städte s NATURAL JOIN Länder l WHERE l.einwohner > 5.000.000
Löschen kann man sie mit DROP VIEW name. Auf Views sind alle Operationen erlaubt außer Sortieren. Probleme gibt es bei DISTINCT und arithmetischen Anweisungen. Dann sind Insert, Delete und Update nicht möglich. Join-Views sind außerdem nicht aktualisierbar. Reale Views nennt man materialisierte Views.
Rechte kann man mit GRANT rechteliste/all ON relation/view TO benutzerliste [WITH GRANT OPTION] erteilen.
6 – E/R-Modell
Das E/R-Modell stellt eine Möglichkeit dar, ein reales Problem als Beziehung von Entitäten zu formalisieren. Die Entitäten besitzen im Normalfall noch Attribute. Das E/R-Modell hilft bei der Umsetzung in ein relationales Datenbankschema, das man ablesen kann, indem man die Entitäten in Relationen umwandelt, die Attribute in deren Domains und die Beziehungen als Fremdschlüssel oder als Schlüsselpaar-Relationen modelliert.
Beispiel für das E/R-Modell
Als Relationen für das obere Beispiel würden sich hier anbieten:
Angestellter(Name, Kürzel)
Projekt(Kürzel, AngKürzel)
Unterstrichen bedeutet hierbei, dass das Attribut der Schlüssel der Relation ist, Kursivdruck, dass es sich um einen Fremdschlüssel handelt.
7 – Normalisierung
Um in allen Fällen aus dem E/R-Diagramm die entsprechenden Relationen lesen zu können und dabei kein problematisches Datenbankschema zu bekommen, nutzt man die Normalisierung.
Die möglichen Probleme sind v.a. Anomalien, die durch Redundanzen entstehen. Es gibt Insert-, Update- und Delete-Anomalien. Eine Insert-Anomalie tritt dann auf, wenn für das Einfügen eines Datensatzes ein Attribut nötig ist, das in der Realität dafür belanglos ist. Wenn in obiger Relation bspw. noch Projekt als Attribut in der Relation Angestellter wäre, könnte man keinen Angestellten hinzufügen, der noch keinem Projekt zugewiesen ist. Eine Update-Anomalie würde dann bspw. auftreten, wenn der Mitarbeiter mehreren Projekten zugewiesen ist, also mehrere Tupel belegt und man zur Änderung seines Kürzels nur auf eine Tupel zugreift (anstatt sie in allen zu ändern). Eine Delete-Anomalie wäre, wenn durch das Löschen aller Projekte einer Person der Datensatz zur Person verschwindet.
Redundanzen sind eng verknüpft mit dem Begriff der Funktionalen Abhängigkeit. Sie ist wie folgt definiert:
Gegeben ein Relationenschema R und A, B als Mengen von Attributen bzw. Teilmengen von R.
B ist genau dann von A funktional abhängig (A->B), wenn für alle möglichen Ausprägungen von R gilt:
1. Für alle r, s ε R mit r.A = s.A gilt r.B = s.B
2. Zu jedem Wert in A existiert genau ein Wert in B.
Bei der Funktionalen Abhängigkeit (FA) handelt es sich um eine Verallgemeinerung des Schlüsselkonzepts. Der Unterschied zum Schlüssel besteht darin, dass ein Attribut auch von Nicht-Schlüssel-Attributen oder nur einem Teil des Schlüssels abhängig sein kann. Eine volle FA ist A->C, eine partielle FA A,B->C, wenn C in beiden Fällen nur von A funktional abhängig ist.
Es gibt auch mehrwertige Abhängigkeiten, die wie folgt definiert sind:
Gegeben a,b,c ε R mit R = a u b u c (Vereinigung)
b ist dann mehrwertig abhängig von a (a->->b), wenn für jede gültige Ausprägung von R gilt: Für jedes Paar aus Tupeln t1, t2 mit t1.a = t2.a, aber t1.bc =/= t2.bc existieren zwei weitere Tupel t3 und t4 mit den Eigenschaften:
t1.a = t2.a = t3.a = t4.a
t3.b = t1.b
t4.b = t2.b
t3.c = t2.c
t4.c = t1.c
Jede FA ist auch eine mehrwertige Abhängigkeit (MVD).
Die triviale Abhängigkeit ist die Abhängigkeit eines Elements von sich selbst. Jedes Element ist von sich selbst abhängig.
Neben Schlüsseln gibt es auch Schlüsselkandidaten. Das sind Attribute, die als Schlüssel in Frage kommen. Als Beispiel: In der Relation Auto(HerstellerName, HerstellerNr, Modell) wären die Schlüsselkandidaten (HerstellerName, Modell) und (HerstellerNr, Modell).
Nachdem wir nun wissen, was Funktionale Abhängigkeiten und Schlüsselkandidaten sind, können wir uns an die Normalisierung wagen. Es gibt verschiedene Normalformen, die folgende Voraussetzungen haben:
- 1. Normalform: Alle Attributwerte müssen atomar sein. In relationalen Datenbanken ist das eh nicht möglich (nur temporär bei GROUP).
- 2. Normalform: Jedes Attribut ist voll funktional abhängig von allen Schlüsselkandidaten (SK) oder prim, d.h. Teil eines SK. Sie besagt, dass es keine partiellen Abhängigkeiten geben soll.
- 3. Normalform: Für jede nichttriviale Abhängigkeit X->A gilt: X enthält einen SK oder A ist prim. Sie besagt, dass es keine transitiven Abhängigkeiten geben soll (z.B. A->C bei A->B->C). Außerdem besagt sie, dass es keine FA von nicht-primen Attributen geben soll.
- Boyce-Codd-Normalform (BCNF): Für jede nichttriviale Abhängigkeit X->A gilt: X enthält einen SK. Der Grund dafür, dass die Prim-Möglichkeit der 3.NF wegfällt, ist, dass auch dort Anomalien auftreten können. In der obigen Relation Auto sind alle Attribute prim, aber HerstellerName->HerstellerNr. Solche Abhängigkeiten werden mit der BCNF eliminiert. Man könnte auch formulieren, dass es keine FA zwischen SK-Attributen geben darf.
- 4. Normalform: Für jede MVD a->->b gilt: Die MVD ist trivial oder a ist ein Superschlüssel.
Die Normalisierung führt zu einem ausgegorenen Datenbankschema. Um die einzelnen Anforderungen zu erreichen, werden Abhängigkeiten in eigene Relationen ausgelagert. Bis zur 3. Normalform kann das ursprüngliche Schema verlustfrei und abhängigkeitserhaltend zerlegt werden.Normalisierung ist allerdings teilweise schädlich für die Performanz. Deswegen sollte man in der Realität nicht jede FA berücksichtigen, sondern lieber Integritätsbedingungen formulieren, um Anomalien zu vermeiden.
Zur Normalisierung bis in die 3. NF gibt es eine eingängliche Vorgehensweise, den Synthesealgorithmus. Er funktioniert wie folgt:
1. Bestimmung der kanonischen Überdeckung
1.1. Linksreduktion: Eliminierung partieller FA
1.2. Rechtsreduktion: Eliminierung transitiver FA (dabei direkte FA auf leere Menge kürzen)
1.3. Entfernen rechtsleerer FA, die evtl. in 1.2. entstanden sind
1.4. Zusammenfassen von FA mit gleichen linken Seiten.
2. Relationenschema bilden
3. Rekonstruktion eines SK: Wenn SK nicht vollständig in einer Rel., dann neue Rel. bestehend aus SK
4. Elimination überflüssiger Relationen (in anderen Rel. enthaltene Rel.)
Als Beispiel hierzu:
Sei die Relation R(A,B,C,D,E) mit dem SK (A,B) gegeben mit den FA
a) A -> C, D // = A->C und A->D
b) A, B -> C
c) B, C -> E
d) A, B -> E
Die Schritte sind dann…
1.1. b) wird zu A->C, B fällt weg, da a) schon besagt, dass A->C
1.2. d) wird zu A,B -> Ø, da es mit c) eine transitive Abh. gibt: A, B -> B,C -> E
1.3. d) wird entfernt
1.4. b) wird mit a) zusammengelegt, übrig geblieben sind a) A->C,D und c) B,C->E
2. Relationenschema: R1(A,C,D) und R2(B,C,E)
3. Rekonstruktion eines SK (hier A,B) nicht möglich, deswegen neue Relation R3(A,B)
4. Eliminierung überflüssiger Relationen hier nicht möglich, da keine überflüssig ist.
8 – Transaktionen
Eine Transaktion ist eine Folge von Befehlen, die die Datenbank von einem konsistenten Zustand in einen anderen konsistenten Zustand überführt, also ein Einheit integritätserhaltender Zuständsänderungen einer Datenbank.
Wichtig sind dabei als Aufgaben des Datenbankmanagementsystems (DBMS) Synchronisation bei Mehrfachzugriffen, Integritätserhaltung (Typintegrität, Schlüsselintegrität, Referentielle Integrität bzw. Fremdschlüsselintegrität) und Schutz gegen Datenverlust. Griffig formuliert wurden die Anforderungen an ein DBS bezüglich Transaktionen im sogenannten ACID-Prinzip:
A = Atomicity = Atomarität: Eine Transaktion wird entweder ganz oder gar nicht in die Datenbank übernommen.
C = Consistency = Konsistenz: Eine Transaktion führt von einem konsistenten Zustand zu einem konsistenten Zustand.
I = Isolation: Ein Nutzer nimmt einen anderen bei gleichzeitigen Zugriffen nicht wahr.
D = Durability = Persistenz: Eine abgeschlossene Transaktion bleibt dauerhaft in der Datenbank erhalten.
Befehle für Transaktionen sind BOT (Begin Of Transaction), der bei SQL implizit ist, EOT (End Of Transaction), bei SQL heißt es COMMIT, abort (SQL: ROLLBACK [TO name]) und save (SAVEPOINT name).
Bei Transaktionen können Fehler und Anomalien auftreten. Fehler sind entweder Transaktionsfehler (z.B. Fehler im Anwendungsprogramm, Deadlock), Systemfehler (z.B. Stromausfall, OS-Absturz) oder Medienfehler (z.B. Festplattencrash). Sie können durch Lokales Undo (Rücksetzung auf BOT), Globales Undo (Rücksetzung aller nicht-abgeschlossenen Transaktionen in der DB) und Globales Redo (Nachführen aller bereits abgeschlossenen Transaktionen, die noch nicht in der DB sind) überwunden werden. Dazu braucht es Zusatzinformationen aus einer Logdatei, die alle Aktionen protokolliert.
Anomalien können bei unkontrolliertem Mehrbenutzerbetrieb auftreten. Das können verlorengegangene Änderungen (Lost Updates, verursacht durch Überschreiben) oder der Zugriff auf schmutzige Daten (Dirty Read/Write, Lesen und Schreiben auf geänderten Werten, die später zurückgesetzt werden) oder nicht reproduzierbares Lesen sein (Non-Repeatable Read, nochmaliges Auslesen liefert anderen Wert durch Änderungen einer anderen Transaktion). Eine Ausprägung des Non-Repeatable Read ist das “Phantomproblem”, das beim Auslesen Aggregatfunktionen nutzt, deren Ergebnis nicht mit der Anfrage in Einklang zu bringen ist, da zwischendrin eine DB-Änderung durch eine andere Transaktion stattfand.
Diese Synchronisationsprobleme treten nur bei Schreibvorgängen auf. Um ihnen zu entgehen, nutzt man entweder Locking als pessimistische Ablaufsteuerung oder ein Zeitstempelverfahren als optimistische Ablaufsteuerung. Locking ist das Standardverfahren und sperrt bei kritischen Transaktionen die betroffenen Ressourcen. Beim Zeitstempelverfahren werden Transaktionen im Konfliktfall zurückgesetzt. Es ist deswegen nur geeignet, wenn Konflikte zwischen Schreibern selten auftreten. Es ist auch möglich, zur Performanzverbesserung mit SET TRANSACTION READ-ONLY festzulegen, dass eine Transaktion nur lesend zugreift.

Neueste Kommentare