Wat je beschrijft, worden polymorfe associaties genoemd. Dat wil zeggen, de kolom "vreemde sleutel" bevat een id-waarde die moet voorkomen in een van een set doeltabellen. Meestal zijn de doeltabellen op de een of andere manier gerelateerd, zoals instanties van een algemene superklasse van gegevens. U heeft ook een andere kolom nodig naast de kolom met externe sleutels, zodat u in elke rij kunt aangeven naar welke doeltabel wordt verwezen.
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
);
Er is geen manier om polymorfe associaties te modelleren met behulp van SQL-beperkingen. Een externe sleutelbeperking verwijst altijd naar één doeltabel.
Polymorphic Associations worden ondersteund door frameworks zoals Rails en Hibernate. Maar ze zeggen expliciet dat je SQL-beperkingen moet uitschakelen om deze functie te gebruiken. In plaats daarvan moet de applicatie of het framework gelijkwaardig werk doen om ervoor te zorgen dat aan de referentie wordt voldaan. Dat wil zeggen, de waarde in de refererende sleutel is aanwezig in een van de mogelijke doeltabellen.
Polymorfe associaties zijn zwak met betrekking tot het afdwingen van databaseconsistentie. De gegevensintegriteit hangt af van het feit dat alle clients toegang hebben tot de database met dezelfde toegepaste referentiële integriteitslogica, en ook de handhaving moet vrij zijn van bugs.
Hier zijn enkele alternatieve oplossingen die profiteren van door databases afgedwongen referentiële integriteit:
Maak één extra tafel per doel. Bijvoorbeeld popular_states
en popular_countries
, die verwijst naar states
en countries
respectievelijk. Elk van deze "populaire" tabellen verwijst ook naar het gebruikersprofiel.
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),
);
Dit betekent wel dat om alle populaire favoriete plaatsen van een gebruiker te krijgen, u beide tabellen moet doorzoeken. Maar het betekent dat u op de database kunt vertrouwen om consistentie af te dwingen.
Maak een places
tafel als een supertafel. Zoals Abie vermeldt, is een tweede alternatief dat uw populaire plaatsen verwijzen naar een tabel zoals places
, wat een ouder is voor beide states
en countries
. Dat wil zeggen, zowel staten als landen hebben ook een vreemde sleutel naar places
(je kunt deze externe sleutel zelfs ook de primaire sleutel maken van states
en 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)
);
Gebruik twee kolommen. Gebruik twee kolommen in plaats van één kolom die naar een van de twee doeltabellen kan verwijzen. Deze twee kolommen kunnen NULL
zijn; in feite mag er maar één niet-NULL
. zijn .
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 termen van relationele theorie schendt Polymorphic Associations Eerste normaalvorm
, omdat de popular_place_id
is in feite een kolom met twee betekenissen:het is een staat of een land. Je zou de age
van een persoon niet opslaan en hun phone_number
in een enkele kolom, en om dezelfde reden moet u niet beide state_id
. opslaan en country_id
in een enkele kolom. Het feit dat deze twee attributen compatibele datatypes hebben, is toeval; ze duiden nog steeds verschillende logische entiteiten aan.
Polymorfe associaties schendt ook Derde normale vorm , omdat de betekenis van de kolom afhangt van de extra kolom die de tabel noemt waarnaar de refererende sleutel verwijst. In de derde normaalvorm mag een attribuut in een tabel alleen afhankelijk zijn van de primaire sleutel van die tabel.
Re reactie van @SavasVedova:
Ik weet niet zeker of ik je beschrijving volg zonder de tabeldefinities of een voorbeeldquery te zien, maar het klinkt alsof je gewoon meerdere Filters
hebt tabellen, elk met een externe sleutel die verwijst naar een centrale Products
tafel.
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...
Het koppelen van de producten aan een specifiek type filter is eenvoudig als u weet bij welk type u zich wilt aansluiten:
SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)
Als u wilt dat het filtertype dynamisch is, moet u toepassingscode schrijven om de SQL-query te maken. SQL vereist dat de tabel wordt opgegeven en gerepareerd op het moment dat u de query schrijft. U kunt de samengevoegde tabel niet dynamisch laten kiezen op basis van de waarden in afzonderlijke rijen van Products
.
De enige andere optie is om lid te worden van allen filter tabellen met behulp van outer joins. Degenen die geen overeenkomende product_id hebben, worden alleen geretourneerd als een enkele rij nulls. Maar je moet nog steeds alle hardcoderen de samengevoegde tabellen, en als u nieuwe filtertabellen toevoegt, moet u uw code bijwerken.
SELECT * FROM Products
LEFT OUTER JOIN FiltersType1 USING (product_id)
LEFT OUTER JOIN FiltersType2 USING (product_id)
LEFT OUTER JOIN FiltersType3 USING (product_id)
...
Een andere manier om deel te nemen aan alle filtertabellen is door het serieel te doen:
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)
...
Maar dit formaat vereist nog steeds dat je verwijzingen naar alle tabellen schrijft. Daar kun je niet omheen.