はじめまして、NACKと申します。
「毎月7日は罠(わな=07)の日」……と言い張ってみます。
※記念日認定されているわけではありませんので、ご注意ください。
というわけで、Laravelの開発中に、実際に罠にはまった話を、昔のゲームブック風に書いてみます。
楽しんでいただければ幸いです。「では、幸運を祈っています(1へ進む)」
「そういうお遊びは興味無いから!」という方は、直接 罠の解説まで飛んでください。
実際にはまった罠の話が列挙されています。
Laravelトラップ(ゲームブック風)
1
君は小さく、四角い、明るい部屋にいる。
扉は全部で2箇所ある。1つは東の壁、もう1つは西の壁にある。
東の扉には「Eloquentの旅」、西の扉には「envの旅」と書かれている。
東の扉を開けるなら11へ進め。
西の扉を開けるなら9へ進め。
2
冒険を楽しみ、さらに挑戦してみようと思うなら1へ進む。
もう満足したか、全ての冒険を達成したなら、Laravelの開発に挑戦すべきだろう。
楽しんでくれてありがとう。
3
値は正しく取得できた。
目の前に、次の部屋への扉が現れる。
扉を開けて、13へ進め。
4
更新日時は無事更新できた。
目の前に、次の部屋への扉が現れる。
扉を開けて、10へ進め。
5
6
データは無事更新できた(2へ)。
7
値は正しく取得できた(2へ)。
8
9
commandファイル内で、envに設定された値を取得したい。
君はconfigファイル経由で取得してもいいし(15へ)、
commandファイルで直接取得してもいい(3へ)。
$app_name = \Config::get('app.name'); // config/app.php に return ['name' => env('APP_NAME', 'Laravel')];という定義済
$app_name = env('APP_NAME', 'Laravel');
10
Eloquentを使い、テーブルの全レコードのデータを更新したい。
君はそのままupdateしてもいいし(12へ)、全件該当する条件を指定してからupdateしてもいい(6へ)。
もしくは、queryメソッドを呼び出してからupdateするのも自由だ(17へ)。
App\Flight::update(['piyo' => 1]);
App\Flight::whereRaw('1=1')->update(['piyo' => 1]);
App\Flight::query()->update(['piyo' => 1]);
11
更新日時を更新したい。
ただし、今回は「更新日時以外の更新対象項目の値が、変更前後で変わらないこともある」状況だ。
君はsave()を使ってもいいし(8へ)、update()を使ってもいい(4へ)。
もしくは、touch()を使うのも自由だ(16へ)
$flight = App\Flight::find(1);
$flight->name = 'ほげ'; // 元のnameも"ほげ"
$flight->save();
App\Flight::where('id', 1)
->update(['name' => 'ほげ']); // 元のnameも"ほげ"
$flight = App\Flight::find(1);
$flight->name = 'ほげ'; // 元のnameも"ほげ"
$flight->touch();
12
13
commandファイル内で、envに設定された値を取得したい。
(ただし、このアプリは php artisan config:cache
を使用している)。
君はconfigファイル経由で取得してもいいし(7へ)、
commandファイルで直接取得してもいい(5へ)。
$app_name = \Config::get('app.name'); // config/app.php に return ['name' => env('APP_NAME', 'Laravel')];という定義済
$app_name = env('APP_NAME', 'Laravel');
14
君は失敗した。だが、くじけることはない。君はいつだって再挑戦できるのだ。
15
値は正しく取得できた。
目の前に、次の部屋への扉が現れる。
扉を開けて、13へ進め。
16
更新日時は無事更新できた。
目の前に、次の部屋への扉が現れる。
扉を開けて、10へ進め。
※special thanks: @mikkame
17
データは無事更新できた(2へ)。
※special thanks: @mikkame
罠の解説
Eloquent / QueryBuilder
saveメソッドとupdateメソッドの違い
「(更新日時以外の)値が、元の値から変更されているかどうか」で挙動が異なります。
- 値が変更されている場合のみ更新するのがsaveメソッド
- 値の変更状況に関わらず更新するのがupdateメソッド
です。
参考: Eloquentのメソッド saveとupdateは処理が異なる
そのため、(「変更しようとしたが、結果的に同じ値だった」場合を含め)値が変更されなかった場合、saveメソッドでは更新日時が変更されません。
どうしてもsaveメソッドと同じような実装をしつつ、時間も確実に更新したい場合は、saveメソッドの"替わり"にtouchメソッドを使いましょう。データ部分は(更新が必要であれば)更新した上で、時間は確実に更新されます(special thanks: @mikkame )。ただ、touchメソッドだと、ネーミング的に「データが更新される感が薄い」のが悩ましいところです……。
なお、こちら「データが更新されていないのだから、更新日時も更新しなくて良いのでは?」と思われる方もいらっしゃると思います。
正しい見解ではあるのですが、データの差分取込バッチを作った時、この仕様のために問題が発生しました……。
発生した問題:
親子テーブルを対象とする差分取込バッチで
「親テーブルの更新日時が変わっていたら取込対象とする。
そのため、子テーブルだけデータ更新したい場合でも、親テーブルの更新日時は更新しておく」
という作りにしたつもりだった。
が、各テーブルの更新処理をsaveメソッドで行っていたため、親テーブルのデータ更新が無い場合に、更新日時が更新されず、取込対象から漏れた。
一括更新時のupdateメソッド
百聞は一見に如かず……ということで、下記のサンプルコードをご覧ください(関連コードのみ抜粋)。
// クエリログ出力準備
\DB::enableQueryLog();
// 条件指定無しで更新
App\Flight::update(['piyo' => 1]);
// クエリログ&実行速度出力
var_dump(\DB::getQueryLog());
/*
出力結果:
array(0) {
}
→SQLの実行ログが無い!
*/
// クエリログ出力準備
\DB::enableQueryLog();
// 全件当てはまる条件付きで更新
App\Flight::whereRaw('1 = 1')->update(['piyo' => 1]);
// クエリログ&実行速度出力
var_dump(\DB::getQueryLog());
/*
出力結果:
array(1) {
[0] =>
array(3) {
'query' =>
string(xx) "update `flights` set `piyo` = ?, `updated_at` = ? where 1 = 1"
'bindings' =>
array(2) {
[0] =>
int(1)
[1] =>
string(19) "2018-12-07 12:34:56"
}
'time' =>
double(439.91)
}
}
→SQLの実行ログが出てきた!
*/
というわけで、条件指定無しでupdateした時は、SQLが実行されません。
原因は、「条件が指定されているかどうか」ではなく、「Eloquent(Model)クラスから直接呼び出すupdateメソッド」と「whereメソッド使用後に呼び出すupdateメソッド」の実装が異なっているからです(当然といえば当然なのですが……)。
// whereメソッド(whereRawメソッド等も含む)を使用せず、Eloquentクラスから直接updateメソッドを呼び出すと、ここに来る。
/**
* Update the model in the database.
*
* @param array $attributes
* @param array $options
* @return bool
*/
public function update(array $attributes = [], array $options = [])
{
if (! $this->exists) {
return false;
}
return $this->fill($attributes)->save($options);
}
// whereメソッド(whereRawメソッド等も含む)使用後にupdateメソッドを呼び出すと、ここに来る。
/**
* Update a record in the database.
*
* @param array $values
* @return int
*/
public function update(array $values)
{
return $this->toBase()->update($this->addUpdatedAtColumn($values));
}
なお、私が修正した時は Illuminate\Database\Eloquent\Builder
クラスのupdateメソッドを呼び出す」ために whereRaw
メソッドを使ってしまいましたが、全件更新時の正攻法は「 query
メソッドを使う」です(special thanks: @mikkame )。
App\Flight::query()->update(['piyo' => 1]);
Environment
env値の呼出し箇所とキャッシュ
special thanks: @cflat0528
基本的に、.envファイルに設定した値は、どこからでも取得できます(ControllerやModel、Commandからも取得可能です)。
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class HogeCommand extends Command
{
protected $signature = 'hoge:fuga';
public function handle()
{
// envファイルからAPP_NAMEという変数の値が取得できればその値を、なければ"Laravel"という文字列を出力する
echo env('APP_NAME', 'Laravel') . "\n";
}
}
ただし、 php artisan config:cache
と組み合わせて使う場合は注意が必要です。
php artisan config:cache
の実行前後で、上記のコマンドの実行結果を比較してみましょう。
$ php artisan hoge:fuga # ↓envの値が取得できている
hoge-batch
$ php artisan config:cache #configの値をキャッシュすると……
Configuration cache cleared!
Configuration cached successfully!
$ php artisan hoge:fuga # ↓出力結果が変わってしまった!
Laravel
$
これは、「envファイル読み込み処理の仕様」です。
php artisan config:cache
を実行した時、APP_NAME
という変数がconfig/app.php
で以下のように設定されている場合、
<?php
'name' => env('APP_NAME', 'Laravel'),
この値を envファイル内に定義した定数値
に置き換えた「configのキャッシュファイル」が生成されます。
<?php
'name' => 'hoge-batch' // .envで指定したAPP_NAME
ここまでは良いのですが、一度 php artisan config:cache
をした後は、アプリの起動時にenvファイルが読み込まれなくなります。
/**
* Bootstrap the given application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function bootstrap(Application $app)
{
if ($app->configurationIsCached()) {
return; // ←configキャッシュがある場合、ここでリターンしてしまうため、後続のenvファイル読み込みが行われない
}
$this->checkForSpecificEnvironmentFile($app);
try {
(new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
} catch (InvalidPathException $e) {
//
}
}
そのため、php artisan config:cache
実行後は、configファイル以外に書かれているenvの値は(キャッシュファイルに保存されておらず、envファイルの読み込みも行われないので)取得できなくなってしまうのです。