Mümkün olan iki tablodan birine MySQL yabancı anahtarı yapmak mümkün müdür?


180

İşte benim sorunum üç tablo var; bölgeler, ülkeler, eyaletler. Ülkeler bölgelerin içinde olabilir, eyaletler bölgelerin içinde olabilir. Bölgeler besin zincirinin en tepesidir.

Şimdi iki sütunlu bir popular_areas tablosu ekliyorum; region_id ve popular_place_id. Popular_place_id'in iki ülke VEYA eyaleti için de yabancı anahtar olmasını sağlamak mümkün mü ? Muhtemelen kimliği bir ülke veya eyalet her iki şekilde açıklıyor olup olmadığını belirlemek için bir popular_place_type sütun eklemek zorunda kalacak.

Yanıtlar:


282

Açıkladığınız şeye Polimorfik Dernekler denir. Diğer bir deyişle, "yabancı anahtar" sütunu, bir dizi hedef tablodan birinde bulunması gereken bir kimlik değeri içerir. Tipik olarak hedef tablolar, bazı yaygın veri sınıflarının örnekleri gibi bir şekilde ilişkilidir. Ayrıca, yabancı anahtar sütununun yanında başka bir sütuna ihtiyacınız olacak, böylece her satırda hangi hedef tabloya başvuruda bulunulacağını belirtebilirsiniz.

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

SQL kısıtlamaları kullanarak Polimorfik İlişkileri modellemenin bir yolu yoktur. Yabancı anahtar kısıtlaması her zaman bir hedef tabloya başvurur.

Çok Biçimli İlişkiler, Rails ve Hibernate gibi çerçevelerle desteklenir. Ancak açıkça bu özelliği kullanmak için SQL kısıtlamalarını devre dışı bırakmanız gerektiğini söylüyorlar. Bunun yerine, başvurunun karşılandığından emin olmak için uygulama veya çerçeve eşdeğer çalışmalar yapmalıdır. Yani, yabancı anahtardaki değer olası hedef tablolarından birinde bulunur.

Çok Biçimli Dernekler veritabanı tutarlılığının güçlendirilmesi bakımından zayıftır. Veri bütünlüğü, aynı başvuru bütünlüğü mantığının uygulandığı veritabanına erişen tüm istemcilere bağlıdır ve ayrıca uygulamanın hatasız olması gerekir.

Veritabanının uyguladığı referans bütünlüğünden yararlanan bazı alternatif çözümler şunlardır:

Hedef başına bir ekstra tablo oluşturun. Örneğinpopular_states ve popular_countriesaynı zamanda, referans statesve countriessırasıyla. Bu "popüler" tabloların her biri de kullanıcının profilini gösterir.

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

Bu, bir kullanıcının popüler favori yerlerinin tümünü almak için bu tabloların her ikisini de sorgulamanız gerektiği anlamına gelir. Ancak tutarlılığı uygulamak için veritabanına güvenebileceğiniz anlamına gelir.

placesSüper tablo olarak bir tablo oluşturun . Abie'nin belirttiği gibi, ikinci bir alternatif, popüler yerlerinizin placeshem statesve hem de ebeveynleri gibi bir tabloya başvurmasıdır countries. Yani, hem eyaletlerin hem de ülkelerin de yabancı bir anahtarı vardır places(bu yabancı anahtarı bilestates ve vecountries ).

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

İki sütun kullanın. İki hedef tablodan birine başvurabilecek bir sütun yerine iki sütun kullanın. Bu iki sütunNULL ; aslında bunlardan sadece biri olmamalıdır 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)
);

İlişkisel teori açısından, Polimorfik Dernekler İlk Normal Formu ihlal eder , çünkü bu popular_place_idaslında iki anlamı olan bir sütundur: ya bir eyalet ya da bir ülke. Bir kişinin ageve onlarınkini phone_numbertek bir sütunda depolamazsınız ve aynı nedenle her ikisini de state_idve country_idtek bir sütunda saklamamalısınız . Bu iki özelliğin uyumlu veri türlerine sahip olması rastlantısaldır; hala farklı mantıksal varlıkları ifade ediyorlar.

Polimorfik İlişkiler ayrıca Üçüncü Normal Formu ihlal eder , çünkü sütunun anlamı yabancı anahtarın başvurduğu tabloyu adlandıran ekstra sütuna bağlıdır. Üçüncü Normal Formda, tablodaki bir öznitelik yalnızca o tablonun birincil anahtarına bağlı olmalıdır.


@ SavasVedova'dan yorum:

Tablo tanımlarını veya örnek bir sorguyu görmeden açıklamanızı takip ettiğimden emin değilim, ancak Filtersher biri merkezi bir Productstabloya başvuran yabancı bir anahtar içeren birden çok tablonuz var gibi görünüyor.

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

Hangi türe katılmak istediğinizi biliyorsanız, ürünleri belirli bir filtreye eklemek kolaydır:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Filtre türünün dinamik olmasını istiyorsanız, SQL sorgusunu oluşturmak için uygulama kodu yazmanız gerekir. SQL, sorguyu yazarken tablonun belirtilmesini ve sabitlenmesini gerektirir. Birleştirilen tablonun tek tek satırlarında bulunan değerlere göre dinamik olarak seçilmesini sağlayamazsınızProducts .

Diğer tek seçenek dış birleştirmeler kullanarak tüm filtre tablolarına katılmaktır . Eşleşen product_id değeri olmayanlar yalnızca tek bir boş satır olarak döndürülür. Ancak yine de birleştirilmiş tüm tabloları kodlamanız gerekir ve yeni filtre tabloları eklerseniz, kodunuzu güncellemeniz gerekir.

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

Tüm filtre tablolarına katılmanın başka bir yolu da bunu seri olarak yapmaktır:

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

Ancak bu biçim yine de tüm tablolara referanslar yazmanızı gerektirir. Bunun üstesinden gelmek yok.


Hangisini Bill önerirsiniz? Bir veritabanı tasarlamanın ortasındayım ama kayboldum. Temelde filtreleri bir ürünle ilişkilendirmem gerekiyor ve filtrelerin değerleri farklı tablolar arasında doldurulacak. Ancak sorun, filtrelerin yöneticiler tarafından üretilmesidir, bu nedenle filtre türüne bağlı olarak veriler değişebilir ve bu nedenle joinhedef de değişecektir ...... Çok fazla mı karmaşıklaşıyorum ya da ne? Yardım!
Savas Vedova

+1 harika bir çözüm için teşekkür ederim. Birinci / ikinci çözümle ilgili bir sorum var: birden çok tablonun bu meta tablodaki aynı birincil anahtara başvurabileceği gerçeğiyle ilgili herhangi bir normalleştirme ihlali var mı? Bunu mantıkla çözebileceğinizi biliyorum, ama bir şey eksik olmadıkça veritabanının zorlaması için herhangi bir yol göremiyorum.
Rob

5
"CONSTRAINT CHECK" ile yaklaşımı çok seviyorum. Ancak "VEYA" yı "XOR" olarak değiştirirsek geliştirilebilir. Bu şekilde, kümeden yalnızca bir sütunun NULL
DEĞİLDİĞİNİ

1
@alex_b, evet, bu iyi, ama mantıksal XOR standart SQL değil ve tüm SQL markaları tarafından desteklenmiyor. MySQL'de var, ancak PostgreSQL'de yok. Oracle'da var, ancak Microsoft 2016'ya kadar sahip değil.
Bill Karwin

1
"Bu iki sütun NULL olabilir; aslında onlara NULL olmayan olmalı sadece bir" - Bu ederim 1nf ihlal!
oneday2

10

Bu dünyadaki en zarif çözüm değil, ancak bu işi yapmak için beton masa mirasını kullanabilirsiniz.

Kavramsal olarak, üç tür yerinizin miras aldığı bir "popüler alanlar olabilecek şeyler" kavramını öneriyorsunuz. Sen Örneğin, adlı bir tablo olarak bu temsil edebilir placesher satır bir satırla bire bir ilişki vardır nerede regions, countriesya da states. (Varsa bölgeler, ülkeler veya eyaletler arasında paylaşılan özellikler bu yerler tablosuna aktarılabilir.) popular_place_idArdından, yerler tablosunda sizi bir bölgeye, ülkeye yönlendirecek bir satıra yabancı anahtar referansınız olur veya eyalet.

İlişki türünü tanımlamak için ikinci bir sütunla önerdiğiniz çözüm, Rails'in polimorfik ilişkilendirmeleri nasıl ele aldığıdır, ancak genel olarak bunun hayranı değilim. Bill, polimorfik derneklerin neden sizin arkadaşınız olmadığını çok iyi açıklıyor.


1
aka "supertype-
subtype


5

İşte ( place_type, place_id )algılanan normal form ihlallerini çözmek için bileşik bir anahtar kullanarak Bill Karwin'in "supertable" yaklaşımında bir düzeltme :

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

Bu tasarımın içerdiği her satır için veya placesiçinde bir satır olmasını (ancak her ikisini birden değil) sağlayamaz. Bu, SQL'deki yabancı anahtarların bir sınırlamasıdır. Tam SQL-92 Standartları ile uyumlu bir DBMS'de, bunu başarmanıza izin verecek ertelenebilir tablolar arası kısıtlamalar tanımlayabilirsiniz, ancak bu karmaşıktır, işlemi içerir ve böyle bir DBMS henüz piyasaya sürmemiştir.statescountries


0

Bu iş parçacığının eski olduğunu fark ettim, ama bunu gördüm ve bir çözüm aklıma geldi ve orada atacağımı düşündüm.

Bölgeler, Ülkeler ve Devletler hiyerarşide yaşayan Coğrafi Konumlardır.

Üç satırla (Bölge, Ülke, Eyalet) dolduracağınız geographical_location_type adlı bir alan tablosu oluşturarak sorununuzu tamamen önleyebilirsiniz.

Ardından, üç konum tablosu yerine, geographic_location_type_id dosyasının yabancı anahtarına sahip tek bir geographic_location tablosu oluşturun (böylece örneğin bir Bölge, Ülke veya Eyalet olup olmadığını biliyorsunuz).

Bir State örneğinin fKey öğesini kendi üst Country örneğinde tutması ve bu sırada fKey öğesini üst Bölge örneğine tutması için bu tabloyu kendi kendine başvuruda bulunarak hiyerarşiyi modelleyin. Bölge örnekleri o fKey'de NULL tutacaktır. Bu, üç tabloyla yaptıklarınızdan farklı değildir (bölge ve ülke ile ülke ve eyalet arasında 1 - birçok ilişkiniz olur), ancak şimdi hepsi tek bir tabloda.

Popular_user_location tablosu, kullanıcı ve georgraphical_location arasında bir kapsam çözümleme tablosu olacaktır (birçok kullanıcı birçok yeri beğenebilir).

Soooo…

resim açıklamasını buraya girin

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

Hedef DB ne olduğundan emin değildi; Yukarıdaki MS SQL Server.


0

İki masam var:

  1. şarkılar

a) Şarkı numarası b) Şarkı adı ....

  1. çalma listeleri a) Çalma listesi numarası b) Çalma listesi başlığı ...

ve üçte bir var

  1. songs_to_playlist_relation

Sorun, bazı çalma listelerinin diğer çalma listelerine bağlanmasıdır. Ancak mysql'de iki tabloyla ilişkili yabancı anahtarımız yok.

Benim çözümüm: songs_to_playlist_relation içine üçüncü bir sütun koyacağım. Bu sütun boole olacak. 1 sonra şarkı, başka çalma listesi tablosuna bağlanır.

Yani:

  1. songs_to_playlist_relation

a) Çalma listesi_numarası (int) b) Şarkı mı (boole) c) Göreceli sayı (şarkı numarası veya çalma listesi numarası) (int) ( hiçbir tablonun yabancı anahtarı değil )

 # tablo şarkıları oluşturma 
    sorgular . append ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) 
    sorguları . append ( "TABLO OLUŞTUR songs( NUMBERint (11) BOŞ DEĞİL, SONG POSITIONint (11) BOŞ DEĞİL, PLAY SONGtinyint (1) BOŞ DEĞİL '1', int (11) BOŞ DEĞİL '33', int (11) BOŞ DEĞİL ' 5 ', varchar (600) KARAKTER SETİ utf8 COLLATE utf8_general_ci BOŞ DEĞİL, varchar (500) KARAKTER SETİ utf8 COLLATE utf8_general_ci BOŞ DEĞİL, int (11) BOŞ DEĞİL' 0 ', DEĞİL DEĞİL DEĞİL' 1 ', DEĞİL DEĞİL BOŞ DEĞİL ' 1 ', '1') MOTOR = InnoDB VARSAYILAN CHARSET = utf8; " ) 
    sorgular .SONG TITLE varchar (255) KARAKTER SET utf8 COLLATE utf8_general_ci NULL DEĞİL, DESCRIPTIONvarchar (1000) KARAKTER SET utf8 Utf8_general_ci NULL DEĞİL, ARTISTvarchar (255) KARAKTER SETİ utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστος καλλιτέχνης', AUTHORvarchar (255) CHARACTER SET utf8 COLLATE utf8_general_ci DEĞİL υ DEĞİŞTİR 255 DEĞİŞTİR 255 DEĞİŞTİR 255 DEĞİŞTİR 255 DEĞİŞTİR 255 DEĞİŞTİR COMPOSER255 VARSAYILAN 'Άγνωστος συνθέτης',ALBUMvarchar (255) KARAKTER SETİ utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστο άλμπουμ', utfgen N__ ( benzersiz bir anahtar ADD) ( );") 
    sorgularYEARRATINGIMAGESONG PATHSONG REPEATVOLUMESPEED"Alter tablo songsbirincil anahtar ( NUMBER) ekleyin benzersiz anahtar POSITION( SONG POSITION), benzersiz anahtar ekleyinTITLESONG TITLEPATHSONG PATH. 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")

Bu kadar!

playlists_query = "SELECT s1. *, s3. *, s4. * Şarkılardan s1 INNER JOIN` çalma listesinin şarkılarını s2 AÇIK s1 olarak .`NUMBER` = s2. .`NUMBER` = s2.`PLAYLIST NUMBER` INNER` çalma listelerine`4 s4 AÇIK olarak s4.`NUMBER` = s2.`LESEL NUMBER` SİPARİŞ S3.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.