Ist es jemals in Ordnung, Listen in einer relationalen Datenbank zu verwenden?

94

Ich habe versucht, eine Datenbank zu entwerfen, die zu einem Projektkonzept passt, und bin auf ein Problem gestoßen, das anscheinend heiß diskutiert wurde. Ich habe ein paar Artikel gelesen und einige Stack Overflow-Antworten, die besagen, dass es nie (oder fast nie) in Ordnung ist, eine Liste von IDs oder Ähnlichem in einem Feld zu speichern - alle Daten sollten relational sein usw.

Das Problem, auf das ich stoße, ist, dass ich versuche, einen Aufgabenzuweiser zu erstellen. Die Benutzer erstellen Aufgaben, weisen sie mehreren Benutzern zu und speichern sie in der Datenbank.

Wenn ich diese Aufgaben einzeln in "Person" speichere, muss ich natürlich Dutzende von Dummy-Spalten "TaskID" haben und sie mikroverwalten, da beispielsweise einer Person 0 bis 100 Aufgaben zugewiesen werden können.

Andererseits, wenn ich die Aufgaben in einer "Aufgaben" -Tabelle speichere, muss ich Dutzende von Dummy "PersonID" -Spalten haben und sie mikro-verwalten - das gleiche Problem wie zuvor.

Ist es für ein Problem wie dieses in Ordnung, eine Liste von IDs in der einen oder anderen Form zu speichern, oder denke ich nur nicht an einen anderen Weg, wie dies erreicht werden kann, ohne die Prinzipien zu brechen?

linus72982
quelle
22
Ich weiß , dies ist mit „relational database“ so werde ich lass es einfach als Kommentar keine Antwort, sondern auch in anderen Arten von Datenbanken es tut Sinn machen , Listen zu speichern. Cassandra kommt in den Sinn, da es keine Verknüpfungen hat.
Captain Man
12
Gute Arbeit beim Recherchieren und dann hier fragen! In der Tat hat die 'Empfehlung', niemals die 1. Normalform zu verletzen, wirklich gut für Sie getan, denn Sie sollten sich wirklich einen anderen relationalen Ansatz einfallen lassen, nämlich eine "Viele-zu-Viele" -Relation, für die es ein Standardmuster gibt relationale Datenbanken, die verwendet werden sollten.
JimmyB
6
"Ist es jemals in Ordnung" ja ... was auch immer folgt, die Antwort lautet ja. Solange Sie einen gültigen Grund haben. Es gibt immer einen Anwendungsfall, der Sie dazu zwingt, Best Practices zu verletzen, da dies sinnvoll ist. (In Ihrem Fall sollten Sie dies jedoch definitiv nicht
tun
3
Ich verwende derzeit ein Array ( keine durch Trennzeichen getrennte Zeichenfolge - a VARCHAR ARRAY), um eine Liste von Tags zu speichern. Das ist wahrscheinlich nicht der Grund, warum sie später gespeichert werden, aber Listen können während der Prototyping-Phase äußerst nützlich sein, wenn Sie auf nichts anderes verweisen müssen und nicht das gesamte Datenbankschema aufbauen möchten, bevor Sie können mach irgendetwas anderes.
Nic Hartley
3
@ Ben „ (obwohl sie nicht Wende sein) “ - in Postgres, mehrere Abfragen für JSON Spalten (und wahrscheinlich auch XML, obwohl ich nicht überprüft haben) sind Wende.
Nic Hartley

Antworten:

249

Das Schlüsselwort und Schlüsselkonzept Sie untersuchen müssen , ist Datenbank Normalisierung .

Anstatt Informationen zu den Zuweisungen zu den Personen- oder Aufgabentabellen hinzuzufügen, fügen Sie eine neue Tabelle mit diesen Zuweisungsinformationen und relevanten Beziehungen hinzu.

Zum Beispiel haben Sie folgende Tabellen:

Personen:

+ −−−− + −−−−−−−−−−− +
| ID | Name |
+ ==== + =========== +
| 1 | Alfred |
| 2 | Jebediah |
| 3 | Jacob |
| 4 | Hesekiel |
+ −−−− + −−−−−−−−−−− +

Aufgaben:

+ - - - + - - - - - - - - - - - - - - - - - - - +
| ID | Name |
+ ==== + =================== +
| 1 | Füttere die Hühner
| 2 | Pflug |
| 3 | Milchkühe |
| 4 | Eine Scheune aufrichten
+ - - - + - - - - - - - - - - - - - - - - - - - +

Sie würden dann eine dritte Tabelle mit Zuweisungen erstellen. Diese Tabelle würde die Beziehung zwischen den Personen und den Aufgaben modellieren:

+ - - - + - - - - - - - - - - + - - - - - - - - +
| ID | PersonId | TaskId |
+ ==== + ========== + ========= +
| 1 | 1 | 3 |
| 2 | 3 | 2 |
| 3 | 2 | 1 |
| 4 | 1 | 4 |
+ - - - + - - - - - - - - - - + - - - - - - - - +

Wir hätten dann eine Fremdschlüssel-Einschränkung, sodass die Datenbank erzwingt, dass die Personen-ID und die Aufgaben-ID gültige IDs für diese Fremdelemente sein müssen. Für die erste Reihe, können wir sehen PersonId is 1, so Alfred , zugeordnet ist TaskId 3, Kühe melken .

Was Sie hier sehen sollten, ist, dass Sie so wenige oder so viele Aufgaben pro Aufgabe oder pro Person haben können, wie Sie möchten. In diesem Beispiel ist Hesekiel keine Aufgabe zugewiesen, und Alfred ist 2 zugewiesen. Wenn Sie eine Aufgabe mit 100 Personen ausführen, SELECT PersonId from Assignments WHERE TaskId=<whatever>;werden 100 Zeilen mit einer Vielzahl von verschiedenen Personen zugewiesen. In WHEREder PersonId können Sie alle dieser Person zugewiesenen Aufgaben suchen.

Wenn Sie Abfragen zurückgeben möchten, indem Sie die IDs durch die Namen und die Tasks ersetzen, lernen Sie, wie Sie Tabellen verbinden.

Whatsisname
quelle
86
Das Schlüsselwort, nach dem Sie suchen möchten, um mehr zu erfahren, ist " Viele-zu-Viele-Beziehung "
BlueRaja - Danny Pflughoeft
34
Um ein wenig auf Thierrys Kommentar einzugehen: Sie denken vielleicht, dass Sie nicht normalisieren müssen, weil ich nur X benötige und es sehr einfach ist, die ID-Liste zu speichern , aber für jedes System, das später erweitert werden könnte, werden Sie bedauern, dass Sie es nicht normalisiert haben vorhin. Normalisieren Sie immer ; Die Frage ist nur, in welcher normalen Form
Jan Doggen
8
Einverstanden mit @Jan - Ich habe meinem Team vor einiger Zeit erlaubt, eine Designverknüpfung zu verwenden und stattdessen JSON für etwas zu speichern, das "nicht erweitert werden muss". Das dauerte ungefähr sechs Monate FML. Unser Upgrade-Programm hatte dann einen üblen Kampf, um die JSON auf das Schema zu migrieren, mit dem wir hätten beginnen sollen. Ich hätte es wirklich besser wissen sollen.
Leichtigkeitsrennen im Orbit
13
@ Deduplicator: Dies ist nur eine Darstellung einer gartensortierten, automatisch inkrementierenden ganzzahligen Primärschlüsselspalte. Ziemlich typisches Zeug.
Whatsisname
8
@whatsisname In der Tabelle Personen oder Aufgaben stimme ich Ihnen zu. In einer Bridge-Tabelle, in der der einzige Zweck darin besteht, die Viele-zu-Viele-Beziehung zwischen zwei anderen Tabellen darzustellen, die bereits Ersatzschlüssel haben? Ich würde ohne guten Grund keine hinzufügen. Es ist nur ein Aufwand, da es niemals in Abfragen oder Beziehungen verwendet wird.
jpmc26
35

Sie stellen hier zwei Fragen.

Zuerst fragen Sie, ob es in Ordnung ist, Listen zu speichern, die in einer Spalte serialisiert sind. Ja es ist gut. Wenn Ihr Projekt es erfordert. Ein Beispiel könnten Produktbestandteile für eine Katalogseite sein, bei der Sie nicht versuchen möchten, jeden Bestandteil einzeln zu verfolgen.

Leider beschreibt Ihre zweite Frage ein Szenario, in dem Sie sich für einen relationaleren Ansatz entscheiden sollten. Du brauchst 3 Tische. Eine für die Personen, eine für die Aufgaben und eine, die die Liste der Aufgaben verwaltet, die welchen Personen zugewiesen sind. Das letzte wäre vertikal, eine Zeile pro Person / Aufgabenkombination, mit Spalten für Ihren Primärschlüssel, Ihre Aufgaben-ID und Ihre Personen-ID.

GroßmeisterB
quelle
9
Das Inhaltsstoffbeispiel, auf das Sie verweisen, ist auf der Oberfläche korrekt. aber es wäre in diesem Fall Klartext. Es ist keine Liste im Sinne der Programmierung (es sei denn, Sie meinen, die Zeichenfolge ist eine Liste von Zeichen, die Sie offensichtlich nicht haben). OP, das ihre Daten als "eine Liste von IDs" (oder sogar nur "eine Liste von [..]") beschreibt, impliziert, dass sie diese Daten irgendwann als einzelne Objekte behandeln.
Flater
10
@Flater: Aber es ist eine Liste. Sie müssen in der Lage sein, eine HTML-Liste, eine Markdown-Liste, eine JSON-Liste usw. neu zu formatieren, um sicherzustellen, dass die Elemente ordnungsgemäß auf einer Webseite, einem Nur-Text-Dokument oder einem Mobiltelefon angezeigt werden App ... und das kann man mit Klartext nicht wirklich machen.
Kevin
12
@ Kevin Wenn das Ihr Ziel ist, dann ist es viel einfacher und einfacher, die Zutaten in einer Tabelle zu speichern! Ganz zu schweigen davon, ob die Leute später ... oh, ich weiß nicht, ob sie sich empfohlene Substitute wünschen , oder ob sie nach Rezepten ohne Erdnüsse, Gluten oder tierische Proteine suchen ...
Dan Bron
10
@ DanBron: YAGNI. Momentan verwenden wir nur eine Liste, da dies die Benutzeroberflächenlogik vereinfacht. Wenn wir brauchen oder werden Liste ähnliches Verhalten in der Business - Logik - Schicht benötigen, dann sollte es in einer separaten Tabelle normalisiert werden. Tabellen und Joins sind nicht unbedingt teuer, aber nicht kostenlos, und sie werfen Fragen zur Elementreihenfolge ("Kümmern wir uns um die Reihenfolge der Zutaten?") Und zur weiteren Normalisierung ("Werdet ihr 3 Eier drehen") auf. in ('Eier', 3)? Was ist mit 'Salz, nach Geschmack', ist das ('Salz', NULL)? ").
Kevin
7
@ Kevin: YAGNI ist hier ganz falsch. Sie selbst haben die Notwendigkeit argumentiert, die Liste auf viele Arten transformieren zu können (HTML, Markdown, JSON) und argumentieren damit, dass Sie die einzelnen Elemente der Liste benötigen . Sofern es sich bei den Anwendungen für Datenspeicherung und "Listenverarbeitung" nicht um zwei Anwendungen handelt, die unabhängig voneinander entwickelt wurden (und beachten Sie, dass separate Anwendungsebenen! = Separate Anwendungen), sollte die Datenbankstruktur immer so erstellt werden, dass die Daten in einem Format gespeichert werden, das sie leicht verfügbar lässt - unter Vermeidung zusätzlicher Analyse- / Konvertierungslogik.
Flater
22

Was Sie beschreiben, ist in Ihrem Fall als "viele zu viele" Beziehung zwischen Personund bekannt Task. Es wird normalerweise mithilfe einer dritten Tabelle implementiert, die manchmal als "Link" - oder "Querverweistabelle" bezeichnet wird. Zum Beispiel:

create table person (
    person_id integer primary key,
    ...
);

create table task (
    task_id integer primary key,
    ...
);

create table person_task_xref (
    person_id integer not null,
    task_id integer not null,
    primary key (person_id, task_id),
    foreign key (person_id) references person (person_id),
    foreign key (task_id) references task (task_id)
);
Mike Partridge
quelle
2
Möglicherweise möchten Sie auch task_idzuerst einen Index hinzufügen , wenn Sie Abfragen ausführen, die nach Aufgaben gefiltert sind.
jpmc26
1
Auch als Brückentisch bekannt. Ich wünschte auch, ich könnte Ihnen ein zusätzliches Plus für das Fehlen einer Identitätsspalte geben, obwohl ich einen Index für jede Spalte empfehlen würde.
Jmoreno
13

... es ist niemals (oder fast nie) in Ordnung, eine Liste von IDs oder dergleichen in einem Feld zu speichern

Das einzig Mal , Sie könnten mehr als ein Datenelement in einem einzigen Feld speichern, wenn das Feld ist nur immer als eine Einheit verwendet und wird nie als aus diesen kleineren Elementen berücksichtigt. Ein Beispiel könnte ein Bild sein, das in einem BLOB-Feld gespeichert ist. Es besteht aus vielen, vielen kleineren Elementen (Bytes), die jedoch für die Datenbank nichts bedeuten und nur zusammen verwendet werden können (und für einen Endbenutzer hübsch aussehen).

Da eine "Liste" per Definition aus kleineren Elementen (Elementen) besteht, ist dies hier nicht der Fall und Sie sollten die Daten normalisieren.

... wenn ich diese Aufgaben einzeln in "Person" speichere, werden Dutzende von Dummy-Spalten "TaskID" benötigt ...

Nein. In einer Kreuzungstabelle (auch bekannt als schwache Entität) befinden sich einige Zeilen zwischen Person und Aufgabe. Datenbanken können sehr gut mit vielen Zeilen arbeiten. Sie sind eigentlich ziemlich albern, wenn sie mit vielen [wiederholten] Spalten arbeiten.

Nettes klares Beispiel von Whatsisname.

Phill W.
quelle
4
Bei der Erstellung von realen Systemen ist "Sag niemals nie" eine sehr gute Regel, um danach zu leben.
10.
1
In vielen Fällen können die Kosten pro Element für das Verwalten oder Abrufen einer Liste in normalisierter Form die Kosten für das Verwalten der Elemente als Klecks erheblich übersteigen, da jedes Element der Liste die Identität des Masterelements enthalten müsste, mit dem es verknüpft ist ist zugeordnet und sein Ort in der Liste zusätzlich zu den eigentlichen Daten. Selbst in Fällen, in denen Code von der Möglichkeit profitieren kann, einige Listenelemente zu aktualisieren, ohne die gesamte Liste zu aktualisieren, ist es möglicherweise günstiger, alles als Blob zu speichern und alles neu zu schreiben, wann immer etwas neu geschrieben werden muss.
Supercat
4

In bestimmten vorberechneten Bereichen kann dies legitim sein.

Wenn einige Ihrer Abfragen teuer sind und Sie vorberechnete Felder verwenden, die mithilfe von Datenbank-Triggern automatisch aktualisiert werden, ist es möglicherweise legitim, die Listen in einer Spalte zu belassen.

In der Benutzeroberfläche möchten Sie diese Liste beispielsweise in einer Rasteransicht anzeigen, in der jede Zeile nach einem Doppelklick vollständige Details (mit vollständigen Listen) öffnen kann:

REGISTERED USER LIST
+------------------+----------------------------------------------------+
|Name              |Top 3 most visited tags                             |
+==================+====================================================+
|Peter             |Design, Fitness, Gifts                              |
+------------------+----------------------------------------------------+
|Lucy              |Fashion, Gifts, Lifestyle                           |
+------------------+----------------------------------------------------+

Sie aktualisieren die zweite Spalte nach Auslöser, wenn der Kunde einen neuen Artikel oder eine geplante Aufgabe besucht.

Sie können ein solches Feld auch für die Suche zur Verfügung stellen (als normaler Text).

In solchen Fällen ist das Führen von Listen legitim. Sie müssen nur den Fall berücksichtigen, dass möglicherweise die maximale Feldlänge überschritten wird.


Wenn Sie Microsoft Access verwenden, sind die angebotenen mehrwertigen Felder ein weiterer spezieller Anwendungsfall. Sie bearbeiten Ihre Listen in einem Feld automatisch.

Sie können jedoch jederzeit auf die in anderen Antworten angegebene normalisierte Standardform zurückgreifen.


Zusammenfassung: Normale Datenbankformen sind theoretische Modelle, die zum Verständnis wichtiger Aspekte der Datenmodellierung erforderlich sind . Bei der Normalisierung werden jedoch natürlich weder die Leistung noch andere Kosten für das Abrufen der Daten berücksichtigt. Es liegt außerhalb des Geltungsbereichs dieses theoretischen Modells. Das Speichern von Listen oder anderen vorberechneten (und kontrollierten) Duplikaten ist jedoch häufig durch die praktische Implementierung erforderlich.

Würden wir im Lichte der obigen Ausführungen in der praktischen Umsetzung eine Abfrage bevorzugen, die sich auf die perfekte Normalform stützt und 20 Sekunden lang ausgeführt wird, oder eine äquivalente Abfrage, die sich auf vorberechnete Werte stützt, die 0,08 s dauern? Niemand mag es, wenn sein Softwareprodukt der Langsamkeit beschuldigt wird.

Miroxlav
quelle
1
Es kann sogar ohne vorberechnetes Zeug legitim sein. Ich habe es ein paar Mal gemacht, wo die Daten richtig gespeichert wurden, aber aus Leistungsgründen ist es nützlich, ein paar zwischengespeicherte Ergebnisse in den Hauptdatensätzen abzulegen.
Loren Pechtel
@LorenPechtel - Ja, danke. In meinem vorberechneten Ausdruck sind auch Fälle von zwischengespeicherten Werten enthalten, die bei Bedarf gespeichert werden. In Systemen mit komplexen Abhängigkeiten sorgen sie für eine normale Leistung. Und wenn diese Werte mit entsprechendem Know-how programmiert werden, sind sie zuverlässig und immer synchron. Ich wollte der Antwort nur keinen Fall von Caching hinzufügen, um die Antwort einfach und sicher zu halten. Es wurde trotzdem herabgestimmt. :)
Miroxlav
@LorenPechtel Eigentlich wäre das immer noch ein schlechter Grund ... Cache-Daten sollten in einem Zwischenspeicher zwischengespeichert werden, und während der Cache noch gültig ist, sollte diese Abfrage niemals die Hauptdatenbank treffen.
Tezra
1
@Tezra Nein, ich sage, dass manchmal ein Teil der Daten aus einer Sekundärtabelle oft genug benötigt wird, damit es Sinn macht, eine Kopie in den Hauptdatensatz aufzunehmen. (Beispiel , das ich getan habe - die Mitarbeitertabelle enthält das letzte Mal in und das letzte Mal Sie werden nur für die Anzeige verwendet wird , jede tatsächliche Berechnung kommt aus der Tabelle mit dem Takt-in / clock-outen Aufzeichnungen..)
Loren Pechtel
0

Zwei Tische gegeben; Wir nennen sie Person und Task, jede mit ihrer eigenen ID (PersonID, TaskID). Die Grundidee ist, eine dritte Tabelle zu erstellen, um sie miteinander zu verbinden. Wir nennen diese Tabelle PersonToTask. Zumindest sollte es eine eigene ID haben, ebenso wie die beiden anderen. Also, wenn es darum geht, jemanden einer Aufgabe zuzuweisen; Sie müssen die Person-Tabelle nicht mehr aktualisieren, sondern müssen lediglich eine neue Zeile in die PersonToTaskTable einfügen. Und die Wartung wird einfacher - das Löschen einer Aufgabe wird einfach zu einem LÖSCHEN basierend auf der Aufgaben-ID, wodurch die Personentabelle und das damit verbundene Parsen nicht mehr aktualisiert werden

CREATE TABLE dbo.PersonToTask (
    pttID INT IDENTITY(1,1) NOT NULL,
    PersonID INT NULL,
    TaskID   INT NULL
)

CREATE PROCEDURE dbo.Task_Assigned (@PersonID INT, @TaskID INT)
AS
BEGIN
    INSERT PersonToTask (PersonID, TaskID)
    VALUES (@PersonID, @TaskID)
END

CREATE PROCEDURE dbo.Task_Deleted (@TaskID INT)
AS
BEGIN
    DELETE PersonToTask  WHERE TaskID = @TaskID
    DELETE Task          WHERE TaskID = @TaskID
END

Wie wäre es mit einem einfachen Bericht oder wer ist einer Aufgabe zugeordnet?

CREATE PROCEDURE dbo.Task_CurrentAssigned (@TaskID INT)
AS
BEGIN
    SELECT PersonName
    FROM   dbo.Person
    WHERE  PersonID IN (SELECT PersonID FROM dbo.PersonToTask WHERE TaskID = @TaskID)
END

Sie könnten natürlich noch viel mehr tun. Ein TimeReport kann durchgeführt werden, wenn Sie DateTime-Felder für TaskAssigned und TaskCompleted hinzugefügt haben. Es hängt alles von dir ab

Mad Myche
quelle
0

Es kann funktionieren, wenn Sie sagen, dass Sie von Menschen lesbare Primärschlüssel haben und eine Liste von Task-Nrn. Möchten, ohne sich mit der vertikalen Natur einer Tabellenstruktur befassen zu müssen. dh viel einfacher, erste Tabelle zu lesen.

------------------------  
Employee Name | Task 
Jack          |  1,2,5
Jill          |  4,6,7
------------------------

------------------------  
Employee Name | Task 
Jack          |  1
Jack          |  2
Jack          |  5
Jill          |  4
Jill          |  6
Jill          |  7
------------------------

Die Frage wäre dann: Sollte die Aufgabenliste bei Bedarf gespeichert oder generiert werden, was weitgehend von Anforderungen abhängen würde, wie oft die Liste benötigt wird, wie genau wie viele Datenzeilen existieren, wie die Daten verwendet werden usw. .. Danach sollten die Kompromisse zur Benutzererfahrung und zur Erfüllung der Anforderungen analysiert werden.

Vergleichen Sie beispielsweise die Zeit, die zum Abrufen der 2 Zeilen benötigt wird, mit der Ausführung einer Abfrage, die die 2 Zeilen generiert. Wenn es lange dauert und der Benutzer nicht die aktuellste Liste benötigt (* erwartet weniger als 1 Änderung pro Tag), kann es gespeichert werden.

Oder wenn der Benutzer eine historische Aufzeichnung der ihm zugewiesenen Aufgaben benötigt, ist es auch sinnvoll, wenn die Liste gespeichert wurde. Es kommt also wirklich darauf an, was du tust, sag niemals nie.

Doppelte E-CPU
quelle
Wie Sie sagen, hängt alles davon ab, wie die Daten abgerufen werden sollen. Wenn Sie diese Tabelle / nur / je nach Benutzername abfragen, ist das Feld "Liste" vollkommen ausreichend. Wie können Sie jedoch eine solche Tabelle abfragen, um herauszufinden, wer an Aufgabe Nr. 1234567 arbeitet, und um die Leistung zu gewährleisten? Nahezu jede Art von "find-X-anywhere-in-the-field" -String-Funktion führt zu einer solchen Abfrage von / Table Scan / und verlangsamt das Crawling. Bei richtig normalisierten, richtig indizierten Daten passiert das einfach nicht.
Phill W.
0

Sie nehmen einen anderen Tisch, drehen ihn um 90 Grad und wandeln ihn in einen anderen Tisch um.

Es ist so, als hätten Sie eine Bestelltabelle, in der Sie itemProdcode1, itemQuantity1, itemPrice1 ... itemProdcode37, itemQuantity37, itemPrice37 haben. Abgesehen davon, dass Sie programmgesteuert umständlich sind, können Sie garantieren, dass morgen jemand 38 Dinge bestellen möchte.

Ich würde es nur so machen, wenn die 'Liste' nicht wirklich eine Liste ist, dh wenn sie als Ganzes steht und sich jede einzelne Werbebuchung nicht auf eine klare und unabhängige Entität bezieht. In diesem Fall füllen Sie einfach alles mit einem Datentyp, der groß genug ist.

Eine Bestellung ist also eine Liste, eine Stückliste ist eine Liste (oder eine Liste von Listen, die noch mehr zum Albtraum werden würde, wenn man sie "seitwärts" implementiert). Aber eine Notiz / ein Kommentar und ein Gedicht sind es nicht.

Kerl den Pub runter
quelle
0

Wenn es "nicht in Ordnung" ist, ist es ziemlich schlimm, dass jede Wordpress-Site jemals eine Liste in wp_usermeta mit wp_capabilities in einer Zeile, dismissed_wp_pointers in einer Zeile und andere hat ...

In solchen Fällen ist die Geschwindigkeit möglicherweise besser, da Sie die Liste fast immer benötigen . Es ist jedoch nicht bekannt, dass Wordpress das perfekte Beispiel für Best Practices ist.

NoBugs
quelle