Friction River Software

  • お問い合わせ

CakePHP5入門【WebAPI編⑫】データベース操作

A子

ねぇ、管理ページ上での話なんだけどさ

データベースの「バックアップ」や「リストア」を手動で行えるようにしたいんだよね

C菜

バックアップ」については、クーロンを使って自動的に行えば良いんじゃないでしょうか~?

まぁ、「リストア」については、それなりの知識(コマンドの知識)が無いとできませんけど~

A子

そう、そこなのよ

誰でもブラウザ上で「リストア」できれば便利じゃない?
(「バックアップ」は、ついでだけどね)

B美

あー、管理ページを階層化して、さらに複数のページを作るの?
(面倒くさい…)

C菜

トップページでやったようにタブ切り替えにしたらどうでしょう~?

A子

お、それ良いね

んじゃ、「Excelインポート」「データベースバックアップ」「データベースリストア」の三つのタブを作ろう

C菜

ちゃちゃっと作ってみました~

こんな感じでいかがでしょうか~?

B美

さすがはC菜ね
(HTMLとCSSの知識については、私よりも上じゃないかしら…)

C菜

問題はここからです~

バックアップ」はボタンだけあれば良いんですよね~?
リストア」については、(「Excelインポート」と同様に)ファイル選択のフォームを作る形ですか~?

B美

そうね

バックアップ」のほうはフォーム不要よ
(リンクをボタンとして表示するだけで良いかな)

リストア」についてもC菜の考えでOKだから…

A子

よし!
それじゃ、やってみよう

まずは「バックアップ」からね

C菜

タブ切り替えの箇所は省略するとして~
ボタンについては、こんな感じで記述してみました~

<?= $this->Html->link(__('バックアップ実行'), ['action' => 'backup'], ['class' => 'button is-link', 'style' => 'margin: 20px;']) ?>

A子

ついでに「リストア」の画面も作ったよ
(「Excelインポート」の記述をコピペするだけだし…)

<?= $this->Form->create(null, ['id' => 'restore_form', 'type' => 'file', 'url' => ['action' => 'restore']]) ?>
    <?= $this->Form->file('upload2', ['id' => 'upload2', 'label' => false, 'div' => false, 'accept' => '.sql']) ?>
    <?= $this->Form->button(__('リストア実行'), ['type' => 'submit', 'id' => 'submit_btn2', 'class' => 'button is-link']) ?>
<?= $this->Form->end() ?>

C菜

src/Controller/AdminController.php」の中に「backup」メソッドと「restore」メソッドを作りますね~

B美

さて、あなたたち

データベースのバックアップコマンドって憶えてる?

A子

mysqldump」だよね?

C菜

もっと正確に言えば、こうです~

mysqldump -u root -p (データベース名) > (ファイル名)[Enter]

(【CakePHP5応用編⑩】を参照)

A子

もしかしてこれをプログラムで実行するの?

あれ?
でもこのあとパスワード入力があるよね?

B美

実はコマンドの中に「パスワード」を含めることもできるのよ

mysqldump -u root -p(パスワード) (データベース名) > (ファイル名)[Enter]

A子

それってさー
もしもパスワードが「abc123」でデータベース名が「numbersdb」、ファイル名を「numbers_backup.sql」としたとき

mysqldump -u root -p abc123 numbersdb > numbers_backup.sql

…ってことで良いの?

B美

それではダメよ

C菜

あっ、もしかしてこうですか~?

mysqldump -u root -pabc123 numbersdb > numbers_backup.sql

B美

さすがはC菜ね

それが正解よ
「-p」のあとに空白文字を入れちゃいけないの)

なお、プログラム中に記述する場合、こう書くことが多いかな
(さっきのは省略形で、こっちのほうが正式な書き方なんだけどね…ダブルクォートは省略可)

mysqldump --user="root" --password="abc123" numbersdb

あと、出力先であるファイル名はコマンド内には書かず、header関数を使って指定するの

A子

MATE端末」上で手入力するならいつものやり方(省略形)のほうが良いけど、分かりやすいのはあとのほうだね

B美

私も同意見よ

さて、PHPプログラムの中からシェルコマンドを実行するには二つの方法があるの
一つが「shell_exec」、もう一つが「exec」関数よ

C菜

どちらを使えば良いんでしょうか~?

B美

違いはこんな感じね

関数引数戻り値用途
shell_exec1個(コマンド文字列のみ)コマンドの実行結果
(末尾に必ず改行が入る)
(エラーの場合は「null」)
簡単なコマンド実行
exec1~3個
・コマンド文字列
・戻り値を格納する配列
・ステータスコードを格納する変数
(配列と変数はオプション)
コマンド文字列のみを渡した場合は実行結果
(エラーの場合は「空文字列」)
実行結果の詳細な制御
・配列には複数行の結果が一行ずつ入る
・変数にはステータスコードが入る

まぁ、基本的には「shell_exec」関数を使えば良いってこと
(簡単だからね)

A子

なるほどね

あ、「ユーザ名」「パスワード」「データベース名」なんかは定数として定義しておくの?

B美

もちろんそれでも良いんだけど、「ConnectionManager」を使えば「データベース接続情報」の取得もできるわよ
(開発環境と本番環境でデータベースのパスワードが異なる場合もあるから、こっちのほうが便利かもね)

use Cake\Datasource\ConnectionManager;

のあと

$connection = ConnectionManager::get('default');
$dbconfig = $connection->config();
$host = $dbconfig['host'];
$user = $dbconfig['username'];
$password = $dbconfig['password'];
$db = $dbconfig['database'];
$charset = $dbconfig['encoding'];
$cmd = $dump." --host=".$host." --user=".$user." --password=".$password." --default-character-set=".$charset." ".$db;

…って感じかな
(「ConnectionManager」のuse文はすでにあるけどね…「import」メソッドで使ったから)

C菜

「host」や「encoding」の指定は必要なんでしょうか~?

「encoding」って文字コードのことですよね~?

B美

--host=""」の指定って、省略すると「localhost」…つまり自分自身のことだから別に要らないかな

--default-character-set=""」も私たちの環境では不要よ
(だって、文字コードは「UTF-8」で統一してるからね)

あえてここに記載した理由は、昔は「シフトJIS」や「EUC-JP」といった文字コードを使うことが多かったからね
(現在は、ほとんどのWebアプリが「UTF-8」になってるけど…)

A子

あれ?
Windowsって「シフトJIS」じゃなかったっけ?

B美

正解!

だからWindows上で開発してる人は未だに「シフトJIS」を使っているかもしれない
(私たちはLinux上で開発してるから、デフォルトの文字コードが「UTF-8」になってるってわけ)

C菜

なるほどですねぇ

これで必要な情報は全て出そろった感じですか~?

B美

いいえ、あと一つあるわ

さっきのコードの中に「$dump」という変数があったわよね
あれって、次のコードを実行して取得しないといけないの

$dump = trim(exec("which mysqldump"));

C菜

あ~、分かりました~
mysqldump」のフルパスですね~?

でも、なぜ「shell_exec」ではなく「exec」なんですか~?
あと、「trim」って~?

B美

これはまぁ、よくある落とし穴と言うか何というか…(苦笑)

「shell_exec」の戻り値って、末尾に必ず改行コードが入っちゃうのよね
(「exec」も環境によっては入る可能性があるから、念のためにtrimで削ってるの)

まぁ、実行環境が決まっている場合は、「which」せずに決め打ちしちゃっても良いわよ(苦笑)
(本番サーバの引っ越しによって他のディストリビューションに移行した場合、「mysqldump」の場所が変わる可能性もあるからこうしているだけ)

A子

なるほどねぇ

あ、ファイル名ってどうする?
さっき例で出した「numbers_backup.sql」にする?

C菜

バージョン管理したいですね~

「年月日時分秒」をファイル名に含めるのはどうですか~?

A子

お、良いね
んじゃ、こうかな?

$dumpfile = "numbers_backup_".date("Ymd_His").".sql";

B美

ふむ、問題ないわね
そのファイル名をダウンロード時のデフォルトとするためにはheader関数内にこう書くの

header ("Content-disposition: attachment; filename=" . $dumpfile);
header ("Content-type: application/octet-stream; name=" . $dumpfile);

んで最後に、「mysqldump」の実行結果をレスポンスするってわけ

$res = shell_exec($cmd);
return $this->response->withStringBody($res);

あ、ビューファイルへの移行はしないから「$this->autoRender = false;」を忘れないようにね

C菜

まとめてみました~

//データベースのバックアップ
public function backup()
{
    //mysqldumpのフルパスを取得
    $dump = trim(exec("which mysqldump"));

    if ($dump != "") {
        //データベース接続情報を取得
        $connection = ConnectionManager::get('default');
        $dbconfig = $connection->config();
        $host = $dbconfig['host'];
        $user = $dbconfig['username'];
        $password = $dbconfig['password'];
        $db = $dbconfig['database'];
        $charset = $dbconfig['encoding'];

        //コマンド文字列の生成と実行
        $cmd = $dump." --host=".$host." --user=".$user." --password=".$password." --default-character-set=".$charset." ".$db;
        $res = shell_exec($cmd);

        if (!is_null($res)) {
            //成功
            $this->autoRender = false;

            //ファイル名
            $dumpfile = "numbers_backup_".date("Ymd_His").".sql";

            header ("Content-disposition: attachment; filename=" . $dumpfile);
            header ("Content-type: application/octet-stream; name=" . $dumpfile);

            return $this->response->withStringBody($res);
        } else {
            //失敗
            $this->Flash->error('mysqldumpの実行時にエラーが発生しました。');
            return $this->redirect(['action' => 'index']);
        }
    } else {
        $this->Flash->error('mysqldumpのフルパスを取得できませんでした。');
        return $this->redirect(['action' => 'index']);
    }
}


B美

ふむ、ざっと見た感じ、問題は無いかな

それじゃ、ブラウザから実行確認してみなさい

A子

やってみよう

・・・

うん!
ばっちり成功したよ
(ダウンロードしたファイルの中身も確認してみたけど、問題なかった)

B美

次は「リストア」ね

MATE端末」でのやり方は憶えてる?

C菜

当然です~

mysql -u root -p < (バックアップファイル名)[Enter]

ですよね~?
(【CakePHP5応用編⑩】を参照)

B美

正解!
だったらもう分かるでしょ?

A子

手順としてはこうかな?

1.アップロードされたファイルをどこかに保存
2.そのファイルを使って「mysql」コマンドでリストア
3.保存したファイルを削除

…で、どう?

B美

もちろん、それでもOKよ

でもね
ファイルの保存場所(ディレクトリ)のパーミッションとか設定しなきゃいけないし、面倒くさいのよ

なので、これを使います

$upload = $this->request->getData('upload2');

・・・

$tmpPath = $upload->getStream()->getMetadata('uri');

上記の「$tmpPath」という変数がサーバにアップロードされたファイルと考えて良いわ

C菜

あ~、それってExcelファイルのインポート時にも出てきましたね~
(【WebAPI編④】を参照)

A子

あ、ほんとだ(苦笑)
(「import」メソッドで使ってた…)

だったら、こんな感じかな

//データベースのリストア
public function restore()
{
    if ($this->request->is('post')) {
        $upload = $this->request->getData('upload2');

        //オリジナルのファイル名を取得
        $original_filename = $upload->getClientFilename();
        if ($original_filename == '') {
            $this->Flash->error('ファイルが選択されていません。');
            return $this->redirect(['action' => 'index']);
        }
        $extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION)); //拡張子

        if ($extension == 'sql') {
            //拡張子チェックOK
            $tmpPath = $upload->getStream()->getMetadata('uri');

            //mysqlのフルパスを取得
            $mysql = trim(exec("which mysql"));

            if ($mysql != "") {
                //データベース接続情報を取得
                $connection = ConnectionManager::get('default');
                $dbconfig = $connection->config();
                $host = $dbconfig['host'];
                $user = $dbconfig['username'];
                $password = $dbconfig['password'];
                $db = $dbconfig['database'];
                $charset = $dbconfig['encoding'];

                //コマンド文字列の生成と実行
                $cmd = $mysql." --host=".$host." --user=".$user." --password=".$password." --default-character-set=".$charset." ".$db." < ".$tmpPath;
                $ret = shell_exec($cmd);
                if (!is_null($ret)) {
                    //成功
                    $this->Flash->success(__('データベースの復元に成功しました。'));
                } else {
                    //失敗
                    $this->Flash->error(__('データベースの復元に失敗しました。'));
                }
            } else {
                $this->Flash->error(__('MySQLのクライアントが見つかりません。'));
            }
        } else {
            $this->Flash->error(__('バックアップファイルではない可能性があります。'));
        }

        return $this->redirect(['action' => 'index']);
    }
}

どうよ
完璧でしょ?

B美

うーん、ほとんど大丈夫なんだけど、一点だけ惜しい!

「mysql」コマンドの実行結果は「shell_exec」関数じゃ取得できないわよ

A子

いやいや、成功なら何らかの実行結果で、失敗なら「null」が返るって、さっき言ってたじゃん

B美

MATE端末」上で「mysql」コマンドを使ってリストアしたときって、何か返ってきたかしら?

C菜

あ~
何も返ってこなかったです~

つまり「null」判定になっちゃうってことですか~

B美

そういうこと
そして「mysql」コマンドの実行結果(成功か失敗か)については、以下の呼び出し方法で取得できるの

exec($cmd, $output, $ret);

この第三引数である「$ret」という変数の値が「0」だったら成功、「0以外」だったら失敗ってことよ

A子

なるほどねぇ
ならば、こうよ

//データベースのリストア
public function restore()
{
    if ($this->request->is('post')) {
        $upload = $this->request->getData('upload2');

        //オリジナルのファイル名を取得
        $original_filename = $upload->getClientFilename();
        if ($original_filename == '') {
            $this->Flash->error('ファイルが選択されていません。');
            return $this->redirect(['action' => 'index']);
        }
        $extension = mb_strtolower(pathinfo($original_filename, PATHINFO_EXTENSION)); //拡張子

        if ($extension == 'sql') {
            //拡張子チェックOK
            $tmpPath = $upload->getStream()->getMetadata('uri');

            //mysqlのフルパスを取得
            $mysql = trim(exec("which mysql"));

            if ($mysql != "") {
                //データベース接続情報を取得
                $connection = ConnectionManager::get('default');
                $dbconfig = $connection->config();
                $host = $dbconfig['host'];
                $user = $dbconfig['username'];
                $password = $dbconfig['password'];
                $db = $dbconfig['database'];
                $charset = $dbconfig['encoding'];

                //コマンド文字列の生成と実行
                $cmd = $mysql." --host=".$host." --user=".$user." --password=".$password." --default-character-set=".$charset." ".$db." < ".$tmpPath;
                exec($cmd, $output, $ret);
                if ($ret == 0) {

                    //成功
                    $this->Flash->success(__('データベースの復元に成功しました。'));
                } else {
                    //失敗
                    $this->Flash->error(__('データベースの復元に失敗しました。'));
                }
            } else {
                $this->Flash->error(__('MySQLのクライアントが見つかりません。'));
            }
        } else {
            $this->Flash->error(__('バックアップファイルではない可能性があります。'));
        }

        return $this->redirect(['action' => 'index']);
    }
}


B美

ふむ
まぁ、良いでしょう

それじゃ、実行確認してみて…

C菜

まずは「MATE端末」で「mysql」クライアントに接続するです~

A子

んじゃ、全レコードを削除して、レコード数を確認してみるね

うん、「0」になった

C菜

ブラウザ上でバックアップファイルを選択して「リストア実行」ボタンを押しますね~

・・・

お~
成功したみたいです~



A子

念のため、確認してみよう

・・・

うん、大丈夫っぽい

B美

さて、これで今回の「データベース操作」というテーマについては完了よ

案外、簡単だったでしょ?

A子

んなわけあるかい!

B美がいなかったら絶対に終わってないよ(苦笑)

C菜

ですね~(笑)