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_states
e popular_countries
, quale riferimento states
e countries
rispettivamente. 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 places
tabella 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 states
e 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 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)
);
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_id
in effetti è una colonna con due significati: è uno stato o un paese. Non memorizzeresti una persona age
e la sua phone_number
in una singola colonna e per lo stesso motivo non dovresti memorizzare entrambe state_id
e country_id
in 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ù Filters
tabelle, ognuna contenente una chiave esterna che fa riferimento a una Products
tabella 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.
join
target cambierà ... Sto complicando troppo o cosa? Aiuto!