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

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

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

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

PDO

PDO(PHP Data Objects)はPHPのデータベース抽象化レイヤーです。

PHP

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

Q&A

解決済

3回答

4389閲覧

PDO::ATTR_PERSISTENTでbeginTransactionがエラーになる理由を知りたい

dameo

総合スコア943

MySQL

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

PDO

PDO(PHP Data Objects)はPHPのデータベース抽象化レイヤーです。

PHP

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

1グッド

0クリップ

投稿2021/05/30 21:42

編集2021/05/31 05:59

前提・実現したいこと

PHPでPDOを使ってMySQLにアクセスしています。複数のPDOオブジェクトを作り、それぞれbeginTransaction()しているのですが、PDO::ATTR_PERSISTENTtrueにしていると、エラーになってしまいます。PDO::ATTR_PERSISTENTは別のリクエストで使った接続をコネクションプールに返して再利用するのかと思っていたのですが、他のPDOオブジェクトで使用中の接続にも影響が出ているということなのか、といった疑問が生まれよく分かりません。

エラーになる理由を教えて下さい。

発生している問題・エラーメッセージ

There is already an active transaction #0 /var/www/html/hoge.php(14): PDO->beginTransaction() #1 /var/www/html/hoge.php(29): PdoTest->beginTransaction() #2 /var/www/html/hoge.php(45): test() #3 {main}

該当のソースコード

PHP

1<?php 2class PdoTest { 3 public function __construct($persistent = true) { 4 $this->pdo = new PDO( 5 "mysql:host=db;dbname=exampledb", 6 "exampleuser", 7 "examplepass", 8 array( 9 PDO::ATTR_PERSISTENT => $persistent, 10 )); 11 $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 12 } 13 public function beginTransaction() { 14 $this->pdo->beginTransaction(); 15 } 16 public function commit() { 17 $this->pdo->commit(); 18 } 19 function __destruct() { 20 $this->pdo = null; 21 } 22} 23function test($persistent = true) { 24 echo "Persistent: ".($persistent ? "true" : "false")."\n"; 25 try { 26 $test1 = new PdoTest($persistent); 27 $test2 = new PdoTest($persistent); 28 $test1->beginTransaction(); 29 $test2->beginTransaction(); 30 $test1->commit(); 31 $test2->commit(); 32 echo "->Success\n"; 33 } 34 catch(Exception $e) { 35 echo "->Failed\n"; 36 echo $e->getMessage()."\n".$e->getTraceAsString()."\n"; 37 } 38 finally { 39 $test1 = null; 40 $test1 = null; 41 } 42} 43echo "<pre>"; 44test(false); 45test(); 46?>

試したこと

MySQLのprocesslistを見てみました。

bash

1$ docker-compose exec db bash 2root@ecff0829b2de:/# mysql -D exampledb -u exampleuser -p 3Enter password: 4Welcome to the MySQL monitor. Commands end with ; or \g. 5Your MySQL connection id is 21 6Server version: 8.0.25 MySQL Community Server - GPL 7 8Copyright (c) 2000, 2021, Oracle and/or its affiliates. 9 10Oracle is a registered trademark of Oracle Corporation and/or its 11affiliates. Other names may be trademarks of their respective 12owners. 13 14Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 15 16mysql> show full processlist; 17+----+-------------+------------------+-----------+---------+------+-------+-----------------------+ 18| Id | User | Host | db | Command | Time | State | Info | 19+----+-------------+------------------+-----------+---------+------+-------+-----------------------+ 20| 8 | exampleuser | 172.25.0.3:58734 | exampledb | Sleep | 1553 | | NULL | 21| 9 | exampleuser | 172.25.0.3:58756 | exampledb | Sleep | 3192 | | NULL | 22| 10 | exampleuser | 172.25.0.3:58760 | exampledb | Sleep | 3071 | | NULL | 23| 13 | exampleuser | 172.25.0.3:58842 | exampledb | Sleep | 1864 | | NULL | 24| 16 | exampleuser | 172.25.0.3:58848 | exampledb | Sleep | 1783 | | NULL | 25| 21 | exampleuser | localhost | exampledb | Query | 0 | init | show full processlist | 26+----+-------------+------------------+-----------+---------+------+-------+-----------------------+ 276 rows in set (0.00 sec) 28 29mysql> show full processlist; ←この直前でphpを実行している 30+----+-------------+------------------+-----------+---------+------+-------+-----------------------+ 31| Id | User | Host | db | Command | Time | State | Info | 32+----+-------------+------------------+-----------+---------+------+-------+-----------------------+ 33| 8 | exampleuser | 172.25.0.3:58734 | exampledb | Sleep | 1625 | | NULL | 34| 9 | exampleuser | 172.25.0.3:58756 | exampledb | Sleep | 3264 | | NULL | 35| 10 | exampleuser | 172.25.0.3:58760 | exampledb | Sleep | 3143 | | NULL | 36| 13 | exampleuser | 172.25.0.3:58842 | exampledb | Sleep | 1936 | | NULL | 37| 16 | exampleuser | 172.25.0.3:58848 | exampledb | Sleep | 1855 | | NULL | 38| 21 | exampleuser | localhost | exampledb | Query | 0 | init | show full processlist | 39| 24 | exampleuser | 172.25.0.3:58902 | exampledb | Sleep | 6 | | NULL | 40+----+-------------+------------------+-----------+---------+------+-------+-----------------------+ 417 rows in set (0.00 sec) 42 43mysql>

1つしか接続が出来てないように見えます。ただしPHPにsleepなどを入れて、ATTR_PERSISTENT=falseの接続を見ると新規のポートが開いて、終了と同時に消えているようでした(ログは割愛。ATTR_PERSISTENT=falseでは$test1, $test2には接続が1つずつ出来ており、nullセットと同時に消えていました。)

追記

調べているうちに、以下の記事に当たりました。

MySQL Connection Pooling と Persistent Connections はチョット違うという話 - mita2 database life

私がConnection PoolingとPersistent Connectionsを混同していたのは間違いなく(Connection Poolingのみが正しいと思っていた)、PDOではPersistent Connectionsを実装したから、同じワーカーには同じConnectionが割り当てられたのだろうという仮説に辿り着きました。ちなみにこの記事だけでなく、Connection PoolingとPersistent Connectionsを比較する記事は(主に英語で)沢山見つかり、1対1対応については解釈にブレがあるものの、おおよそ似たような内容の記事でした。

仮説が正しければ理由としては十分なので、当面「同じワーカーには同じConnectionが割り当てられる」仮説を処理系コード上で確認する作業を続けます。

追記2

PHP: 永続的なデータベース接続 - 手動

公式にもそれっぽい内容がPDOではありませんが見つかりました。

永続的な接続は、スクリプトの実行が終了しても閉じないリンクです。持続的接続が要求されると、PHP は同一の持続的接続 (以前から開いていたもの) がすでに存在するかどうかをチェックします。存在する場合は、それを使用します。存在しない場合は、リンクを作成します。「同一の」接続とは、同じユーザー名とパスワード (該当する場合) を使用して、同じホストに対して開かれた接続です。

動作中の接続に対する、具体的な動きが明確ではありませんが、内容的には現象と一致する説明です。

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

以下の環境(docker-compose)で確認しています。

yaml

1version: "3" 2services: 3 web: 4 build: . 5 ports: 6 - 80:80 7 volumes: 8 - ./html:/var/www/html 9 - ./php.ini:/usr/local/etc/php/php.ini 10 11 db: 12 image: mysql 13 environment: 14 MYSQL_DATABASE: exampledb 15 MYSQL_USER: exampleuser 16 MYSQL_PASSWORD: examplepass 17 MYSQL_RANDOM_ROOT_PASSWORD: '1' 18 volumes: 19 - ./db:/var/lib/mysql

Dockerfile

1FROM php:apache 2RUN docker-php-ext-install pdo_mysql 3RUN pecl install xdebug \ 4 && docker-php-ext-enable xdebug

php.ini

ini

1[xdebug] 2xdebug.mode=debug 3xdebug.start_with_request=yes 4; IDEが動いてるところのIP 5xdebug.client_host= 6xdebug.client_port=9003 7xdebug.log=/tmp/xdebug.log 8; ビルド時に出たもの 9zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20200930/xdebug.so

vscodeのデバッグ設定

json

1{ 2 "version": "0.2.0", 3 "configurations": [ 4 { 5 "name": "Listen for Xdebug", 6 "type": "php", 7 "request": "launch", 8 "port": 9003, 9 "pathMappings": { 10 "/var/www/html":"${workspaceFolder}/html" 11 }, 12 } 13 ] 14}
rana_kualu👍を押しています

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

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

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

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

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

guest

回答3

0

自己解決

PDO::ATTR_PERSISTENTは別のリクエストで使った接続をコネクションプールに返して再利用するのかと思っていたのですが、他のPDOオブジェクトで使用中の接続にも影響が出ているということなのか、といった疑問が生まれ

これはConnection Poolingではなく、Persistent Connectionsだから。質問の「試したこと」の「追記1,2」にあるとおりだが、また非公式なドキュメントを追記しておく。

MySQL persistent connections in PHP - Valinv

現状のPHP処理系の実装調査結果

現状のPHP処理系が実際にどう処理しているかを確認した。

php-src/pdo_dbh.c at 01b3fc03c30c6cb85038250bb5640be3a09c6a32 · php/php-src · GitHub

このコードは2021年5月31日現在のmasterブランチのリビジョン。

ハイライトしている箇所がPDOのコンストラクタで、PDO::ATTR_PERSISTENTが文字列でないケース(今回は真偽値)では、その値がtrueのとき"PDO:DBH:DSN=mysql:host=db;dbname=exampledb:exampleuser:examplepass"をキーにpersistent_listというハッシュテーブルを走査し、PDOオブジェクトを探している。

またその少し後ろの

[php-src/pdo_dbh.c at 01b3fc03c30c6cb85038250bb5640be3a09c6a32 · php/php-src · GitHub]
(https://github.com/php/php-src/blob/01b3fc03c30c6cb85038250bb5640be3a09c6a32/ext/pdo/pdo_dbh.c#L390-L398)

では、PDO::ATTR_PERSISTENTがtrueのときpersistent_listというハッシュテーブルに値を入れている。この処理の直前のdriver->db_handle_factory()ではコネクション相当のオブジェクトが必要に応じて完成し、本体のdbhに設定されている。

結論

以上から、PERSISTENT=trueな同一DSN:user:passのPDOは、エラーで使えなくなってない限り同じdbh(接続)が使われる実装に見える。

→仮説は正しいということになる。

回答

Connection PoolingではなくPersitent Connectionsなので、ワーカーが同じなら同じDBには同じ接続が使用される。そのため、あるPDOオブジェクトでトランザクションが開始されれば、同じ接続を使う他のPDOでトランザクションを開始するとエラーになる。

投稿2021/05/31 08:22

dameo

総合スコア943

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

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

dameo

2021/05/31 11:46

テストから見る手がありましたね。盲点でした。ありがとうございます。
guest

0

単純にトランザクション重複がNGだからでは?

PDO::beginTransaction

エラー / 例外
トランザクションが既に開始されている場合や、ドライバがトランザクションに対応していない場合に PDOException をスローします。

投稿2021/05/30 21:49

編集2021/05/30 21:50
m.ts10806

総合スコア80850

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

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

dameo

2021/05/30 21:52

同じPDOオブジェクトに対してという意味だと思っていたのですが、、、違うのでしょうか?
m.ts10806

2021/05/30 22:55

オブジェクトが保持してるのはDBへのコネクションです。 接続先が同じならコミットまたはロールバックするまで解放されないと考えるのが自然です。
dameo

2021/05/30 23:23

異なるPDOオブジェクトが使用する、コネクションが同じであるということなのですか? PDOではATTR_PERSISTENT=trueにすると、接続先が同じだと全て同じコネクションになると言っていますか?それはPHP(PDO)仕様で意図したとおりの挙動ということなのですか? 全く自然であるとは思えませんが。。。 例えば、DB内のあるデータを制御するクラスAと別のDB内のあるデータを制御するクラスBがあるとします。スレッドなどで並列に動作させる必要はないのですが処理的には平行で動作させたく、それらにトランザクションがそれぞれ必要だとすると、できなくなってしまいます。また単純に一律クラスのコンストラクタでPDOを作ると、接続を全部共有することになってしまい、トランザクションの開始タイミングをかなりシビアに設計しないといけなくなります。 PHPでは必要ないという判断なのだとしたら、どういう経緯や理由なのでしょうか? またご存知であれば(推測でなければ)事実を元にした根拠の提示をお願いします。
m.ts10806

2021/05/30 23:36

コネクションが同じとは書いてませんし、何が不思議かわかりませんが、出ているメッセージが事実では? >There is already an active transaction このことからオブジェクトが別でも接続先が同じで閉じられてなければ同時にトランザクションは貼れないという事実となり、マニュアルにある「トランザクションが既に開始されている場合」という仕様通りの挙動になっています。 解放されないままトランザクションは得られないですよ。解放されるまでロックされてると理解してください。(それにDB関係にシビアな設計が必要なのは当たり前です) 質問者さんこそ「思っていた」と自身の解釈のみで根拠がありません。
dameo

2021/05/30 23:58

えーっと、伝わっていないようですね。ATTR_PERSISTENT=falseなら接続は2つできるのです。 ATTR_PERSISTENT=trueで使用済みのコネクションを破棄せずにコネクションプールに返し、取ってくるだけなら、使用中のものを他に渡す必要はないのではないか?と言ってるのですよ。私の知る限りですが、他の言語の実装はそうなっています。 そうしていないからには、何かしらの理由があるはずと言っています。(原理的に不可能というわけではありませんが)同じ物理的な接続でトランザクションを複数開始できないデータベースは多いです。そのこととは私も知っています。そういう話ではなく、その前提の上で持続的接続をm.ts10806さんのおっしゃる仕様にするのは窮屈に過ぎるのです。理由については前回のコメントで事細かにしましたが全然伝わってないので割愛します... 残念ですが、私にとって付加される価値のある情報が何もないので、低評価にしておきます。
m.ts10806

2021/05/31 00:06

低評価されるのは結構なのですけど、自身の自信の根拠を示されないことには単に駄々こねてるのと変わりません。今のところすべて自己解釈のみです。 せめて公式ドキュメントくらい引っ張ってこれませんか? エラーの理由はすでに述べています。 どこまで突っ込みたいのか分かりませんが、自身の懸念を確認するようなミニマムコードは組んで確かめられましたか?
m.ts10806

2021/05/31 00:11

私はプログラムを思ったとおりには動かせないので、プログラムの仕様に則り書くことしかできません。 PHPやDBの仕様が気に入らない窮屈だとおっしゃるのでしたら、極論、言語もDBも自身のやりたい設計を体現したものに変更するか、自分で作るしかないのでは? 探求したいのでしたら、PHPの元ソースはGithubに公開されているのでそちらを追うのが最も確実です。 ネット上で他人の解釈を聞くより100%の情報源です。
dameo

2021/05/31 00:17

そもそも現象を説明するためだけの最低限のコードが上ですよ。ほぼ空でしょ? 公式ドキュメントはあなたが提示したものも含め読んでいます。もし載せるならコレです。 https://www.php.net/manual/ja/pdo.connections.php ここに書いてない理由を聞いているのですよ。自分より分かってなくて伝わらない人に説明したいのではなく、質問したいのです。とりあえず要件を満たしていない回答をしていつまでもコメントしないでもらえますか?
退会済みユーザー

退会済みユーザー

2021/05/31 01:41

PHP開発公式側に問うならともかく、一般の開発者はドキュメントから察してミニマルなテストコードを書いて挙動を把握して使うしかないわけで。 ところで、PDO::ATTR_PERSISTENT => true のときって、トランザクションもコミット/ロールバックせずにphpの処理を抜けたら、そのトランザクションってどうなっちゃうんですかね。 書いたコードにデバッグ情報を仕込んで、トランザクション周りがどうなっているか観測できませんか?
dameo

2021/05/31 02:28

今回のように調べた上で経緯や理由などを確認するために(もしくは勘違いや不具合でないことを確認するために)、有識者に問うのが普通です。ただココには有識者がいないようですね。それだけです。 PHPの処理系の開発手順が不明で、仕様がどう決まってるのかよく分かりませんが、とりあえずCのコードから現在追っているところです。PHP自体には何の興味もないのですが...(調べた経緯は https://teratail.com/questions/340982 なので) 私が説明するのもなんですが、自動コミットを切ってトランザクションにしてるのに勝手にコミットするアホな実装はありえないので、ロールバックです。そうなってなければ、あなたがどうかは分かりませんが、多くの人はバグだと思うと思いますよ。いろいろな資料を読むと、過去にはそういう状態になって問題を起こし、ATTR_PERSISTENT自体を使用しない選択をした人も多かったようです。 実際私が使うにしても、今の挙動がバグでないのなら、トランザクションの開始を実際のDMLまで遅らせるラッパーフレームを作るところから始めるでしょうね。
退会済みユーザー

退会済みユーザー

2021/05/31 02:46

《有識者に問うのが普通》という割には、ここではなく本来もっとふさわしい土俵(php専門に議論してるメーリングリストなど)があるはずなので、様々な言語や仕組みを扱うteartailへの過剰な期待を抱いたところでどうしようもないかと。(あくまで個人の独断と偏見ですが。)
dameo

2021/05/31 03:01

仮に回答が付かなかったとしても、有識者が回答すべき質問が増えれば、teratailも少しはマシになると思いますよ。ただPHPやってる人(普通はそういう人を有識者と呼ぶんだけど...)なら別に誰でも知っているべき簡単な話だと思っています。特に昔からWebで発展したphpはDBと共に歩んできたはずなので。。。 MLは今まともに活動してるところあるんでしょうかね。よく知りません。slackとかdiscordならあるかもしれませんが、英語かもしれませんね。
rana_kualu

2021/05/31 11:33

日本には無いんじゃないかなあ。
guest

0

少し前のお話しなので、今更かもしれませんが、
複数のPDOオブジェクトでそれぞれ異なる接続を使いながら永続接続を利用したい
というのがそもそもやりたいことだと思いましたので、
PDO::ATTR_PERSISTENT に数値でもboolでもない文字列を設定すればよいのでは
ないでしょうか?
設定された文字列が同じもの同士が1つの接続になると思いますので、
例えば「該当のソースコード」にあるpdoTestのインスタンスを作成する際に
$test1 = new PdoTest('con1');
$test2 = new PdoTest('con2');

のように異なる文字列がPDO::ATTR_PERSISTENTに設定されれば
別接続として管理されると思うのですが。

投稿2021/09/25 13:01

編集2021/09/25 13:59
beadv

総合スコア144

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

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

dameo

2021/09/25 13:18

ちょっともうあまり覚えていませんが、やりたいことではなくて、trueにしたときになぜエラーになるのか理由を聞いている質問なので、無関係な回答ですね。
dameo

2021/09/25 13:27

あと、コンストラクタの書き方が流石にまずいので質問コードを見て、正しく直しておいてください。
beadv

2021/09/25 14:01

無関係でしたか・・・失礼しました。 コンストラクタの件は、元のソースのクラスを生成する際のサンプルとして記載したのですが 前提が記載されていないのでわかりづらいですね。 例えば「該当のソースコード」にあるpdoTestのインスタンスを作成する際に の1文を追加しました。ありがとうございます。
dameo

2021/09/25 14:08

いやあの例えば↓な感じでお願いします。 $test1 = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => 'con1')); $test2 = new PDO($dsn, $user, $pass, array(PDO::ATTR_PERSISTENT => 'con2'));
dameo

2021/09/26 01:18

すみませんが覚えていられないので、修正されるまでの間だけ低評価させてもらいました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問