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

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

ただいまの
回答率

87.78%

lastInsertId()によるDB登録時のidの競合について

解決済

回答 4

投稿

  • 評価
  • クリップ 0
  • VIEW 3,991

score 403

DB登録時のuser_idの競合について教えて頂きたいです。

$stmt = $pdo->prepare("INSERT INTO user_data(・・・・) VALUES (・・・・)");
$stmt->execute([・・・・]);

$user_id = $pdo->lastInsertId();

$programs1 = $_SESSION['join2']['programs1_'];
$year1 = $_SESSION['join2']['programs_year1_'];

for($i=0;$i<count($programs1);$i++) {
    $stmt = $pdo->prepare("INSERT INTO user_skill(・・・) VALUES (・・・)");
    $stmt->execute([
        $user_id,
        $programs1[$i],
        $year1[$i]
        ]);
}

プログラムの処理の流れはこうですが、

  1. user_dataへ登録(ユーザーAによる登録)
  2. user_skill を作成(ユーザーAによる登録)

例えば銀行のようにミリ秒でDBへのアクセスが頻繁なサービスがあったとします。

  1. user_dataへ登録(ユーザーAによる登録)
  2. user_dataへ登録(ユーザーBによる登録)
  3. user_skill を作成(ユーザーAによる登録)
  4. user_skillへ登録(ユーザーBによる登録)

lastInsertId()は最後の追加された行のIDを返すとのことですが、この場合、

  1. user_skill を作成(ユーザーAによる登録)
    のuser_idは
  2. user_dataへ登録(ユーザーBによる登録)
    のIDにならない理由を教えてください。

順序的には
最後に追加された行(lastInsertId()の直近に記述しているテーブル)のIDを返すはずなので
競合しないのは何故かと不思議に思っています。

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • KiyoshiMotoki

    2016/06/23 12:15

    lastInsertId メソッド「だけ」を議論することは無意味です。
    マニュアルに説明があるように、このメソッドの挙動は「構成しているドライバに依存」しているからです。
    http://php.net/manual/ja/pdo.lastinsertid.php

    お使いのDB製品の名称を追記すれば、具体的な回答を得やすくなると思います。

    キャンセル

回答 4

checkベストアンサー

+1

Aさん「俺のデータ追加して」(INSERT INTO user_data)
管理者「分かりました(Aさんのデータはuser_id1で登録…と)」
Bさん「俺のデータ追加して」(INSERT INTO user_data)
管理者「分かりました(Bさんのデータはuser_id2で登録…と)」
Aさん「ちょっとスキルのデータ追加するから最後にユーザ登録した番号教えて。俺さっき追加したばっかだから最後の番号のはずだよね」(lastInsertId)
管理者「えーと、user_dataの最後の番号は2番ですね」
Aさん「じゃあこのスキルのデータ2番で登録しといて」
管理者「了解しました」
Bさん「ちょっとスキルのデータ追加するから最後にユーザ登録した番号教えて。俺さっき追加したばっかだから最後の番号のはずだよね」(lastInsertId)
管理者「えーと、user_dataの最後の番号は2番ですね」
Bさん「じゃあこのスキルのデータ2番で登録しといて」
管理者「了解しました」

以上が「もしミリ秒レベルで処理が重なってしまって問題が起きたパターン」です。

Aさんは自分のスキルを登録したつもりがBさんの番号である2番に登録してしまいます。
Webページを閲覧する処理は排他ではなく並列で動いているため、Aさんが登録し終わるまでBさんが待ってくれているわけではありません。こういう行き違いが発生してしまうので、「自分の番号は自分の情報で参照すべき」と何度も伝えております。

Aさん「ちょっとスキルのデータ追加するから最後にユーザ登録した番号教えて。俺さっき追加したばっかだから最後の番号のはずだよね」(lastInsertId) 

ではなく

Aさん「俺の番号が知りたい。俺の登録情報は…そうだな、メールアドレスはxxxx@xxxx.co.jpだ」
管理人「そのデータの番号でしたら1番ですね」

と聞くべきなのです。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/23 11:24

    回答ありがとうございます。
    内容はとても分かりやすいです。

    回答内容からは
    $user_id = $pdo->lastInsertId();

    重複しないユニークな内容を基にidを取得するように変更する必要があるようにうかがえます。

    $sql = "SELECT id FROM user_data WHERE email =Aさんのアドレス ";
    $stmt = $pdo->query($sql);
    foreach($stmt as $row){
    $user_id = $row['id'];
    }

    $programs1 = $_SESSION['join2']['programs1_'];
    $year1 = $_SESSION['join2']['programs_year1_'];
    for($i=0;$i<count($programs1);$i++) {
    $stmt = $pdo->prepare("INSERT INTO user_skill(・・・) VALUES (・・・)");
    $stmt->execute([
    $user_id,
    $programs1[$i],
    $year1[$i]
    ]);
    }


    ↑ohashiさんの回答内容からは多分、こういう風に変える必要があるように伺えます。
    まだ勉強中なのでforeachの部分がかみ合わないと思いますが...

    email =Aさんのアドレスの、
    Aさんのアドレス、の部分を
    をどうやってそれぞれに対応できるようにしたらいいかが分からないですが...


    lastInsertId()を使うと競合しないという内容で質問しましたが、ohashiさんの回答を見ると、lastInsertId()を使うと競合してしまうと読み取れますが、他の方の回答などを見ていると競合しないと受け取れるのですが、その点どうなのでしょうか?

    キャンセル

  • 2016/06/23 11:48

    ・メールアドレスについて
    まさにそのような処理の流れに変えたほうがよいです。ログインしているユーザの情報、例えばログインに使ったユーザを一意に示す何かをセッションに残していませんか?表示名とかはセッションにすでに残しているので、それと同じ方法でセッションにメールアドレスなりなんなりを保持しておけば使えるはずです。

    ・競合について
    http://php.net/manual/ja/pdo.lastinsertid.php
    ここにlastInsertIdについて書かれています。
    「もし name パラメータにシーケンス名が指定されなかった場合、 PDO::lastInsertId() はデータベースに挿入された最後の行の行IDに相当する文字列を返します。」
    そして、これはMySQLでいうとテーブルのAUTO_INCREMENTの現在の値になります。
    shi_ueさんのいうように接続内での値であるか、ttyp03さんの言うようにトランザクション処理がある場合、ない場合どうなるかは検証したことがないのではっきり申し上げることは出来ません。しかし、lastInsertIdに「そのへんの動作についてどう動く仕様である」という明言が無いため、何かしら意図しない動作になる可能性が生まれます。
    しかし、ユーザをユニークに識別可能な情報によって得たuser_idは、意図しない動作になる可能性は(コード的なミスでもないかぎり)ありません。ですので、確実性を求めるのであればこのやり方のほうがよい、という話になります。

    キャンセル

  • 2016/06/23 13:29 編集

    なるほどです!
    ありがとうございます!

    ということはこうですね!
    $email = $_SESSION['join1']['email'];

    $sql = "SELECT id FROM user_data WHERE email = $email ";
    $stmt = $pdo->query($sql);
    foreach($stmt as $row){
    $user_id = $row['id'];
    }


    これでできました。
    ありがとうございます。
    $email = $_SESSION['join1']['email'];
    $sql = "SELECT id FROM user_data WHERE email = '$email' ";
    $stmt = $pdo->query($sql);
    $row = $stmt->fetch();
    $user_id = $row['id'];

    キャンセル

+1

lastInsertId()は大概のDBの場合、その接続中での最後のIdを返します。
すべての接続ではありません。

すべての接続の場合MAX(id)などで最大のIDをとる必要があります。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/23 11:25

    ありがとうございます。

    キャンセル

0

たぶんですが、トランザクション内で最後に追加したIDを返す仕組みだからではないでしょうか。
ここの記事なんかを読むと、トランザクション外では正しく取得できないようですので。
現状のソースだとトランザクションを意識した作りにはなってないと思います。
(自分でbeginTransactionなどをしていない)
意図的にトランザクションをしない場合、プログラムの実行から終了までがトランザクションの範囲になります。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/06/23 11:25

    ありがとうございます。
    リンク先拝見させて頂きます。

    キャンセル

0

たとえば

create table hoge(id int not null auto_increment primary key,val1 varchar(10));
insert into hoge(val1) values('a'),('b'),('c');
create table fuga(id int,val2 varchar(10));


として、

insert into hoge(val1) values('d');
select sleep(5);
insert into fuga(id,val2) values(last_insert_id(),'x');


を実行後、並行して5秒以内に

insert into hoge(val1) values('e');
insert into fuga(id,val2) values(last_insert_id(),'y');


を実行すれば
dを挿入したhogeのid=4を引き継いでfugaのid=4の値はx、
同様にeを挿入したhogeのid=5を引き継いでfugaのid=5の値はyとなり
同じセッション内での最終挿入idを引き継いでいることがわかると思います

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

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

  • ただいまの回答率 87.78%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る