PDOでMySQLとの接続が切れた際に再接続する


PHP で MySQL に接続する際に PDO を使う場合、 MySQLi と比べて不便なのがDBとの接続が切れたときの再接続。

MySQLi の場合なら mysqli::ping があるのですが、PDO には無い。

なので、自前で似たようなものを作って対応する。

再接続する意味

そもそも、なんで再接続なんかが発生するのか?

これは、環境と実行する内容によるのでなんとも言えないが、下記のような構成の処理の場合には比較的頻繁に発生すると思う。

  • phpスクリプト と DB が別なサーバーにある
  • ループ回数が多い(数十万回とか)
  • ループで処理される内容の中に DB での処理とは別な時間のかかる処理がある
  • DB のタイムアウトの設定が短め

前に、MySQL server has gone away の時は wait_timeout でも書いたように、timeout 系のパラメーターが短めに設定されている場合、処理の中で時間のかかる処理、例えば別な WEB API を叩くとか、どっかからファイルをダウンロードしてくるとか、その手の処理があると確実に発生します。

もちろん、回避策は再接続以外にもあります。

代表的なのは、(意図してかどうかは別として)クエリ実行のたびに DB 接続を初期化する(プリペアドステートメントも毎回 prepare してたりする)タイプのソースで、この手のソースはDB接続が切れても全く問題ありません。

あとは、処理件数が少ない場合には、バッチ処理自体を再実行しても良いかもですね。

ただ、再接続できるとコネクションが切れたとしても処理を止める必要がない。

数百万レコードの突合など、比較的処理時間が長い処理を定期的に実行するような場合には、接続が切れたとしてもエラーで止まらないほうが楽だったりします。

というわけで、 mysqli::ping 的な処理を PDO で実現する処理を自前で作ります。

ping の要件

基本的に、 mysqli::ping と同じ。

サーバーとの接続をチェックし、もし切断されている場合は再接続を試みる
https://www.php.net/manual/ja/mysqli.ping.php

元にしたソース

似たような需要は必ずあるはずなので、探してみたら
https://terenceyim.wordpress.com/2009/01/09/adding-ping-function-to-pdo/
に、理想のソースがあったので、それを自分の処理に合うように改良しました。

改良のポイント

  • 再接続を行ったかどうかのステータスを取得可能にした
  • 再接続を試みる条件を server has gone away に限定
  • エラー処理を入れた

私はプリペアドステートメントを使うことが多いので、単純に再接続するだけだとオブジェクトが無くなってエラーになってしまうので、再接続時にプリペアドステートメントを作り直す処理を入れたかった、というところです。

PDO に ping 追加したクラス

<?php
class RPDO {
    private $pdo;
    private $params;
    public $reconnect;

    public function __construct() 
    {
        $this->params = func_get_args();
        $this->init();
    }

    public function __call($name, array $args) 
    {
        return call_user_func_array(array($this->pdo, $name), $args);
    }

    public function ping() 
    {
        $this->reconnect    = False;

        try {
            $this->pdo->query('SELECT 1');

            $errorInfo = $this->pdo->errorInfo();

            if (!empty($errorInfo[1])) {
                if ($errorInfo[0] == 'HY000' && stristr($errorInfo[2], 'server has gone away') ) {
                    $this->reconnect    = TRUE;
                } else {
                    throw new Exception("RPDO::pingでエラーが発生しました。" . print_r($errorInfo, TRUE));
                }
            }
        } catch (PDOException $e) {
            if ($e->getCode() == 'HY000' && stristr($e->getMessage(), 'server has gone away') ) {
                $this->reconnect    = TRUE;
            } else {
                throw $e;
            }
        }

        if ($this->reconnect) {
            $this->init();
        }

        return true;
    }

    private function init() 
    {
        $class = new ReflectionClass('PDO');
        $this->pdo = $class->newInstanceArgs($this->params);
    }
}

このソースでは再接続の対象を 2006 MySQL server has gone away に限定しているが、他にも再接続で回避可能なエラーが発生した場合にはそれらも含めて処理するように改良する余地はある。

利用方法

RPDO::ping を実行して接続が確立されていることを確認してからクエリを実行する。

例えば PDO::prepare で作成した PDOStatement が必要な場合には、単純に再接続しただけではエラーになる。

下記のように ping を実行し、$reconnect == TRUE ならオブジェクトを再作成するような処理を入れておくと便利。

<?php
$this->pdo->ping();

if ($this->pdo->reconnect) {
    // ここで PDO::prepare で PDOStatementオブジェクトを作成する
}

このあたりの処理は実際にはメソッド化した方が楽。

という感じ。

当然、トランザクション内では利用できないので、その辺りは考慮して実装する必要があります。