2つの可能なテーブルの1つにMySQL外部キーを実行することは可能ですか?


180

これが私の問題です。3つのテーブルがあります。地域、国、州。国は地域の内部にある場合があり、州は地域の内部にある場合があります。地域は食物連鎖のトップです。

ここで、2つの列を持つPopular_Areasテーブルを追加します。region_idおよびpopular_place_id。popular_place_idを国または州の外部キーにすることは可能ですか?おそらく、idが国または州のどちらを表すのかを判別するために、popular_place_type列を追加する必要があります。

回答:


282

あなたが説明しているものは、ポリモーフィックな関連付けと呼ばれています。つまり、「外部キー」列には、一連のターゲットテーブルの1つに存在する必要があるid値が含まれます。通常、ターゲットテーブルは、データの一般的なスーパークラスのインスタンスであるなど、何らかの方法で関連付けられています。また、外部キー列の横に別の列が必要になるため、各行で、参照するターゲットテーブルを指定できます。

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制約を使用してポリモーフィックな関連付けをモデル化する方法はありません。外部キー制約は常に1つのターゲットテーブルを参照します。

ポリモーフィックな関連付けは、RailsやHibernateなどのフレームワークでサポートされています。ただし、この機能を使用するにはSQL制約を無効にする必要があると明確に述べています。代わりに、アプリケーションまたはフレームワークは、参照が確実に満たされるように同等の作業を行う必要があります。つまり、外部キーの値は、可能なターゲットテーブルの1つに存在します。

ポリモーフィックアソシエーションは、データベースの一貫性の強制に関して弱いです。データの整合性は、同じ参照整合性ロジックが適用された状態でデータベースにアクセスするすべてのクライアントに依存します。また、実施にはバグがないことが必要です。

次に、データベースによる参照整合性を利用する代替ソリューションをいくつか示します。

ターゲットごとに1つの追加テーブルを作成します。たとえば、popular_statesおよびはpopular_countries、それぞれ参照statescountriesます。これらの「人気のある」各テーブルは、ユーザーのプロファイルも参照します。

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

つまり、ユーザーの人気のあるお気に入りの場所をすべて取得するには、これらの両方のテーブルにクエリを実行する必要があります。ただし、データベースに依存して整合性を確保できることを意味します。

placesスーパーテーブルとしてテーブルを作成します。 アビーは言及ように、第二の選択肢は、あなたの人気のある場所は次のようにテーブルを参照することでplaces、両方の親である、statescountries。つまり、州と国の両方に外部キーがありplacesます(この外部キーをstatesandの主キーにすることもできます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)
);

2つの列を使用します。 2つのターゲットテーブルのいずれかを参照する1つの列の代わりに、2つの列を使用します。これらの2つの列はNULL、実際には、そのうちの1つだけが非であってはなりません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)
);

関係理論の観点から見ると、ポリモーフィックアソシエーションは、実質的に2つの意味を持つ列であるため、First Normal Formに違反しpopular_place_idています。人ageとその人phone_numberを1つの列に格納しないでください。同じ理由で、両方state_idcountry_id1つの列に格納しないでください。これら2つの属性に互換性のあるデータ型があるという事実は偶然です。彼らはまだ別の論理エンティティを示しています。

列の意味は、外部キーが参照するテーブルを指定する追加の列に依存するため、多態的な関連付けも第3正規形に違反します。第3正規形では、テーブルの属性は、そのテーブルの主キーのみに依存する必要があります。


@SavasVedovaからの再コメント:

テーブルの定義やクエリの例を見ずに説明に従うかわかりませんがFilters、中央のProductsテーブルを参照する外部キーを含む複数のテーブルがあるように思えます。

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

結合したいタイプがわかっていれば、製品を特定のタイプのフィルターに結合するのは簡単です。

SELECT * FROM Products
INNER JOIN FiltersType2 USING (product_id)

フィルタータイプを動的にしたい場合は、SQLクエリを作成するアプリケーションコードを記述する必要があります。SQLでは、クエリを作成するときにテーブルを指定して修正する必要があります。の個々の行にある値に基づいて、結合されたテーブルを動的に選択することはできませんProducts

他の唯一のオプションは、外部結合を使用してすべてのフィルターテーブルに結合することです。一致するproduct_idがないものは、nullの単一行として返されます。ただし、結合されたすべてのテーブルをハードコードする必要があり、新しいフィルターテーブルを追加する場合は、コードを更新する必要があります。

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

すべてのフィルターテーブルに結合するもう1つの方法は、連続的に実行することです。

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

ただし、この形式でも、すべてのテーブルへの参照を書き込む必要があります。それを回避する方法はありません。


どちらをお勧めしますか?データベースの設計の最中ですが、迷っています。基本的にはフィルターを製品に関連付ける必要があり、フィルターの値はさまざまなテーブルに入力されます。しかし問題は、フィルターが管理者によって生成されるため、フィルターの種類によってはデータが異なり、joinターゲットも変化するということです。複雑すぎますか、それとも何ですか?助けて!
Savas Vedova

+1素晴らしいソリューションをありがとう。1つ目と2つ目の解決策に関する1つの質問は次のとおりです。複数のテーブルがそのメタテーブルの同じ主キーを参照できるという事実に正規化違反はありますか?私はあなたがこれをロジックで解決できることを知っていますが、私が何かを見落とさない限り、データベースがそれを強制する方法を私は見ていません。
Rob

5
「CONSTRAINT CHECK」のアプローチが本当に好きです。ただし、「OR」を「XOR」に変更すれば改善できます。そうすることで、セットの1つの列だけがNULLでないことが保証されます
alex_b

1
@alex_b、はい、それは良いことですが、論理XORは標準SQLではなく、すべてのSQLブランドでサポートされているわけではありません。MySQLにはありますが、PostgreSQLにはありません。Oracleにはありますが、Microsoftには2016年までありません。
ビルカーウィン2015

1
「これらの2つの列はNULLである可能性があります。実際、それらの1つだけが非NULLであるべきです」-これ 1NFに違反します!
2016年

10

これは世界で最もエレガントなソリューションではありませんが、具体的なテーブル継承を使用してこれを機能させることができます。

概念的には、3つのタイプの場所が継承する「人気のエリアになることができるもの」のクラスの概念を提案しています。あなたは、例えば、というテーブル、としてこれを表すことができplaces、各列は、の行と1対1の関係を有しているregionscountriesまたはstates。(地域、国、または州間で共有されている属性は、存在する場合、このプレイステーブルにプッシュできます。)popular_place_id次に、場所テーブル内の行への外部キー参照は、地域、国につながります。 、または状態。

連想のタイプを説明するために2番目の列で提案するソリューションは、Railsが多態的な連想を処理する方法ですが、私は一般にそれが好きではありません。ビルは、多態的な関連があなたの友達ではない理由を非常に詳細に説明しています。


1
別名「スーパータイプ-サブタイプパターン」
ErikE

また、この記事は、コンセプトduhallowgreygeek.com/polymorphic-association-bad-sql-smellをよく説明しています
Marco Staffoli

5

( place_type, place_id )認識された正規形違反を解決するために複合キーを使用する、Bill Karwinの「スーパーテーブル」アプローチに対する修正は次のとおりです。

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

この設計では、すべての行について、または行(または両方ではない)placesが存在することを保証できません。これは、SQLの外部キーの制限です。完全なSQL-92標準に準拠したDBMSでは、遅延可能なテーブル間制約を定義して同じことを実現できますが、これは扱いにくく、トランザクションを伴い、そのようなDBMSはまだ市場に出ていません。statescountries


0

このスレッドが古いことに気づきましたが、これを見て解決策が思いついたので、そこに捨てようと思いました。

地域、国、州は、階層内に存在する地理的な場所です。

3つの行(Region、Country、State)を入力するgeogical_location_typeというドメインテーブルを作成することで、問題を完全に回避できます。

次に、3つのロケーションテーブルの代わりに、geographical_location_type_idの外部キーを持つ単一のgeographical_locationテーブルを作成します(インスタンスが地域、国、または州であるかどうかがわかります)。

このテーブルを自己参照にして、StateインスタンスがfKeyをその親Countryインスタンスに保持し、さらにそのfKeyをその親Regionインスタンスに保持するようにして、階層をモデル化します。リージョンインスタンスは、そのfKeyでNULLを保持します。これは、3つのテーブルで行ったのと同じです(1と地域と国の間、および国と州の間の多くの関係があります)、現在はすべて1つのテーブルになっています。

Popular_user_locationテーブルは、ユーザーとgeorgraphical_locationの間のスコープ解決テーブルになります(そのため、多くのユーザーが多くの場所を好きになる可能性があります)。

Soooo…

ここに画像の説明を入力してください

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

ターゲットDBが何かはわかりませんでした。上記はMS SQL Serverです。


0

さて、私は2つのテーブルを持っています:

a)曲番号b)曲のタイトル....

  1. プレイリストa)プレイリスト番号b)プレイリストタイトル...

そして私は3番目を持っています

  1. songs_to_playlist_relation

問題は、一部の種類のプレイリストが他のプレイリストにリンクしていることです。しかし、mysqlでは、2つのテーブルに関連付けられた外部キーはありません。

私の解決策:songs_to_playlist_relationに3番目の列を配置します。その列はブール値になります。1の場合は曲、それ以外の場合はプレイリストテーブルにリンクします。

そう:

  1. songs_to_playlist_relation

a)Playlist_number(int)b)Is song(boolean)c)Relative number(song number or playlist number)(int)(not foreign key to any table)

 テーブルソングを作成 
    クエリ追加"SET SQL_MODE = NO_AUTO_VALUE_ON_ZERO;" 
    クエリappend "CREATE TABLE songsNUMBERint(11)NOT NULL、SONG POSITIONint(11)NOT NULL、PLAY SONGtinyint(1)NOT NULL DEFAULT '1'、SONG TITLEvarchar(255)CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL、DESCRIPTIONvarchar(1000)CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL、ARTISTVARCHAR(255)キャラクタ・セットUTF8 COLLATE utf8_general_ci NOT NULLのDEFAULT 'Άγνωστοςのκαλλιτέχνης'、AUTHORVARCHAR(255)キャラクタ・セットUTF8 COLLATE utf8_general_ci NOT NULLのDEFAULT 'Άγνωστοςのστιχουργός'、COMPOSERVARCHAR(255)キャラクタ・セットUTF8 COLLATE utf8_general_ci NOT NULLデフォルト 'Άγνωστοςσυνθέτης'、ALBUMvarchar(255)CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'Άγνωστοάλμπουμ'、YEARint(11)NOT NULL DEFAULT '33'、RATINGint(11)NOT NULL DEFAULT '5'、IMAGEvarchar(600)CHARACTER SET utf8 GENLATE COLLATE 、SONG PATHvarchar(500)CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL、SONG REPEATint(11)NOT NULL DEFAULT '0'、VOLUMEfloat NOT NULL DEFAULT '1'、SPEEDfloat NOT NULL DEFAULT '1')ENGINE = InnoDB DEFAULT CHARSET = utf8; " 
    クエリ追記"ALTER TABLEはsongs(PRIMARY KEYを追加NUMBER)、UNIQUEキーを追加POSITIONSONG POSITION)、UNIQUEキーを追加TITLESONG TITLE)、UNIQUEキーを追加PATHSONG PATH);"
    クエリ追加"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")

それで全部です!

playlists_query = "SELECT s1。*、s3。*、s4。* FROM songs as s1 INNER JOIN` songs of playlist` as s2 ON s1.`NUMBER` = s2.`RELATIVE NUMBER` INNER JOIN `playlists` as s3 ON s3 .`NUMBER` = s2.`PLAYLIST NUMBER` INNER JOIN `playlists` as s4 ON s4.`NUMBER` = s2.`RELATIVE NUMBER` ORDER BY s3.`PLAYLIST POSITION`、` s1`.`SONG POSITION` "
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.