M.Y. 雑感ブログ

札幌からWebの技術などを発信しています。

Laravel のDBトランザクション機能を見てみる

Laravel 5.4 からはトランザクションに関するメソッドが trait 化されているのでとても分かりやすい。

Concerns(関心) というくくりで trait 分けしているみたいです。その trait の使い方自体も面白いですね。

\Illuminate\Database\Concerns\ManagesTransactions(v5.4.11)

マルチプルトランザクション

まず、トランザクションは複数回かけることが出来そうです。

<?php
DB::beginTransaction(); // ここで `$this->getPdo()->beginTransaction();` が走る
DB::beginTransaction(); // ここでは `SAVEPOINT trans2;` が走る
DB::commit(); // ここは何もしない
DB::commit(); // ここで `$this->getPdo()->commit()` が走る

SAVEPOINT とは MySQL のリファレンス にある通り、トランザクション中の任意の場所にセーブポイントを作成し、ロールバック時にそのセーブポイント後の行操作だけを戻す、という動作を行えるようです。

セーブポイント後の行ロックは解放されないので注意ということも書いてありますね。

マルチプルトランザクションのロールバック

マルチプルトランザクションの途中でロールバックが発生した場合はどうなるでしょうか。

<?php
DB::beginTransaction();
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "aa"
DB::update("UPDATE users SET name = 'hoge' WHERE id = 1;");
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "hoge"

DB::beginTransaction();
DB::update("UPDATE users SET name = 'foo' WHERE id = 1;");
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "foo"

DB::rollback(); // `ROLLBACK TO trans2;` が走る
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "hoge"

DB::commit(); // `COMMIT;` する
DB::select('SELECT name FROM tbl_users WHERE id = 1;')[0]->name; // "hoge"

おお、途中まで戻って、コミットしたら途中までの結果がコミットされるんですね。

便利は便利なんですが、トランザクションの定義をしっかりしないとコワイことになりそう。

<?php
DB::transaction(function () {
  DB::update("UPDATE tbl_users SET name = 'hoge' WHERE id = 1;");
  DB::transaction(function () {
    DB::update("UPDATE tbl_users SET name = 'foo' WHERE id = 1;");
    throw new Exception;
  });
});

ちなみにこれも先ほどのコードと等価です。クロージャにより自動的にBEGINとCOMMIT(またはROLLBACK)を行ってくれるので、こちらの方が安全ですね。

デッドロック時の処理

デッドロックの場合にリトライ処理をさせることが出来ます。

<?php
$retries = 3;
DB::transaction(function () {
  DB::select('SELECT * FROM users WHERE id = 1 FOR UPDATE');
}, $retries);

これでトランザクション中にデッドロックしても、ロールバックして三回までリトライしてくれます。

<?php
$retries = 3;
DB::transaction(function () {
  DB::transaction(function () {
    DB::select('SELECT * FROM users WHERE id = 1 FOR UPDATE');
    DB::update("UPDATE users SET name = 'foo' WHERE id = 1;");
  });
}, $retries);

マルチプルトランザクションの場合にデッドロックが起こった時は、一旦全トランザクションを抜けなければならないので全てロールバックしてからリトライが行われます。

Phan Daemon mode 試用してみた

etsy/phan というPHPの静的解析ツールが最近話題です。

最新の0.9.2(PHP7.0の場合は0.8.4)で、 Phan Daemon Mode という仕組みが搭載されました。

クラス依存情報を持つ必要があるため、これまでは解析に関わるvendorを含む全てのファイルを対象とした解析の実行しか行えませんでした。

これは、CIなどのプロセスにおいては有効ですが、エディタによる編集作業中にエラーを指示してくれないことになり、フィードバックが遅れるという問題があります。

  • (Linux/Unix only) Add Experimental Phan Daemon mode (PR #563 for Issue #22), which allows phan to run in the background, and accept TCP requests to analyze single files. (The implementation currently requires the pcntl extension, which does not in Windows) Server usage: path/to/phan --daemonize-tcp-port 4846 (In the root directory of the project being analyzed) Client usage: path/to/phan_client --daemonize-tcp-port 4846 -l src/file1.php [ -l src/file2.php ]

新しい仕組みでは、バックグラウンドで常に全ファイルの解析を行ったものを保持して、エディタ等でファイル変更が生じた際にそのファイルだけを解析にかけることが出来るというものです。

利用wikiはこちら

実際に実行するのは簡単で、

# プロジェクト外にphanを置く
$ git clone https://github.com/etsy/phan.git /opt/phan
$ cd /opt/phan
# PHP7.1の場合はmaster、PHP7.0の場合は0.8ブランチで
$ git checkout -b 0.8 origin/0.8
$ composer install -o --no-dev
$ cd /path/to/project
$ /opt/phan/phan --daemonize-tcp-port 4846 --quick # `/.phan/config.php` を事前に置いておくの推奨
Warning: This daemon is slower when xdebug is installedListening for Phan analysis requests at tcp://127.0.0.1:4846

# 警告の通り、xdebug付きで実行すると遅くなるそうです。

これでデーモンプロセスが立ち上がるので、後はコマンドなりなんなりで

$ /opt/phan/phan_client -l src/TargetFile.php
Phan error: UndefError: PhanUndeclaredClassMethod: Call to method get from undeclared class \Config in src/TargetFile.php on line 10
...

のように単体ファイルで高速に解析が出来ます。

…とのことでしたが、xdebugをONにしているのが悪いのか、1ファイル解析するのに1分くらいかかったりしてました。

CPUとメモリもそれなりにムシャムシャするみたいなので、まだ安定運用は出来なさそうな雰囲気です。

クライアントをエディタに反映するプラグインemacs用vimスニペット くらいしか出ていません。

今後は Microsoft Language Server Protocol に従った出力をしようぜ、などの提案がなされています。今後に期待ですね。

git alias でいつも叩くコマンドを省略する

git コマンドはよく叩きますよね。

最近はエディタやIDEにネイティブのVCSインテグレーションが搭載されるようになっていますが、中々慣れなくて結局ターミナルからポチポチコマンドを叩くことに。

そういう時、実際打つコマンドはそんなに多くありません。なのでそれらを登録してより早く表示出来るようにしましょう。

$ git config --global -e

でグローバルコンフィグを設定出来るようになります。

[alias]
    fe = fetch --prune --all

などと書くことで、 git fe とコマンドを叩くと代わりにエイリアスに登録されたコマンドが実行されます。

私が使っているエイリアスも紹介しておきましょう。

git fe

fe = fetch --prune --all

これはリモートリポジトリからの情報を取得する fetch サブコマンドの省略形です。

  • --prune を追加することで、リモートリポジトリで削除されたブランチ情報も取得します。
  • --all を追加することで、 originupstream のように複数のリモートリポジトリがある場合、全てに対して情報取得を実施します。

git flash

flash = !git fetch --prune --all && git pull --rebase

エイリアス登録時に ! から始めることによって、通常のシェルスクリプトとしてそのエイリアスを実行することが可能です。

要するに .bash_profile などに alias foo="bar..." と登録するのと変わりませんね。

これは先ほどの git fe に加えて、現在のブランチをリモートと同じ状態にするためリベースプルを行っています。

git now

now = !git fetch --prune --all && git branch -vv && git status

これは実際あまり使わないんですが、現在のローカルブランチ状態とステータスを一発で表示するコマンドです。

単純な省略形

ame = commit --amend
co = checkout
st = status
di = diff
br = branch -vv
l = log --oneline --no-merges

よく使うコマンドはまあ長いので、1~3文字のエイリアスを登録しています。

git branch -vv とverbose表示することで、最新のコミット名と、upstreamブランチがある場合はそれとの差分を表示してくれるようになり、非常に便利です。

git cu

cu = name-rev --name-only HEAD

現在のローカルブランチ名を表示します。後述するエイリアスなどで便利に利用出来ます。

git push-cu

push-cu = !git push -u origin `git cu`

現在のローカルブランチをoriginリモートリポジトリにpushします。一応 git push -u だけでもローカルブランチをpushするような設定も出来ますが(push.default)、明示した方が安全です。

git push-fcu

push-fcu = !git push -uf origin `git cu`

上のフォースプッシュモードです。

git delete-gone

delete-gone = !git branch -vv | grep ': gone]' | awk '{print $1}' | xargs git branch -D

プッシュしPRをマージした後は、該当のリモートブランチを削除するのが風習です。

しかしローカルブランチは手動で削除しなければならないのでちょっと面倒。

そこで、ブランチ一覧からupstreamで削除されたローカルブランチだけを取得し、ローカルブランチを削除するエイリアスを作りました。

(削除出来るブランチがない場合はエラーが出るんですが、まあ別に問題ないということで。)


このように、日常的に使うツールは少しでも早く、作りやすいように調整していきましょう。

一度作っても、後からフローが変わっていく場合もありますので、常にアップデートすることも大事ですね。