Mysql
 sql >> Database >  >> RDS >> Mysql

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

Quello che stai descrivendo si chiama Associazioni Polimorfiche. Ovvero, la colonna "chiave esterna" contiene un valore id che deve esistere in una delle serie di tabelle di destinazione. In genere le tabelle di destinazione sono correlate in qualche modo, ad esempio essendo istanze di alcune comuni superclassi di dati. Avresti anche bisogno di un'altra colonna accanto alla colonna della chiave esterna, in modo che su ogni riga, puoi designare a quale tabella di destinazione si 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 uno tabella di destinazione.

Le associazioni polimorfiche sono supportate da framework come Rails e Hibernate. Ma dicono esplicitamente che devi disabilitare i vincoli SQL per usare questa funzione. Al contrario, l'applicazione o il framework deve 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 rispetto all'imposizione 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 anche l'applicazione deve essere priva di bug.

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

Crea una tabella aggiuntiva per target. Ad esempio popular_states e popular_countries , che fa riferimento a states e countries rispettivamente. Ciascuna 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 di un utente è necessario interrogare entrambe queste tabelle. Ma significa che puoi fare affidamento sul database per imporre la coerenza.

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

Utilizza 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 essi 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, Polymorphic Associations viola la First Normal Form , perché il popular_place_id è in effetti una colonna con due significati:è uno stato o un paese. Non memorizzeresti l'age di una persona e il loro phone_number in una singola colonna e per lo stesso motivo non dovresti memorizzare entrambi state_id e country_id in un'unica colonna. Il fatto che questi due attributi abbiano tipi di dati compatibili è casuale; significano ancora entità logiche diverse.

Le associazioni polimorfiche violano anche la Terza forma normale , perché il significato della colonna dipende dalla colonna extra che denomina la tabella a cui si riferisce la chiave esterna. Nella terza forma normale, un attributo in una tabella deve dipendere solo dalla chiave primaria di quella tabella.

Re commento da @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ù Filters tabelle, ciascuna contenente una chiave esterna che fa riferimento a un Products centrale tavolo.

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 creare la query SQL. SQL richiede che la tabella sia specificata e corretta al momento della scrittura della query. Non puoi fare in modo che la tabella unita venga scelta dinamicamente in base ai valori trovati nelle singole righe di Products .

L'unica altra opzione è partecipare a tutti filtrare le tabelle utilizzando i join esterni. Quelli che non hanno product_id corrispondente verranno semplicemente restituiti come una singola riga di valori null. Ma devi ancora codificare tutti le tabelle unite, e se aggiungi nuove tabelle filtro, 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 filtri è 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.