È possibile eseguire una chiave esterna MySQL su una delle due tabelle possibili?


180

Bene, ecco il mio problema, ho tre tavoli; regioni, paesi, stati. I paesi possono essere all'interno delle regioni, gli stati possono essere all'interno delle regioni. Le regioni sono il vertice della catena alimentare.

Ora sto aggiungendo una tabella popular_areas con due colonne; region_id e popular_place_id. È possibile rendere popular_place_id una chiave esterna per entrambi i paesi o gli stati. Probabilmente dovrò aggiungere una colonna popular_place_type per determinare se l'id sta descrivendo un paese o uno stato in entrambi i modi.

Risposte:


282

Quello che stai descrivendo si chiama Associazioni polimorfiche. Cioè, la colonna "chiave esterna" contiene un valore ID che deve esistere in una di una serie di tabelle di destinazione. In genere, le tabelle di destinazione sono in qualche modo correlate, come ad esempio istanze di alcune superclassi di dati comuni. Avresti anche bisogno di un'altra colonna lungo il lato della colonna chiave esterna, in modo che su ogni riga, puoi designare quale tabella di destinazione fa riferimento.

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

Non c'è modo di modellare le associazioni polimorfiche usando i vincoli SQL. Un vincolo di chiave esterna fa sempre riferimento a una tabella di destinazione.

Le associazioni polimorfiche sono supportate da framework come Rails e Hibernate. Ma affermano esplicitamente che è necessario disabilitare i vincoli SQL per utilizzare questa funzione. Invece, l'applicazione o il framework devono svolgere un lavoro equivalente per garantire che il riferimento sia soddisfatto. Cioè, il valore nella chiave esterna è presente in una delle possibili tabelle di destinazione.

Le associazioni polimorfiche sono deboli per quanto riguarda l'applicazione della coerenza del database. L'integrità dei dati dipende da tutti i client che accedono al database con la stessa logica di integrità referenziale applicata e che l'applicazione deve essere priva di bug.

Ecco alcune soluzioni alternative che sfruttano l'integrità referenziale imposta dal database:

Crea una tabella extra per target. Ad esempio popular_statese popular_countries, quale riferimento statese countriesrispettivamente. Ognuna di queste tabelle "popolari" fa riferimento anche al profilo dell'utente.

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

Ciò significa che per ottenere tutti i luoghi preferiti popolari di un utente è necessario eseguire una query su entrambe queste tabelle. Ma significa che puoi fare affidamento sul database per imporre coerenza.

Crea una placestabella come supertable. Come menziona Abie, una seconda alternativa è che i tuoi luoghi popolari fanno riferimento a una tabella simile places, che è un genitore di entrambi statese countries. Cioè, sia gli stati che i paesi hanno anche una chiave esterna per places(puoi anche rendere questa chiave esterna anche la chiave primaria di statese 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)
);

Usa due colonne. Invece di una colonna che può fare riferimento a una delle due tabelle di destinazione, utilizzare due colonne. Queste due colonne possono essere NULL; infatti solo uno di loro dovrebbe essere non 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 termini di teoria relazionale, le associazioni polimorfiche violano la prima forma normale , perché popular_place_idin effetti è una colonna con due significati: è uno stato o un paese. Non memorizzeresti una persona agee la sua phone_numberin una singola colonna e per lo stesso motivo non dovresti memorizzare entrambe state_ide country_idin una singola colonna. Il fatto che questi due attributi abbiano tipi di dati compatibili è una coincidenza; significano ancora diverse entità logiche.

Anche le associazioni polimorfiche violano la terza forma normale , perché il significato della colonna dipende dalla colonna aggiuntiva che nomina la tabella a cui si riferisce la chiave esterna. In Third Normal Form, un attributo in una tabella deve dipendere solo dalla chiave primaria di quella tabella.


Commento di @SavasVedova:

Non sono sicuro di seguire la tua descrizione senza vedere le definizioni della tabella o una query di esempio, ma sembra che tu abbia semplicemente più Filterstabelle, ognuna contenente una chiave esterna che fa riferimento a una Productstabella centrale .

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...

Unire i prodotti a un tipo specifico di filtro è facile se sai a quale tipo vuoi unirti:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Se si desidera che il tipo di filtro sia dinamico, è necessario scrivere il codice dell'applicazione per costruire la query SQL. SQL richiede che la tabella sia specificata e corretta al momento in cui si scrive la query. Non è possibile scegliere la tabella unita in modo dinamico in base ai valori trovati nelle singole righe diProducts .

L'unica altra opzione è quella di unire tutte le tabelle di filtro usando i join esterni. Quelli che non hanno product_id corrispondenti verranno restituiti come un'unica riga di valori null. Ma devi ancora codificare tutte le tabelle unite e se aggiungi nuove tabelle di filtri, devi aggiornare il tuo codice.

SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...

Un altro modo per unirsi a tutte le tabelle di filtro è farlo in serie:

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)
...

Ma questo formato richiede comunque di scrivere riferimenti a tutte le tabelle. Non c'è niente da fare.


Quale consiglieresti a Bill? Sono nel mezzo della progettazione di un database ma mi sono perso. Fondamentalmente ho bisogno di associare i filtri a un prodotto e i valori dei filtri saranno popolati in diverse tabelle. Ma il problema è che i filtri verranno generati dagli amministratori, quindi a seconda del tipo di filtro i dati possono variare e quindi anche il jointarget cambierà ... Sto complicando troppo o cosa? Aiuto!
Savas Vedova,

+1 grazie per un'ottima soluzione. Una domanda che ho con la prima / seconda soluzione è: c'è qualche violazione della normalizzazione con il fatto che più tabelle possono fare riferimento alla stessa chiave primaria in quella meta-tabella? So che puoi risolverlo con la logica, ma non vedo alcun modo per il database di applicarlo, a meno che non mi manchi qualcosa.
Rob,

5
Mi piace molto l'approccio con "CONSTRAINT CHECK". Ma può essere migliorato se cambiamo "OR" in "XOR". In questo modo assicuriamo che una sola colonna del set NON sia NULL
alex_b

1
@alex_b, sì, va bene, ma XOR logico non è SQL standard e non è supportato da tutti i marchi SQL. MySQL ce l'ha, ma PostgreSQL no. Oracle ce l'ha, ma Microsoft non lo fa fino al 2016. E così via.
Bill Karwin,

1
"Queste due colonne possono essere NULL, infatti solo uno di loro dovrebbe essere non NULL" - questo sarebbe violare 1NF!
giorno

10

Questa non è la soluzione più elegante al mondo, ma potresti usare l' eredità della tabella concreta per farlo funzionare.

Concettualmente stai proponendo una nozione di una classe di "cose ​​che possono essere aree popolari" da cui ereditano i tuoi tre tipi di luoghi. Si potrebbe rappresentare questo come una tabella chiamata, per esempio, placesin cui ogni riga ha una relazione uno-a-uno con una fila in regions, countrieso states. (Gli attributi che sono condivisi tra regioni, paesi o stati, se presenti, potrebbero essere inseriti in questa tabella dei luoghi.) popular_place_idQuindi si tratterebbe di un riferimento di chiave esterna a una riga nella tabella dei luoghi che ti porterebbe quindi a una regione, a un paese o stato.

La soluzione che proponi con una seconda colonna per descrivere il tipo di associazione sembra essere il modo in cui Rails gestisce le associazioni polimorfiche, ma non sono un fan di questo in generale. Bill spiega in modo eccellente perché le associazioni polimorfiche non sono tue amiche.


1
alias "il modello del sottotipo di supertipo"
ErikE

Anche questo articolo spiega bene il concetto duhallowgreygeek.com/polymorphic-association-bad-sql-smell
Marco Staffoli,

5

Ecco una correzione dell'approccio "supertable" di Bill Karwin, usando una chiave composta ( place_type, place_id )per risolvere le violazioni della forma normale percepita:

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

Ciò che questo design non può garantire che per ogni riga placespresente esista una riga in stateso countries(ma non entrambe). Questo è un limite di chiavi esterne in SQL. In un DBMS completo conforme agli standard SQL-92 è possibile definire vincoli inter-tabella differibili che ti consentirebbero di ottenere lo stesso ma è ingombrante, implica una transazione e un tale DBMS deve ancora arrivare sul mercato.


0

Mi rendo conto che questo thread è vecchio, ma ho visto questo e mi è venuta in mente una soluzione e ho pensato di buttarlo lì.

Regioni, Paesi e Stati sono Luoghi geografici che vivono in una gerarchia.

È possibile evitare del tutto il problema creando una tabella di domini denominata Geographic_location_type che si popolerebbe con tre righe (Regione, Paese, Stato).

Successivamente, invece delle tre tabelle delle posizioni, crea una singola tabella di posizione geografica che ha una chiave esterna di Geographic_location_type_id (in modo da sapere se l'istanza è una Regione, Paese o Stato).

Modella la gerarchia rendendo questa tabella autoreferenziale in modo tale che un'istanza di Stato mantenga fKey sull'istanza del Paese di appartenenza che a sua volta trattiene fKey sull'istanza della sua regione padre. Le istanze di regione manterrebbero NULL in quel tasto. Questo non è diverso da quello che avresti fatto con le tre tabelle (avresti 1 - molte relazioni tra regione e paese e tra paese e stato) tranne ora che è tutto in una tabella.

La tabella popular_user_location sarebbe una tabella di risoluzione dell'ambito tra user e georgraphical_location (quindi a molti utenti potrebbero piacere molti posti).

Soooo ...

inserisci qui la descrizione dell'immagine

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])

Non ero sicuro di quale fosse il DB di destinazione; quanto sopra è MS SQL Server.


0

Bene, ho due tavoli:

  1. canzoni

a) Numero del brano b) Titolo del brano ....

  1. playlist a) Numero playlist b) Titolo playlist ...

e ne ho un terzo

  1. songs_to_playlist_relation

Il problema è che alcuni tipi di playlist hanno collegamenti ad altre playlist. Ma in mysql non abbiamo chiave esterna associata a due tabelle.

La mia soluzione: inserirò una terza colonna in songs_to_playlist_relation. Quella colonna sarà booleana. Se 1 quindi il brano, altrimenti si collegherà alla tabella delle playlist.

Così:

  1. songs_to_playlist_relation

a) Playlist_number (int) b) È il brano (booleano) c) Numero relativo (numero del brano o numero della playlist) (int) ( non chiave esterna per qualsiasi tabella)

 # crea tabella  brani da
    domande . 
    query di accodamento ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) . append ( "CREATE TABLE ( int (11) NOT NULL, int (11) NOT NULL, tinyint (1) NOT NULL DEFAULT '1', varchar (255) SET DI CARATTERI utf8 COLLATE utf8_general_ci NOT NULL, varchar (1000) SET DI CARATTERI utf8 COLLATE utf8_general_ci NOT NULL, varchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NON DI DEFAULT NULL 'Άγνωστος καλλιτέχνης', varchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος στιχουργός', varchar (255) CHARACTER SET utf8 COLLATE NOT utf8_general_ci NULL DEFAULT "Άγνωστος συνθέτης",songsNUMBERSONG POSITIONPLAY SONGSONG TITLEDESCRIPTIONARTISTAUTHORCOMPOSERALBUMvarchar (255) SET DI CARATTERI utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστο άλμπουμ', YEARint (11) NOT NULL DEFAULT '33', RATINGint (11) NOT NULL DEFAULT '5', IMAGEvarchar (600) CHARACTER SET utf8 NOTULL__g_ric , SONG PATHvarchar (500) SET DI CARATTERI utf8 COLLATE utf8_general_ci NON NULL, SONG REPEATint (11) NON NULL DEFAULT '0', VOLUMEfloat NOT NULL DEFAULT '1', SPEEDfloat NOT NULL DEFAULT '1') MOTORE = InnoDB DEFAULT CHARSET = utf8; " ) 
    query . append ( "ALTER TABLE songsADD PRIMARY KEY ( NUMBER), ADD UNIQUE KEY POSITION( SONG POSITION), ADD UNIQUE KEY TITLE( SONG TITLE), ADD UNIQUE KEY PATH( SONG PATH);") 
    query. append ( "ALTER TABLE songsMODIFY NUMBERint (11) NOT 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")

È tutto!

playlists_query = "SELEZIONA s1. *, s3. *, s4. * DA brani come s1 INNER JOIN` brani della playlist` come s2 ON s1.`NUMBER` = s2.`RELATIVE NUMBER` INNER JOIN `playlists` come s3 ON s3 .`NUMBER` = s2.`PLAYLIST NUMBER` INNER JOIN `playlist” come s4 ON s4`NUMBER` = s2 `NUMERO RELATIVO` ORDINARE PER s3`PLAYLIST POSITION`,` s1`.`SONG POSITION` "
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.