Möglich, einen MySQL-Fremdschlüssel für eine von zwei möglichen Tabellen zu erstellen?

180

Nun, hier ist mein Problem, ich habe drei Tabellen; Regionen, Länder, Staaten. Länder können sich innerhalb von Regionen befinden, Staaten können sich innerhalb von Regionen befinden. Regionen sind die Spitze der Nahrungskette.

Jetzt füge ich eine popular_areas-Tabelle mit zwei Spalten hinzu. region_id und popular_place_id. Ist es möglich , ein Fremdschlüssel entweder Länder zu machen popular_place_id sein OR Staaten. Ich werde wahrscheinlich eine Spalte popular_place_type hinzufügen müssen, um festzustellen, ob die ID ein Land oder einen Staat beschreibt.

Andrew G. Johnson
quelle

Antworten:

282

Was Sie beschreiben, nennt man polymorphe Assoziationen. Das heißt, die Spalte "Fremdschlüssel" enthält einen ID-Wert, der in einer Reihe von Zieltabellen vorhanden sein muss. In der Regel sind die Zieltabellen in irgendeiner Weise miteinander verbunden, z. B. als Instanzen einer allgemeinen Superklasse von Daten. Sie benötigen außerdem 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 Assoziationen mithilfe von SQL-Einschränkungen zu modellieren. Eine Fremdschlüsseleinschränkung verweist immer auf eine 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 verwenden zu können. Stattdessen muss die Anwendung oder das Framework gleichwertige Arbeit leisten, um sicherzustellen, dass die Referenz erfüllt ist. Das heißt, der Wert im Fremdschlüssel ist in einer der möglichen Zieltabellen vorhanden.

Polymorphe Assoziationen sind im Hinblick auf die Durchsetzung der Datenbankkonsistenz schwach. Die Datenintegrität hängt davon ab, dass alle Clients mit derselben erzwungenen referenziellen Integritätslogik auf die Datenbank zugreifen. Außerdem muss die Durchsetzung fehlerfrei sein.

Hier sind einige alternative Lösungen, die die datenbankgesteuerte referenzielle Integrität nutzen:

Erstellen Sie eine zusätzliche Tabelle pro Ziel. Beispielsweise popular_statesund popular_countries, der Bezugs statesund countriesjeweils. Jede dieser "beliebten" Tabellen verweist auch auf das Benutzerprofil.

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),
);

Dies bedeutet, dass Sie beide Tabellen abfragen müssen, um alle beliebten Lieblingsorte eines Benutzers abzurufen. Sie können sich jedoch auf die Datenbank verlassen, um die Konsistenz zu gewährleisten.

Erstellen Sie eine placesTabelle als Supertabelle. Wie Abie erwähnt, ist eine zweite Alternative , dass Ihre Lieblingsort eine Tabelle wie Referenz places, die ein Elternteil beide ist statesund countries. Das heißt, sowohl Staaten als auch Länder haben auch einen Fremdschlüssel für places(Sie können diesen Fremdschlüssel sogar als Primärschlüssel für statesund festlegen 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 anstelle einer Spalte, die auf eine der beiden Zieltabellen verweisen kann, zwei Spalten. Diese beiden Spalten können sein NULL; in der Tat sollte nur einer von ihnen nicht sein NULL.

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 , da popular_place_ides sich tatsächlich um eine Spalte mit zwei Bedeutungen handelt: Es ist entweder ein Staat oder ein Land. Sie würden die Person ageund ihre Person nicht phone_numberin einer einzigen Spalte speichern , und aus dem gleichen Grund sollten Sie nicht beide state_idund country_idin einer einzigen Spalte speichern . Die Tatsache, dass diese beiden Attribute kompatible Datentypen haben, ist zufällig. Sie bezeichnen immer noch verschiedene logische Entitäten.

Polymorphe Assoziationen verletzen auch 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.


Kommentar von @SavasVedova:

Ich bin nicht sicher, ob ich Ihrer Beschreibung folge, ohne die Tabellendefinitionen oder eine Beispielabfrage zu sehen, aber es scheint, als hätten Sie einfach mehrere FiltersTabellen, die jeweils einen Fremdschlüssel enthalten, der auf eine zentrale ProductsTabelle verweist .

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 Verknüpfen der Produkte mit einem bestimmten Filtertyp ist einfach, wenn Sie wissen, welchem ​​Typ Sie beitreten 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 verknüpfte Tabelle nicht dynamisch basierend auf den Werten in einzelnen Zeilen von auswählen Products.

Die einzige andere Möglichkeit besteht darin, mithilfe äußerer Verknüpfungen mit allen Filtertabellen zu verknüpfen. Diejenigen, die keine übereinstimmende product_id haben, werden nur als einzelne Zeile von Nullen zurückgegeben. Sie müssen jedoch immer noch alle verknüpften Tabellen fest codieren. 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, sich mit allen Filtertabellen zu verbinden, 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)
...

Für dieses Format müssen Sie jedoch weiterhin Verweise auf alle Tabellen schreiben. Daran führt kein Weg vorbei.

Bill Karwin
quelle
Welches würdest du Bill vorschlagen? Ich bin gerade dabei, eine Datenbank zu entwerfen, aber ich bin verloren. Grundsätzlich muss ich einem Produkt Filter zuordnen, und die Werte der Filter werden in verschiedenen Tabellen aufgeführt. Das Problem ist jedoch, dass Filter von Administratoren generiert werden. Je nach Filtertyp können die Daten variieren und daher joinändert sich auch das Ziel ...... Kompliziere ich zu viel oder was? Hilfe!
Savas Vedova
+1 Danke für eine tolle Lösung. Eine Frage, die ich bei der ersten / zweiten Lösung habe, lautet: Gibt es eine Normalisierungsverletzung mit der Tatsache, dass mehrere Tabellen auf denselben Primärschlüssel in dieser Metatabelle verweisen können? Ich weiß, dass Sie dies mit Logik lösen können, aber ich sehe keine Möglichkeit für die Datenbank, dies durchzusetzen, es sei denn, mir fehlt etwas.
Rob
5
Ich mag die Herangehensweise mit "CONSTRAINT CHECK" sehr. Es kann jedoch verbessert werden, wenn wir "ODER" in "XOR" ändern. Auf diese Weise stellen wir sicher, dass nur eine Spalte aus der Menge NICHT NULL ist
alex_b
1
@alex_b, ja, das ist gut, aber logisches XOR ist kein Standard-SQL und wird nicht von allen SQL-Marken unterstützt. MySQL hat es, PostgreSQL jedoch nicht. Oracle hat es, Microsoft jedoch erst 2016. Und so weiter.
Bill Karwin
1
"Diese beiden Spalten können NULL sein; tatsächlich sollte nur eine von ihnen nicht NULL sein" - dies würde 1NF verletzen!
Tag, wenn
10

Dies ist nicht die eleganteste Lösung der Welt, aber Sie können die konkrete Tabellenvererbung verwenden , damit dies funktioniert.

Konzeptionell schlagen Sie eine Vorstellung von einer Klasse von "Dingen vor, die beliebte Gebiete sein können", von denen Ihre drei Arten von Orten erben. Man könnte dies als eine Tabelle mit dem Namen repräsentieren, zum Beispiel, placesin der jede Zeile eine Beziehung mit einer Reihe Eins-zu-eins hat in regions, countriesoder states. (Attribute, die zwischen Regionen, Ländern oder Staaten geteilt werden, falls vorhanden, könnten in diese Orts-Tabelle verschoben werden.) Sie popular_place_idwären dann ein Fremdschlüsselverweis auf eine Zeile in der Orts-Tabelle, die Sie dann zu einer Region, einem Land führen würde oder Zustand.

Die Lösung, die Sie mit einer zweiten Spalte vorschlagen, um die Art der Assoziation zu beschreiben, ist, wie Rails mit polymorphen Assoziationen umgeht, aber ich bin im Allgemeinen kein Fan davon. Bill erklärt ausführlich, warum polymorphe Assoziationen nicht deine Freunde sind.

Abie
quelle
1
aka "das Supertyp-Subtyp-Muster"
ErikE
Auch dieser Artikel erklärt gut das Konzept duhallowgreygeek.com/polymorphic-association-bad-sql-smell
Marco Staffoli
5

Hier ist eine Korrektur von Bill Karwins "Supertable" -Ansatz unter Verwendung eines zusammengesetzten Schlüssels ( place_type, place_id ), um die wahrgenommenen Verstöße gegen die normale Form zu beheben:

CREATE TABLE places (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) NOT NULL
     CHECK ( place_type = 'state', 'country' ),
  UNIQUE ( place_type, place_id )
);

CREATE TABLE states (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'state' NOT NULL
     CHECK ( place_type = 'state' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to states go here
);

CREATE TABLE countries (
  place_id INT NOT NULL UNIQUE,
  place_type VARCHAR(10) DEFAULT 'country' NOT NULL
     CHECK ( place_type = 'country' ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
  -- attributes specific to country go here
);

CREATE TABLE popular_areas (
  user_id INT NOT NULL,
  place_id INT NOT NULL,
  UNIQUE ( user_id, place_id ),
  FOREIGN KEY ( place_type, place_id ) 
     REFERENCES places ( place_type, place_id )
);

Was dieses Design nicht sicherstellen kann, dass für jede Zeile placeseine Zeile in statesoder countries(aber nicht beide) vorhanden ist. Dies ist eine Einschränkung von Fremdschlüsseln in SQL. In einem vollständigen SQLMS-konformen DBMS könnten Sie aufschiebbare Einschränkungen zwischen Tabellen definieren, die es Ihnen ermöglichen würden, dasselbe zu erreichen, aber es ist klobig, beinhaltet Transaktionen und ein solches DBMS muss es noch auf den Markt bringen.

eines Tages, wenn
quelle
0

Mir ist klar, dass dieser Thread alt ist, aber ich habe das gesehen und mir ist eine Lösung in den Sinn gekommen, und ich dachte, ich würde ihn da rauswerfen.

Regionen, Länder und Staaten sind geografische Standorte, die in einer Hierarchie leben.

Sie können Ihr Problem vollständig vermeiden, indem Sie eine Domänentabelle mit dem Namen geographical_location_type erstellen, die Sie mit drei Zeilen (Region, Land, Bundesland) füllen würden.

Erstellen Sie als Nächstes anstelle der drei Standorttabellen eine einzelne geografische Standorttabelle mit dem Fremdschlüssel geographical_location_type_id (damit Sie wissen, ob es sich bei der Instanz um eine Region, ein Land oder ein Bundesland handelt).

Modellieren Sie die Hierarchie, indem Sie diese Tabelle selbstreferenzieren, sodass eine State-Instanz den fKey für ihre übergeordnete Country-Instanz enthält, die wiederum den fKey für ihre übergeordnete Region-Instanz enthält. Regionsinstanzen würden in diesem fKey NULL enthalten. Dies unterscheidet sich nicht von dem, was Sie mit den drei Tabellen gemacht hätten (Sie hätten 1 - viele Beziehungen zwischen Region und Land sowie zwischen Land und Staat), außer jetzt ist alles in einer Tabelle.

Die Tabelle popular_user_location wäre eine Tabelle zur Bereichsauflösung zwischen user und georgraphical_location (so viele Benutzer könnten viele Orte mögen).

Soooo…

Geben Sie hier die Bildbeschreibung ein

CREATE TABLE [geographical_location_type] (
    [geographical_location_type_id] INTEGER NOT NULL,
    [name] VARCHAR(25) NOT NULL,
    CONSTRAINT [PK_geographical_location_type] PRIMARY KEY ([geographical_location_type_id])
)

-- Add 'Region', 'Country' and 'State' instances to the above table


CREATE TABLE [geographical_location] (
   [geographical_location_id] BIGINT IDENTITY(0,1) NOT NULL,
    [name] VARCHAR(1024) NOT NULL,
    [geographical_location_type_id] INTEGER NOT NULL,
    [geographical_location_parent] BIGINT,  -- self referencing; can be null for top-level instances
    CONSTRAINT [PK_geographical_location] PRIMARY KEY ([geographical_location_id])
)

CREATE TABLE [user] (
    [user_id] BIGINT NOT NULL,
    [login_id] VARCHAR(30) NOT NULL,
    [password] VARCHAR(512) NOT NULL,
    CONSTRAINT [PK_user] PRIMARY KEY ([user_id])
)


CREATE TABLE [popular_user_location] (
    [popular_user_location_id] BIGINT NOT NULL,
    [user_id] BIGINT NOT NULL,
    [geographical_location_id] BIGINT NOT NULL,
    CONSTRAINT [PK_popular_user_location] PRIMARY KEY ([popular_user_location_id])
)

ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_type_geographical_location] 
    FOREIGN KEY ([geographical_location_type_id]) REFERENCES [geographical_location_type] ([geographical_location_type_id])



ALTER TABLE [geographical_location] ADD CONSTRAINT [geographical_location_geographical_location] 
    FOREIGN KEY ([geographical_location_parent]) REFERENCES [geographical_location] ([geographical_location_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [user_popular_user_location] 
    FOREIGN KEY ([user_id]) REFERENCES [user] ([user_id])



ALTER TABLE [popular_user_location] ADD CONSTRAINT [geographical_location_popular_user_location] 
    FOREIGN KEY ([geographical_location_id]) REFERENCES [geographical_location] ([geographical_location_id])

War nicht sicher, was die Ziel-DB war; Das obige ist MS SQL Server.

Toolsmythe
quelle
0

Nun, ich habe zwei Tabellen:

  1. Lieder

a) Songnummer b) Songtitel ....

  1. Wiedergabelisten a) Nummer der Wiedergabeliste b) Titel der Wiedergabeliste ...

und ich habe einen dritten

  1. songs_to_playlist_relation

Das Problem ist, dass einige Arten von Wiedergabelisten Links zu anderen Wiedergabelisten haben. In MySQL haben wir jedoch keinen Fremdschlüssel, der zwei Tabellen zugeordnet ist.

Meine Lösung: Ich werde eine dritte Spalte in songs_to_playlist_relation einfügen. Diese Spalte ist boolesch. Wenn 1 dann Lied, wird sonst mit der Wiedergabelistentabelle verlinkt.

So:

  1. songs_to_playlist_relation

a) Playlist_number (int) b) Ist Song (boolean) c) Relative Nummer (Song-Nummer oder Playlist-Nummer) (int) ( kein Fremdschlüssel für eine Tabelle)

 # Erstellen Tabelle Songs 
    Anfragen . append ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) 
    Abfragen . append ( "CREATE TABLE songs( NUMBERint (11) NOT NULL, SONG POSITIONint (11) NOT NULL, PLAY SONGtinyint (1) NOT NULL DEFAULT '1', SONG TITLEvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, DESCRIPTIONvarchar (1000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, ARTISTvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος καλλιτέχνης', AUTHORvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος στιχουργός', COMPOSERvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL STANDARD 'Άγνωστος συνθέτης',ALBUMvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NICHT NULL STANDARD 'Άγνωστο άλμπουμ', YEARint (11) NICHT NULL STANDARD '33', RATINGint (11) NICHT NULL STANDARD '5', IMAGEvarchar (600) CHARACTER SET utf8 COLLATE_ , SONG PATHvarchar (500) CHARACTER SET utf8 COLLATE utf8_general_ci NICHT NULL, SONG REPEATint (11) NICHT NULL STANDARD '0', VOLUMEfloat NICHT NULL STANDARD '1', SPEEDfloat NICHT NULL STANDARD '1') ENGINE = InnoDB DEFAULT CHARSET = utf8; " ) 
    Abfragen . Anhängen ( "ALTER TABLE songsADD PRIMARY KEY ( NUMBER), ADD UNIQUE KEY POSITION( SONG POSITION), ADD UNIQUE KEY TITLE( SONG TITLE), ADD UNIQUE KEY PATH( SONG PATH);") 
    Abfragen. append ( "ALTER TABLE songsMODIFY NUMBERint (11) NICHT NULL AUTO_INCREMENT;" )

#create table playlists
queries.append("CREATE TABLE `playlists` (`NUMBER` int(11) NOT NULL,`PLAYLIST POSITION` int(11) NOT NULL,`PLAYLIST TITLE` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`PLAYLIST PATH` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `playlists` ADD PRIMARY KEY (`NUMBER`),ADD UNIQUE KEY `POSITION` (`PLAYLIST POSITION`),ADD UNIQUE KEY `TITLE` (`PLAYLIST TITLE`),ADD UNIQUE KEY `PATH` (`PLAYLIST PATH`);")
queries.append("ALTER TABLE `playlists` MODIFY `NUMBER` int(11) NOT NULL AUTO_INCREMENT;")

#create table for songs to playlist relation
queries.append("CREATE TABLE `songs of playlist` (`PLAYLIST NUMBER` int(11) NOT NULL,`SONG OR PLAYLIST` tinyint(1) NOT NULL DEFAULT '1',`RELATIVE NUMBER` int(11) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;")
queries.append("ALTER TABLE `songs of playlist` ADD KEY `PLAYLIST NUMBER` (`PLAYLIST NUMBER`) USING BTREE;")
queries.append("ALTER TABLE `songs of playlist` ADD CONSTRAINT `playlist of playlist_ibfk_1` FOREIGN KEY (`PLAYLIST NUMBER`) REFERENCES `playlists` (`NUMBER`) ON DELETE RESTRICT ON UPDATE RESTRICT")

Das ist alles!

playlists_query = "SELECT s1. *, s3. *, s4. * FROM Songs als s1 INNER JOIN` Songs der Playlist` als s2 ON s1.`NUMBER` = s2.`RELATIVE NUMBER` INNER JOIN` Playlists` als s3 ON s3 .`NUMBER` = s2.`PLAYLIST NUMBER` INNER JOIN `Playlists` als s4 ON s4.`NUMBER` = s2.`RELATIVE NUMBER` ORDER BY s3.`PLAYLIST POSITION`,` s1`.`SONG POSITION` "
Chris P.
quelle