Laravelでバッチを作りたい!
Laravelでバルクアップデートを実装したい!
LaravelでCronを使いたい!
こういった悩みに対して書きました!
この記事を書いている私(@Shoot58153748)は、
2020年3月現在メガベンチャーの社内スタートアップの部署で
エンジニア(1年目)をしており、
プログラミング未経験からメガベンチャーへの転職を成功させた経験・ノウハウ
Webエンジニアになってから学んだこと
をブログにまとめています。
前回までは、いいね、通報、インプレッションなどのユーザーアクションに関する機能
を作ってきました。
今回はバッチ実装です。
現在いいねとインプレッション総数をRedisで管理しているので、
Redisに格納されているデータをDBに反映するバッチを作成。
そして作成したバッチをいちいち手で叩くのはナンセンスなので、
1日に1回深夜に実行するスケジューラーの実装も合わせて実装します。
あと、数千、数十万件のデータを扱うことを想定し、
OffsetとLimit、バルクアップデートの実装も加えました。
今回学べるポイントを以下にまとめました。
是非参考にしてください!
今回のポイント
- Laravelでのバッチ作成法
- Laravelでのバルクアップデート
- Laravelでのバッチスケジューラ
目次
画面イメージ(ターミナル)
バルクアップデートとは
バルクアップデートとは、1回のクエリで複数のレコードを一括して更新する手法のことです。
何回もクエリ発行せずに済むので、
更新処理が高速に行うことができます。(万単位だと数十倍は異なるかも)
今回の場合、
万単位のツイート情報の更新を想定しているので、バルクアップデートを導入することにしました。
特にバッチ処理などは該当レコード数が多いので、
バルクアップデート上手く活用できると設計の幅が広がります。
Laravelには特にライブラリが用意されてないので
自分で準備する必要があります。
バッチ実装
処理の流れ
Redisからいいねとインプレッションのデータ(以下のデータ構造)を1000件ずつ取得します。
SortedSet型
Key
“laravel_database_shoot_tweet:like”
“laravel_database_shoot_tweet:impression”
Score
{likeCount}
{impressionCount}
Member
{tweetId}
{tweetId}
1000件取得したら、
ログ出力--update
オプションが付いていない場合、更新作業をせずに終了--like
オプション付きの場合、
いいね総数をバルクアップデート--impression
オプション付きの場合
インプレッション総数をバルクアップデート
という流れで実装していきます。
実装
今回の制作物は以下になります。
- バッチクラス
- Redisクラス
- Configファイル
- Kernel(バッチスケジューラ)
まず前準備として、artisan
コマンドでバッチクラスを自動生成します。
// app/Http/Console/CommandsディレクトリにPHPファイルを自動生成
php artisan make:command UpdateUserActionCommand
バッチクラス
ファイル名:app/Http/Console/Commands/UpdateUserAction.php
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Tweet;
use App\RedisModel;
use Illuminate\Support\Facades\DB;
class UpdateUserActionCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'user-action:update {--like} {--impression} {--update}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'update user action to tweet';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// extract all tweet count
$allTweetCount = Tweet::count();
// chunk tweet user action info
$chunkSize = 1000;
$chunkLoopCount = intval($allTweetCount / $chunkSize) + 1;
for ($chunk_i = 0; $chunk_i < $chunkLoopCount; $chunk_i++) {
$offset = $chunk_i * $chunkSize;
$limit = ($chunk_i + 1) * $chunkSize -1;
$likeTweetMap = RedisModel::zrangeTweetAction(config('tweet.USER_ACTION.LIKE'), $offset, $limit);
$impressionTweetMap = RedisModel::zrangeTweetAction(config('tweet.USER_ACTION.IMPRESSION'), $offset, $limit);
// log output
foreach ($likeTweetMap as $key => $value) {
echo "tweetId: $key, likeCount: $value \n";
\Log::info("tweetId: $key, likeCount: $value");
}
echo "----------------------------- \n";
foreach ($impressionTweetMap as $key => $value) {
echo "tweetId: $key, impressionCount: $value \n";
\Log::info("tweetId: $key, impressionCount: $value ");
}
if (!$this->option('update')) {
echo "if you add --update with --like and --impression, like and impression info will be updated \n";
return 0;
}
// update like
if ($this->option('like')) {
if (count($likeTweetMap)) {
$this->bulkUpdate(config('tweet.USER_ACTION.LIKE'), $likeTweetMap);
}
}
// update impression
if ($this->option('impression')) {
if(count($impressionTweetMap)) {
$this->bulkUpdate(config('tweet.USER_ACTION.IMPRESSION'), $impressionTweetMap);
}
}
}
}
private function bulkUpdate($actionType, $tweetMap)
{
$idList = "";
$countList = "";
foreach ($tweetMap as $tweetId => $count) {
if (is_null($count)) {
continue;
}
$idList .= "," . $tweetId;
$countList .= "," . $count;
}
$field = sprintf(config('const.BULK_UPDATE.FIELD'), $idList);
$elt = sprintf(config('const.BULK_UPDATE.ELT'), $field, $countList);
$query = sprintf(config('tweet.BULK_UPDATE_QUERY.USER_ACTION_COUNT'), $actionType, $elt, ltrim($idList, ","));
// bulk update
DB::connection()->getPdo()->query($query);
\Log::info($query);
}
}
handle()
はバッチ処理のメインとなるメソッド(自動生成される)
この部分に処理を記述していきます。
設計思想としては、
「ブロック(1000件)ごとにバルクアップデートで一括更新」
を反映したい。。
ツイート全体の件数allTweetCount
からfor文のループ回数loopCount
を算出
ループ内では、offset
とlimit
を設定し、Redisからzrange
コマンドでいいねとインプレッションデータを1000件ずつデータを取得する。
コマンドを確認で叩く時用に、--update
オプションを用意しました。
逆にオプション--update
が付けることで実際にDBを更新する権限が得られ、--like
と--impression
も合わせるとそれぞれのデータを更新できます。
もちろん、片方だけとかもできますよ。
そして、バルクアップデートのクエリー作成と処理をプライベートメソッドbulkUpdate()
に切り出す。
ちなみに実際のクエリは、以下のようになる
(例)
UPDATE tweets SET like_count = ELT(FIELD(id,259,256,258,260,261),0,1,1,1,1) WHERE id IN (259,256,25
8,260,261)
UPDATE tweets SET impression_count = ELT(FIELD(id,256,257,258,259,260,261,262,263,264,265,266,267),
1,1,1,1,1,1,1,1,1,1,1,1) WHERE id IN (256,257,258,259,260,261,262,263,264,265,266,267)
コードに埋め込んだときにスマートだったので、ELT
とFIELD
を使ってバルクアップデートのSQLを書いています。
Redisクラス
ファイル名:app/RedisModel.php
<?php
namespace App;
use Illuminate\Support\Facades\Redis;
class RedisModel
{
...
+ public static function zrangeTweetAction($actionType, $offset, $limit)
+ {
+ $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $actionType]);
+ return Redis::command('ZRANGE', [$baseKey, $offset, $limit, 'WITHSCORES']);
+ }
}
zrange "laravel_database_shoot_tweet:like" 0 999 WITHSCORES
を表しています。
Configファイル
ファイル名:configs/tweet.php
<?php
return [
...
+ 'BULK_UPDATE_QUERY' => [
+ 'USER_ACTION_COUNT' => 'UPDATE tweets SET %s_count = %s WHERE id IN (%s)',
+ ]
];
ファイル名:configs/tweet.php
<?php
return [
'BULK_UPDATE' => [
'FIELD' => 'FIELD(id%s)',
'ELT' => 'ELT(%s%s)',
]
];
クエリをコードにベタがきするのは気がひけるので、
バルクアップデート用のクエリを定数化しました。
Kernel
ファイル名:app/Console/Kernel.php
...
protected function schedule(Schedule $schedule)
{
$schedule->command('user-action:update --like --impression --update')->daily();
}
...
実はLaravelには、Cronのようにバッチを定期的に実行するための仕組みが用意されています。Console/Kernel.php
というファイルでスケジューラ登録の設定ができます。
今回は、毎日深夜12時に実行するようにします。
最後に、以下のコマンドを叩けば、登録完了です。
自動でバッチが動いてくれます。
// 本当は-eで設定するのはNG。-rと間違えたらcronの設定全部消える〜
crontab -e
// エディタ出てくるので下記テキストを入力、保存
* * * * * cd /{アプリのディレクトリパスを入力してください} && php artisan schedule:run >> /dev/null 2>&1
まとめ
以上、Laravelのバッチ実装でした!
今回のポイントと成果物をまとめます。
ポイント
- Laravelでのバッチ作成法
- Laravelでのバルクアップデート
- Laravelでのバッチスケジューラ
制作物
- バッチクラス
- Redisクラス
- Configファイル
- Kernelクラス(バッチスケジューラ)
Webアプリ開発では、
リアルタイムではDB更新の処理が重い場合、
バッチで定期的にまとめて処理する
というケースも出てきます。
そして
まとめて更新!をする際は
「バルクアップデート」は有効な手段です。
Laravelでのバッチ、バルクアップデート、スケジューラ、
ぜひ使いこなしたいですね!
次回以降ですが、Vuetify
というライブラリを用いて、CSS
を書かずに楽にログイン画面を作ります!
もし、こんな機能を実装してほしいというリクエストがあったら
連絡ください!
実装して皆さんに共有したいと思います。
それではまた!
コメント