Dimungkinkan untuk melakukan kunci asing MySQL ke salah satu dari dua tabel yang mungkin?

180

Nah inilah masalah saya, saya punya tiga tabel; wilayah, negara, negara bagian. Negara dapat berada di dalam wilayah, negara dapat berada di dalam wilayah. Daerah adalah bagian atas rantai makanan.

Sekarang saya menambahkan tabel popular_areas dengan dua kolom; region_id dan popular_place_id. Apakah mungkin menjadikan popular_place_id sebagai kunci asing untuk salah satu negara ATAU negara. Saya mungkin harus menambahkan kolom popular_place_type untuk menentukan apakah id menggambarkan suatu negara atau negara baik cara.

Andrew G. Johnson
sumber

Jawaban:

282

Apa yang Anda gambarkan disebut Asosiasi Polimorfik. Yaitu, kolom "kunci asing" berisi nilai id yang harus ada di salah satu dari set tabel target. Biasanya tabel target terkait dalam beberapa cara, seperti menjadi contoh beberapa data superkelas umum. Anda juga membutuhkan kolom lain di samping kolom kunci asing, sehingga pada setiap baris, Anda dapat menentukan tabel target mana yang dirujuk.

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

Tidak ada cara untuk memodelkan Asosiasi Polimorfik menggunakan batasan SQL. Batasan kunci asing selalu merujuk satu tabel target.

Asosiasi Polimorfik didukung oleh kerangka kerja seperti Rails dan Hibernate. Tetapi mereka secara eksplisit mengatakan bahwa Anda harus menonaktifkan batasan SQL untuk menggunakan fitur ini. Sebaliknya, aplikasi atau kerangka kerja harus melakukan pekerjaan yang setara untuk memastikan bahwa referensi terpenuhi. Artinya, nilai dalam kunci asing hadir di salah satu tabel target yang mungkin.

Asosiasi Polymorphic lemah sehubungan dengan menegakkan konsistensi database. Integritas data tergantung pada semua klien yang mengakses database dengan logika integritas referensial yang sama ditegakkan, dan juga penegakannya harus bebas bug.

Berikut adalah beberapa solusi alternatif yang memanfaatkan integritas referensial yang didukung oleh database:

Buat satu tabel tambahan per target. Misalnya popular_statesdan popular_countriesreferensi mana statesdan countriesmasing - masing. Setiap tabel "populer" ini juga merujuk profil pengguna.

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

Ini berarti bahwa untuk mendapatkan semua tempat favorit populer pengguna, Anda perlu menanyakan kedua tabel ini. Tetapi itu berarti Anda dapat mengandalkan database untuk menegakkan konsistensi.

Buat placestabel sebagai supertable. Seperti yang Abie sebutkan, alternatif kedua adalah tempat populer Anda mereferensikan tabel seperti places, yang merupakan induk bagi keduanya statesdan countries. Artinya, baik negara bagian maupun negara juga memiliki kunci asing untuk places(Anda bahkan dapat membuat kunci asing ini juga menjadi kunci utama statesdan 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)
);

Gunakan dua kolom. Alih-alih satu kolom yang dapat mereferensikan salah satu dari dua tabel target, gunakan dua kolom. Dua kolom ini mungkin NULL; pada kenyataannya hanya satu dari mereka yang bukan 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)
);

Dalam hal teori relasional, Asosiasi Polimorfik melanggar Bentuk Normal Pertama , karena popular_place_idpada dasarnya kolom dengan dua makna: itu adalah negara atau negara. Anda tidak akan menyimpan seseorang agedan mereka phone_numberdalam satu kolom, dan untuk alasan yang sama Anda tidak harus menyimpan keduanya state_iddan country_iddalam satu kolom. Fakta bahwa kedua atribut ini memiliki tipe data yang kompatibel adalah kebetulan; mereka masih menandakan entitas logis yang berbeda.

Asosiasi Polimorfik juga melanggar Bentuk Normal Ketiga , karena makna kolom tergantung pada kolom tambahan yang menamai tabel yang dirujuk oleh kunci asing. Dalam Bentuk Normal Ketiga, atribut dalam tabel harus hanya bergantung pada kunci utama tabel itu.


Re komentar dari @SavasVedova:

Saya tidak yakin saya mengikuti uraian Anda tanpa melihat definisi tabel atau kueri contoh, tetapi sepertinya Anda hanya memiliki beberapa Filterstabel, masing-masing berisi kunci asing yang mereferensikan Productstabel pusat .

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

Bergabung dengan produk ke tipe filter tertentu mudah jika Anda tahu tipe yang ingin Anda ikuti:

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

Jika Anda ingin tipe filter menjadi dinamis, Anda harus menulis kode aplikasi untuk membangun kueri SQL. SQL mengharuskan tabel ditentukan dan diperbaiki pada saat Anda menulis kueri. Anda tidak dapat membuat tabel bergabung dipilih secara dinamis berdasarkan nilai-nilai yang ditemukan di baris individu Products.

Satu-satunya opsi lain adalah bergabung ke semua tabel filter menggunakan gabungan luar. Mereka yang tidak memiliki product_id yang cocok hanya akan dikembalikan sebagai satu baris nol. Tetapi Anda masih harus melakukan hardcode semua tabel yang tergabung, dan jika Anda menambahkan tabel filter baru, Anda harus memperbarui kode Anda.

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

Cara lain untuk bergabung ke semua tabel filter adalah melakukannya secara serial:

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

Tetapi format ini masih mengharuskan Anda untuk menulis referensi ke semua tabel. Tidak ada jalan keluarnya.

Bill Karwin
sumber
Mana yang akan Anda sarankan Bill? Saya di tengah-tengah merancang database tapi saya tersesat. Saya pada dasarnya perlu mengaitkan filter ke suatu produk dan nilai-nilai filter akan diisi di berbagai tabel. Tetapi masalahnya adalah bahwa filter akan dihasilkan oleh admin sehingga tergantung pada tipe filter data dapat bervariasi dan karenanya jointarget akan berubah juga ...... Apakah saya terlalu rumit atau apa? Tolong!
Savas Vedova
+1 terima kasih atas solusi yang luar biasa. Satu pertanyaan yang saya miliki dengan solusi pertama / kedua adalah: apakah ada pelanggaran normalisasi dengan fakta bahwa beberapa tabel dapat merujuk ke kunci primer yang sama dalam tabel-meta itu? Saya tahu Anda bisa menyelesaikan ini dengan logika, tapi saya tidak melihat cara apa pun untuk menegakkannya, kecuali saya kehilangan sesuatu.
Rob
5
Saya sangat suka pendekatan dengan "CONSTRAINT CHECK". Tapi itu bisa ditingkatkan jika kita mengubah "OR" menjadi "XOR". Dengan begitu kami memastikan bahwa hanya satu kolom dari set BUKAN NULL
alex_b
1
@alex_b, ya, itu bagus, tetapi XOR logis bukan SQL standar dan tidak didukung oleh semua merek SQL. MySQL memilikinya, tetapi PostgreSQL tidak. Oracle memilikinya, tetapi Microsoft tidak sampai 2016. Dan seterusnya.
Bill Karwin
1
"Dua kolom ini mungkin NULL; faktanya hanya satu yang non-NULL" - ini akan melanggar 1NF!
onedaywhen
10

Ini bukan solusi paling elegan di dunia, tetapi Anda bisa menggunakan warisan tabel konkret untuk membuat karya ini.

Secara konseptual Anda mengusulkan gagasan tentang kelas "hal-hal yang dapat menjadi area populer" dari mana ketiga jenis tempat Anda mewarisi. Anda bisa mewakili ini sebagai tabel yang disebut, misalnya, placesdi mana setiap baris memiliki hubungan satu-ke-satu dengan berturut-turut di regions, countries, atau states. (Atribut yang dibagi antara wilayah, negara, atau negara bagian, jika ada, dapat didorong ke tabel tempat ini.) Anda popular_place_idkemudian akan menjadi referensi kunci asing ke baris di tabel tempat yang kemudian akan membawa Anda ke suatu wilayah, negara , atau negara.

Solusi yang Anda usulkan dengan kolom kedua untuk menggambarkan jenis asosiasi terjadi pada bagaimana Rails menangani asosiasi polimorfik, tetapi saya bukan penggemar itu secara umum. Bill menjelaskan dengan sangat terinci mengapa asosiasi polimorfik bukan teman Anda.

Abie
sumber
1
alias "pola supertipe-subtipe"
ErikE
Juga artikel ini menjelaskan konsep duhallowgreygeek.com/polymorphic-association-bad-sql-smell
Marco Staffoli
5

Berikut ini adalah koreksi terhadap pendekatan "luar biasa" Bill Karwin, menggunakan kunci majemuk ( place_type, place_id )untuk menyelesaikan dugaan pelanggaran bentuk normal:

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

Apa yang desain ini tidak dapat memastikan bahwa untuk setiap baris di placessana ada baris dalam statesatau countries(tetapi tidak keduanya). Ini adalah batasan kunci asing dalam SQL. Dalam DBMS yang sesuai dengan Standar SQL-92, Anda dapat mendefinisikan batasan antar-tabel yang dapat ditangguhkan yang akan memungkinkan Anda untuk mencapai hal yang sama tetapi kikuk, melibatkan transaksi dan DBMS semacam itu belum membuatnya dipasarkan.

suatu hari nanti
sumber
0

Saya menyadari bahwa utas ini sudah tua, tetapi saya melihat ini dan solusi muncul di benak saya dan saya pikir saya akan membuangnya di sana.

Wilayah, Negara, dan Negara adalah Lokasi Geografis yang hidup dalam hierarki.

Anda dapat menghindari masalah Anda sama sekali dengan membuat tabel domain yang disebut geografis_lokasi_type yang akan Anda isi dengan tiga baris (Wilayah, Negara, Negara).

Selanjutnya, alih-alih dari tiga tabel lokasi, buatlah tabel geografis_lokasi tunggal yang memiliki kunci asing geografi_location_type_id (sehingga Anda tahu jika turunannya adalah Wilayah, Negara atau Negara).

Modelkan hierarki dengan membuat tabel ini sebagai referensi-sendiri sehingga sebuah instance State menyimpan fKey ke instance induknya yang pada gilirannya menyimpan fKey pada instance Region induknya. Contoh wilayah akan menahan NULL di fKey itu. Ini tidak berbeda dengan apa yang akan Anda lakukan dengan tiga tabel (Anda akan memiliki 1 - banyak hubungan antara wilayah dan negara dan antara negara dan negara) kecuali sekarang semuanya dalam satu tabel.

Tabel popular_user_location akan menjadi tabel resolusi ruang lingkup antara pengguna dan georgraphical_location (begitu banyak pengguna dapat menyukai banyak tempat).

Soooo ...

masukkan deskripsi gambar di sini

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

Tidak yakin apa target DB; di atas adalah MS SQL Server.

Alat saya
sumber
0

Yah, saya punya dua tabel:

  1. lagu

a) Nomor lagu b) Judul lagu ....

  1. daftar putar a) nomor daftar putar b) judul daftar putar ...

dan saya punya yang ketiga

  1. relasi songs_to_playlist_relation

Masalahnya adalah bahwa beberapa jenis daftar putar memiliki tautan ke daftar putar lain. Tetapi dalam mysql kita tidak memiliki kunci asing yang dikaitkan dengan dua tabel.

Solusi saya: Saya akan meletakkan kolom ketiga di songs_to_playlist_relation. Kolom itu akan menjadi boolean. Jika 1 lagu, maka yang lain akan menautkan ke tabel daftar putar.

Begitu:

  1. relasi songs_to_playlist_relation

a) Nomor daftar putar (int) b) Adalah lagu (boolean) c) Nomor relatif (nomor lagu atau nomor daftar putar) (int) ( bukan kunci asing ke tabel mana pun)

 # buat lagu-lagu tabel 
    pertanyaan . tambahkan ( "SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" ) 
    kueri . append ( "CREATE TABLE songs( NUMBERint (11) BUKAN NULL, SONG POSITIONint (11) BUKAN NULL, PLAY SONGtinyint (1) BUKAN NULL DEFAULT '1', SONG TITLEvarchar (255) KARAKTER SET utf8 COLLATE utf8_general_ci NOT NULL, DESCRIPTIONvarchar (1000) CHARACTER SET utf8 Collate utf8_general_ci NOT NULL, ARTISTvarchar (255) KARAKTER SET utf8 Collate utf8_general_ci NOT NULL DEFAULT 'Άγνωστος καλλιτέχνης', AUTHORvarchar (255) KARAKTER SET utf8 Collate utf8_general_ci NOT NULL DEFAULT 'Άγνωστος στιχουργός', COMPOSERvarchar (255) KARAKTER SET utf8 Collate utf8_general_ci NOT NULL DEFAULT 'Άγνωστος συνθέτης',ALBUMvarchar (255) SET KARAKTER utf8 MENGUMPULKAN utf8_general_ci TIDAK NULL DEFAULT 'Άγνωστο άλμπουμ', YEARint (11) NOT NULL DEFAULT '33', RATINGint (11) NOT NULL DEFAULT '5', IMAGEvarchar (600) CHARACTER SET utf8 utf8 , SONG PATHvarchar (500) SET KARAKTER utf8 MENGUMPULKAN utf8_general_ci TIDAK NULL, SONG REPEATint (11) TIDAK NULL DEFAULT '0', VOLUMEfloat NOT NULL DEFAULT '1', SPEEDfloat NOT NULL DEFAULT '1') ENGINE = InnoDB DEFAULT CHARSET = utf8 " ) 
    kueri . tambahkan ( "ALTER TABLE songsADD KUNCI UTAMA ( NUMBER), Tambahkan KUNCI UNIK POSITION( SONG POSITION), Tambahkan KUNCI UNIK TITLE( SONG TITLE), Tambahkan KUNCI UNIK PATH( SONG PATH);") 
    pertanyaan. tambahkan ( "ALTER TABLE songsMODIFY NUMBERint (11) BUKAN 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")

Itu saja!

playlists_query = "SELECT s1. *, s3. *, s4. * DARI lagu sebagai s1 INNER GABUNG` lagu daftar putar` sebagai s2 ON s1.`NUMBER` = s2.`RELATIVE NUMBER` INNER JOIN `playlist` s s3 ON s3 ON s3 .`NUMBER` = s2.`PLAYLIST NUMBER` INNER GABUNG `playlist` s4 ON s4.`NUMBER` = s2.`RELATIVE NUMBER` ORDER OLEH s3.`PLAYLIST POSISI`,` s1`.`SONG POSISI` "
Chris P.
sumber