質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
MySQL

MySQL(マイエスキューエル)は、TCX DataKonsultAB社などが開発するRDBMS(リレーショナルデータベースの管理システム)です。世界で最も人気の高いシステムで、オープンソースで開発されています。MySQLデータベースサーバは、高速性と信頼性があり、Linux、UNIX、Windowsなどの複数のプラットフォームで動作することができます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

Q&A

解決済

1回答

1018閲覧

INSERT IGNORE を使うタイミングと正しい記述

nikuatsu

総合スコア177

MySQL

MySQL(マイエスキューエル)は、TCX DataKonsultAB社などが開発するRDBMS(リレーショナルデータベースの管理システム)です。世界で最も人気の高いシステムで、オープンソースで開発されています。MySQLデータベースサーバは、高速性と信頼性があり、Linux、UNIX、Windowsなどの複数のプラットフォームで動作することができます。

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

0グッド

0クリップ

投稿2022/07/31 09:57

編集2022/07/31 12:21

前提

該当のソースコードのように3つのテーブルがあります。

my_fruits : フルーツテーブル
my_tags : タグテーブル
my_tag_holders : リレーションテーブル

そして my_tag_holders の主キー(34行目)によって 「フルーツとタグの同じ組み合わせはない」 という制限をしています。

発生している問題

「フルーツとタグの同じ組み合わせはない」 という制限のために INSERT IGNORE を使うつもりですが、そのタイミングと正しい記述がわかりません。

該当のソースコード

SQL

1-- フルーツテーブル 2CREATE TABLE my_fruits ( 3 `ID` int AUTO_INCREMENT, 4 `count_likes` int(100) DEFAULT 0, 5 `content` varchar(100) not null, 6 PRIMARY KEY(`ID`)); 7 8INSERT INTO my_fruits (`count_likes`, `content`) 9VALUES 10(10, 'フルーツ1'), 11(56, 'フルーツ2'), 12(36, 'フルーツ3'); 13 14-- タグテーブル 15CREATE TABLE my_tags ( 16 `ID` int AUTO_INCREMENT, 17 `tag_kind_id` int, 18 `tag_name` varchar(100), 19 PRIMARY KEY(`ID`)); 20 21INSERT INTO my_tags 22(`tag_kind_id`, `tag_name`) 23VALUES 24(1, '赤'), 25(1, '青'), 26(1, '緑'), 27(1, '黄'), 28(1, '白'); 29 30-- リレーションテーブル 31CREATE TABLE my_tag_holders ( 32 `fruits_ID` int, 33 `tags_ID` int, 34 PRIMARY KEY (`tags_ID`, `fruits_ID`), #「フルーツとタグの同じ組み合わせはない」という制限 35 CONSTRAINT fk_01 FOREIGN KEY (`fruits_ID`) REFERENCES my_fruits(`ID`), 36 CONSTRAINT fk_02 FOREIGN KEY (`tags_ID`) REFERENCES my_tags(`ID`) ); 37 38INSERT INTO my_tag_holders 39(`fruits_ID`, `tags_ID`) 40VALUES 41(1, 1),(1, 2), 42(2, 2),(2, 4),(2, 5), 43(3, 3); 44 45-- タグテーブルにユニーク制約を追加( sazi様からのご指摘 2022/07/31 20:54 を受けての追加 ) 46ALTER TABLE my_tags ADD CONSTRAINT UNIQUE u_01 (`tag_kind_id`, `tag_name`);

具体例とご質問

例えばあるユーザーが次の値をPOSTするという例で考えますと、下の方は 先述の制限 によってエラーとなります。(既にフルーツ3の緑はINSERT済み)

php

1[ 2 // これは投稿できる 3 [ 'content'=>'フルーツ3', 'tag_name'=>'赤' ], 4 // これは「フルーツとタグの同じ組み合わせはない」という制限によりエラー 5 [ 'content'=>'フルーツ3', 'tag_name'=>'緑' ] 6]

しかし当然エラーによって同じ組み合わせを防ぐのではなく「既存確認」の処理を挟むべきだろうと考えているのですが、では「既存確認」はどのように書いたらよろしいでしょうか?

INSERT IGNORE を使うのかと想像はできるのですが、テーブルが3つにまたがっているせいで書き方がわかりません。

もしテーブルが1つならば次のようにすればいいと思いますが…

SQL

1INSERT IGNORE INTO my_fruits 2(`content`, `tag_name` ) 3VALUES 4('フルーツ3', '赤'), 5('フルーツ3', '緑');

…3つの場合はどうすればいいのでしょうか?

自分で考えられる方法1

自分で考えられるのは2つの方法です。

まず3つのテーブルの INSERT より前に、SELECT で「既存確認」を入れるという方法です。

php

1// SELECT で「既存確認」を入れる 2$sth = $dbh->(" 3SELECT * 4FROM my_fruits F 5LEFT JOIN my_tag_holders TH ON TH.fruits_ID = F.ID 6LEFT JOIN my_tags T ON T.ID = TH.tags_ID 7WHERE F.content = $content 8AND T.tag_name = $tag_name 9;"); 10$count = $sth->rowCount(); 11 12// フルーツとタグの同じ組み合わせがあればdieで中断(質問のための簡易表現) 13if ( $count > 0 ) 14die( 'フルーツとタグの同じ組み合わせが既にあります' ); 15 16// my_fruits を INSERT して、$fruits_id を得る 17$dbh->query(" 18INSERT INTO my_fruits (`content`) 19VALUES ('$content') 20";); 21$fruits_id = $dbh->lastInsertId(); 22 23// my_tags を INSERT して、$tag_id を得る 24$dbh->query(" 25INSERT INTO my_tags (`tag_name`) 26VALUES ('$tag_name') 27";); 28$tag_id = $dbh->lastInsertId(); 29 30// my_tag_holders を INSERT する 31$dbh->query(" 32INSERT INTO my_tag_holders (`tags_ID`, `fruits_ID`) 33VALUES ( $tag_id, $fruits_id ) 34";);

しかしこの方法は、整合性を保つために「既存確認」の前にLOCK TABLES が必要となり負荷が懸念されます。

自分で考えられる方法2

続いて考えれる方法としては、上記のような SELECT での「既存確認」はせず、最後の my_tag_holders への INSERT の際に INSERT IGNORE を使って「既存確認」をするというものです。 最後に ROW_COUNT() して0ならロールバックし整合性を保つつもりです。

php

1// my_fruits を INSERT して、$fruits_id を得る 2$dbh->query(" 3INSERT INTO my_fruits (`content`) 4VALUES ('$content') 5";); 6$fruits_id = $dbh->lastInsertId(); 7 8// my_tags を INSERT して、$tag_id を得る 9$dbh->query(" 10INSERT INTO my_tags (`tag_name`) 11VALUES ('$tag_name') 12";); 13$tag_id = $dbh->lastInsertId(); 14 15// my_tag_holders を INSERT IGNORE を使って「既存確認」してから INSERT する 16$dbh->query(" 17INSERT IGNORE INTO my_tag_holders (`tags_ID`, `fruits_ID`) 18VALUES ( $tag_id, $fruits_id ) 19";);

しかしこの方法は、失敗を前提としているような印象で違和感を覚えます。私がテーブルが1つの場合の INSERT IGNORE しか知らないためにこの方法に至ったのですが、もし3つのテーブルについて INSERT IGNORE ができれば、my_fruits を INSERT するより以前に「既存確認」ができるので、この違和感もなくなるのではと思いました。

改めまして、INSERT IGNORE のタイミングと正しい記述についてご教示頂けませんでしょうか。

補足情報(FW/ツールのバージョンなど)

それぞれ次のバージョンです。

PHP 8.0
MySQL 5.7.2

宜しくお願い致します。

(尚インジェクションリスクがある点はスルーしてください。)

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

sazi

2022/07/31 11:21 編集

my_fruits もmy_tagsPRIMARY KEYはAUTO_INCREMENTですけど更新はあるのでしょうか? あるとしたら更新とする際の条件は何ですか?
nikuatsu

2022/07/31 11:39 編集

コメントありがとうございます。PRIMARY KEY 以外のカラムは更新の可能性があります。 ただ「条件」というのがイメ―ジできないのですが… 「状況」でよろしければ、例えば「いいねがクリックされたとき」に my_fruits.count_likes が更新されたりします。他にも例えば「やっぱり "赤" の tag_kind_id を 2 に変更しようというとき」に my_tags.tag_kind_id が更新されたりもします。 仰る「条件」がわからず「状況」のご説明になってしまいましたが、これで返信になっているでしょうか…
sazi

2022/07/31 11:54

IDを指定して更新するんですね。 では、my_tag_holdersはIDの中身に関係なくIDの値の組合せという事で良いんでしょうか? ※例えば色を変更して内容的には重複している状況が生まれるわけですけど。
nikuatsu

2022/07/31 12:18

ありがとうございます。まったく仰る通りでした。 その点は次のユニーク制約を付与することで解決できるでしょうか。 ALTER TABLE my_tags ADD CONSTRAINT UNIQUE u_01 ( tag_kind_id, tag_name ); この制約によって、tags.ID=1 の "赤" を "青" に更新しようとしても、すでに存在するので更新できない。これにより「> 色を変更して内容的には重複している状況」を生じなくさせることができる。という目論見です。
sazi

2022/07/31 13:41 編集

UNIQUE制約で、IDとその内容に変更が無い事の完全な保証にはならないですが、そこまで求めるという感じでは無いのでそれ良いかと思います。
guest

回答1

0

ベストアンサー

my_fruitsmy_tags の中間テーブルがmy_tag_holdersという関係ですから、重複が無い時のみ追加というのは、my_tag_holdersにのみ行えば良いと思います。

ただ方法として、INSERT IGNOREは「重複は処理しない」のではなく「エラーは返却せずやれることはやってあげるよ」なので、意図しない結果になるかもしれません。

ですので、select、 insert で安全に行った方が良いと思います。
なかったら insert あったら何もしない

投稿2022/07/31 13:25

sazi

総合スコア25188

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nikuatsu

2022/07/31 14:14

ありがとうございます。「自分で考えられる方法1」ということですか。その場合、整合性を保つために SELECT の前に LOCK TABLES が必要となると思います。(ロックしないと、Aさんが「フルーツ3・赤」を INSERT する際の SELECT で既存じゃなくとも、その SELECT の直後にBさんがニアミスで「フルーツ3・赤」を INSERT してしまい、「フルーツ3・赤」が重複してしまう恐れがあるかと思います。ロックすればAさんの SELECT と INSERT が完了するまで、Bさんは INSERT ができないためです。) しかし、わざわざ LOCK TABLES をかければ無駄な待ちができてしまうのではと懸念しているのですが…この懸念点はいかがでしょうか。
sazi

2022/07/31 14:41

であれば、リンク先にあるON DUPLICATE KEY UPDATEでキーを更新する方法もあります。
nikuatsu

2022/08/01 06:10 編集

ありがとうございます。すると「自分で考えられる方法2」のタイミングで、しかし INSERT IGNORE ではなく ON DUPLICATE KEY UPDATE を用いる。ということですね。 ただ先の私のコメントはしょせん素人の懸念なので、やはりsazi様のような方のご意見を伺いたいです。sazi様でしたらリンク先のどちらの方法を採用されますか? リンク先のご質問は単一テーブルについてであって LOCK TABLES による整合性は不要なので、maisumakun様は「SELECTしてから」を正統だとご回答されているのだと思います。 他方で当質問は複数テーブルについてなので、その整合性を何かで保たねばなりません。この場合 LOCK TABLES でなく ON DUPLICATE KEY UPDATE を用いる。というご意見になりますでしょうか?それとも LOCK TABLES を用いるようなケースもございますか?
sazi

2022/08/01 00:48 編集

> sazi様でしたらリンク先のどちらの方法を採用されますか? select insertの方ですね。insert into selectにしそうですけど。 > リンク先のご質問は単一テーブルについてであって LOCK TABLES による整合性は不要なので この質問もmy_tag_holdersについてのみ整合性を取ればいいという認識なのは、回答の最初で述べています。 ただ、競合回避のロックは単一の場合でも考慮は必要です。 > わざわざ LOCK TABLES をかければ無駄な待ちができてしまうのではと懸念しているのですが…この懸念点はいかがでしょうか。 この程度の処理なら待ちの時間は一瞬です。 もし、画面操作を開始して登録するまでを一連のトランザクションと考えているなら、別だし止めた方が良いと思います。 テーブル単位でロックしないと駄目なケースを設計する事は個人的にはありません。 具体的には楽観的排他を採用します。
nikuatsu

2022/08/01 06:13 編集

> この質問もmy_tag_holdersについてのみ整合性を取ればいい 失礼致しました。そうでしたね。 > テーブル単位でロックしないと駄目なケースを設計する事は個人的にはありません。 なるほど、私は「> LOCK TABLES でなく ON DUPLICATE KEY UPDATE を用いる」という二択でしたが、「LOCK TABLES は用いないけど、ON DUPLICATE KEY UPDATE でなく SELECT で整合性を確認する」のですね。 > 具体的には楽観的排他を採用します。 この用語を知らなかったため検索したのですが大変勉強になりました。私の懸念は悲観的排他しか発想になかったせいでした。楽観的排他の方向で考えを進めてみたいと思います。 丁寧なご返信をありがとうございます。助かりました。
nikuatsu

2022/08/01 08:27

すみません、やはり改めて考えてみたものの、理解が及びませんでした。 かなり調べているのですが、本質問の INSERT で整合性を保つためには楽観的排他は使えず、LOCK TABLES が必須ではないでしょうか? 楽観的排他というのは「既存レコードのロックカラムの値で更新可否を判定する」という方法であり UPDATE にのみ使えるものだと理解したためです。 つまり > ですので、select、 insert で安全に行った方が良いと思います。 と仰ったのは、「LOCK TABLES → select → insert → LOCK TABLES 解除」のことですか? もしそうでなければ、LOCK TABLES なくして、そして楽観的排他も使えない INSERT 処理において、いかにして整合性を保つのか?(my_tag_holders において既存でないことを判定するのか?排他的処理が可能なのか?)、といった辺りについてもう少しご教示頂けませんでしょうか。 もしかして、質問の CREATE TABLE の設計からおかしいですか?(ネットで調べたTOXI法というものを採用したつもりなのですが…)
sazi

2022/08/01 15:06 編集

>> ですので、select、 insert で安全に行った方が良いと思います。 >と仰ったのは、「LOCK TABLES → select → insert → LOCK TABLES 解除」のことですか? いいえ、「INSERT IGNORE」を使わずにという事です。 当然、select で存在確認を行った後の追加で重複が発生する場合はありますので、重複エラー検知しての後処理は必要ですが、テーブルのロックは不要です。 更新は行わないこの質問には関係の無い話である楽観的排他の話をコメントで答えた事で、紛らわしくしてしまったようです、すみません。
nikuatsu

2022/08/03 03:43

>すみません。 いえいえいえ、とんでもないです。 楽観的排他はその用語を知れたことで大収穫でしたので感謝しております。 > 重複エラー検知しての後処理は必要ですが、テーブルのロックは不要です。 重複エラー検知に任せるならば、そもそも select による存在確認も不要ではないでしょうか? つまり 「select → 存在しない → INSERT → ニアミスで存在したため重複エラー」という処理と 「INSERT → 存在すれば重複エラー」 という処理ならば select せずども重複エラー検知で拾えるため、その必要性がわからないという疑問です。 もしかして、エラーによる検知はなるべく避けるべきだという思想に基づいての select でしょうか?
nikuatsu

2022/08/04 13:36

もしかして、ここで仰っている select とは、select for update のことでしたか!?
sazi

2022/08/04 17:08 編集

いえ、違います。 追加が目的だからselect for updateに意味はありませんので。 > select せずども重複エラー検知で拾えるため、その必要性がわからないという疑問です。 例外処理は、実装的にも性能的にもコストが高いので、通常の処理で例外を使用する事はあまり選択しないので。
nikuatsu

2022/08/05 10:12 編集

> 例外処理は、実装的にも性能的にもコストが高いので、通常の処理で例外を使用する事はあまり選択しないので。 なるほど。select してもしなくてもエラー検知で拾えるが、select しておけばエラー検知の回数を大きく減らせるためにそのコスト減が見込めるのですね。
nikuatsu

2022/08/05 10:32 編集

> 追加が目的だからselect for updateに意味はありませんので。 私も当初そう思ったのですが、あちらの質問( https://teratail.com/questions/0avu51fjjjiuf4 )でmaisumakun様が「ネクストキーロック」という方法を教えてくださいまして、 select for update でどうやら実現できるみたいなのです。 実際に(あちらの質問の話ですが) select for update で fruits_name が重複しないことを確認できるコードがこちらです。(これはmaisumakun様のコードでなく select for update という言葉をmaisumakun様に教えて頂き私が考えたコードです。) https://onlinephp.io/c/f2af6
sazi

2022/08/05 17:43 編集

処理するテーブルに対してのテーブルロックも行ロックもデッドロックが生じやすいので、殆どのところ私は使用しませんので、的確なアドバイスは出来そうにないですね。 ロックはトランザクションをロックするように制御してます。
nikuatsu

2022/08/06 13:13

いえいえいえ、十分なアドバイスを頂戴しております。誠にありがとうございます。 ロックをあまり使用しないというのもあるのですね。私は最近知ったのでなんでもかんでもロックしている状況なので、必要性の判断能力がほしいです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問