Was Sie beschreiben, nennt man polymorphe Assoziationen. Das heißt, die Spalte „Fremdschlüssel“ enthält einen ID-Wert, der in einer Gruppe von Zieltabellen vorhanden sein muss. Typischerweise stehen die Zieltabellen in irgendeiner Weise in Beziehung, z. B. indem sie Instanzen einer gemeinsamen Oberklasse von Daten sind. Außerdem benötigen Sie eine weitere Spalte neben der Fremdschlüsselspalte, damit Sie in jeder Zeile angeben können, auf welche Zieltabelle verwiesen wird.
CREATE TABLE popular_places (
user_id INT NOT NULL,
place_id INT NOT NULL,
place_type VARCHAR(10) -- either 'states' or 'countries'
-- foreign key is not possible
);
Es gibt keine Möglichkeit, polymorphe Zuordnungen mithilfe von SQL-Einschränkungen zu modellieren. Eine Fremdschlüsseleinschränkung verweist immer auf Eins Zieltabelle.
Polymorphe Assoziationen werden von Frameworks wie Rails und Hibernate unterstützt. Sie sagen jedoch ausdrücklich, dass Sie SQL-Einschränkungen deaktivieren müssen, um diese Funktion zu verwenden. Stattdessen muss die Anwendung oder das Framework gleichwertige Arbeit leisten, um sicherzustellen, dass die Referenz erfüllt wird. Das heißt, der Wert im Fremdschlüssel ist in einer der möglichen Zieltabellen vorhanden.
Polymorphe Assoziationen sind schwach in Bezug auf die Durchsetzung von Datenbankkonsistenz. Die Datenintegrität hängt davon ab, dass alle Clients auf die Datenbank mit der gleichen durchgesetzten referenziellen Integritätslogik zugreifen, und auch die Durchsetzung muss fehlerfrei sein.
Hier sind einige alternative Lösungen, die die datenbankerzwungene referenzielle Integrität nutzen:
Erstelle einen zusätzlichen Tisch pro Ziel. Zum Beispiel popular_states
und popular_countries
, die auf states
verweisen und countries
bzw. Jede dieser "beliebten" Tabellen verweist auch auf das Profil des Benutzers.
CREATE TABLE popular_states (
state_id INT NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY(state_id, user_id),
FOREIGN KEY (state_id) REFERENCES states(state_id),
FOREIGN KEY (user_id) REFERENCES users(user_id),
);
CREATE TABLE popular_countries (
country_id INT NOT NULL,
user_id INT NOT NULL,
PRIMARY KEY(country_id, user_id),
FOREIGN KEY (country_id) REFERENCES countries(country_id),
FOREIGN KEY (user_id) REFERENCES users(user_id),
);
Das bedeutet, dass Sie beide Tabellen abfragen müssen, um alle beliebten Lieblingsorte eines Benutzers zu erhalten. Aber es bedeutet, dass Sie sich darauf verlassen können, dass die Datenbank Konsistenz erzwingt.
Erstellen Sie einen places
Tabelle als Supertabelle. Wie Abie erwähnt, besteht eine zweite Alternative darin, dass Ihre beliebten Orte auf eine Tabelle wie places
verweisen , das beiden states
übergeordnet ist und countries
. Das heißt, sowohl Staaten als auch Länder haben auch einen Fremdschlüssel zu places
(Sie können diesen Fremdschlüssel sogar zum Primärschlüssel von states
machen und countries
).
CREATE TABLE popular_areas (
user_id INT NOT NULL,
place_id INT NOT NULL,
PRIMARY KEY (user_id, place_id),
FOREIGN KEY (place_id) REFERENCES places(place_id)
);
CREATE TABLE states (
state_id INT NOT NULL PRIMARY KEY,
FOREIGN KEY (state_id) REFERENCES places(place_id)
);
CREATE TABLE countries (
country_id INT NOT NULL PRIMARY KEY,
FOREIGN KEY (country_id) REFERENCES places(place_id)
);
Verwenden Sie zwei Spalten. Verwenden Sie statt einer Spalte, die auf eine von zwei Zieltabellen verweisen kann, zwei Spalten. Diese beiden Spalten können NULL
sein; tatsächlich sollte nur einer von ihnen nicht NULL
sein .
CREATE TABLE popular_areas (
place_id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
state_id INT,
country_id INT,
CONSTRAINT UNIQUE (user_id, state_id, country_id), -- UNIQUE permits NULLs
CONSTRAINT CHECK (state_id IS NOT NULL OR country_id IS NOT NULL),
FOREIGN KEY (state_id) REFERENCES places(place_id),
FOREIGN KEY (country_id) REFERENCES places(place_id)
);
In Bezug auf die relationale Theorie verletzen polymorphe Assoziationen die Erste Normalform
, weil die popular_place_id
ist eigentlich eine Spalte mit zwei Bedeutungen:es ist entweder ein Staat oder ein Land. Sie würden das age
einer Person nicht speichern und ihre phone_number
in einer einzigen Spalte, und aus dem gleichen Grund sollten Sie nicht beide state_id
speichern und country_id
in einer einzigen Spalte. Die Tatsache, dass diese beiden Attribute kompatible Datentypen haben, ist zufällig; sie bedeuten immer noch verschiedene logische Einheiten.
Polymorphe Assoziationen verstoßen auch gegen die Dritte Normalform , da die Bedeutung der Spalte von der zusätzlichen Spalte abhängt, die die Tabelle benennt, auf die sich der Fremdschlüssel bezieht. In der dritten Normalform darf ein Attribut in einer Tabelle nur vom Primärschlüssel dieser Tabelle abhängen.
Re-Kommentar von @SavasVedova:
Ich bin mir nicht sicher, ob ich Ihrer Beschreibung folge, ohne die Tabellendefinitionen oder eine Beispielabfrage zu sehen, aber es hört sich so an, als hätten Sie einfach mehrere Filters
Tabellen, die jeweils einen Fremdschlüssel enthalten, der auf ein zentrales Products
verweist Tisch.
CREATE TABLE Products (
product_id INT PRIMARY KEY
);
CREATE TABLE FiltersType1 (
filter_id INT PRIMARY KEY,
product_id INT NOT NULL,
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
CREATE TABLE FiltersType2 (
filter_id INT PRIMARY KEY,
product_id INT NOT NULL,
FOREIGN KEY (product_id) REFERENCES Products(product_id)
);
...and other filter tables...
Das Verbinden der Produkte mit einem bestimmten Filtertyp ist einfach, wenn Sie wissen, mit welchem Typ Sie verbinden möchten:
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
Wenn der Filtertyp dynamisch sein soll, müssen Sie Anwendungscode schreiben, um die SQL-Abfrage zu erstellen. SQL erfordert, dass die Tabelle zum Zeitpunkt des Schreibens der Abfrage angegeben und festgelegt wird. Sie können die verbundene Tabelle nicht dynamisch basierend auf den Werten auswählen, die in einzelnen Zeilen von Products
gefunden werden .
Die einzige andere Möglichkeit ist, allen beizutreten Tabellen mithilfe von Outer Joins filtern. Diejenigen, die keine übereinstimmende product_id haben, werden nur als einzelne Zeile mit Nullen zurückgegeben. Aber Sie müssen trotzdem alle fest codieren die verknüpften Tabellen, und wenn Sie neue Filtertabellen hinzufügen, müssen Sie Ihren Code aktualisieren.
SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...
Eine andere Möglichkeit, allen Filtertabellen beizutreten, besteht darin, dies seriell zu tun:
SELECT * FROM Product
INNER JOIN FiltersType1 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
UNION ALL
SELECT * FROM Products
INNER JOIN FiltersType3 USING (product_id)
...
Bei diesem Format müssen Sie jedoch immer noch Verweise auf alle Tabellen schreiben. Daran führt kein Weg vorbei.