バッチ処理の実装
今回は記事投稿アプリにバッチ処理の実装をしてみます。
最終的にはこんなことをやりたいです。
やりたいこと
記事の状態が「下書き」「公開」「公開待ち」と3種類あります。
例えば記事の公開日を1日後にすると記事の状態は「公開待ち」になります。
時間が経って公開日を過ぎたら自動で記事が公開されるようにしたいです。
必要なもの
・Rakeタスク
・cron
・whenever
これらを使って自動で記事が公開される仕組みを作っていきます。
具体的に言うと1時間毎に記事の公開日時が現在時刻を過ぎているか判定し、過ぎていれば記事の状態を「公開」に変更します。
このように一定の間隔で処理を行うことをバッチ処理と言います。
Rakeとは
Rakeとは、rubyで処理内容を定義できるビルドツールです。
このRakeが実行する処理内容をRakeタスク
と呼び、Rakefile
に定義します。
cronとは
cronとは、Unix系のOSに標準で備わっている仕組みです。
「この時間にこのプログラムを実行」「毎日のこの時間になったらこのプログラムの実行」という感じで定期的にコマンドを実行するためにメモリ場で常に命令を待機しているプロセス(=デーモンプロセス)です。
wheneverとは
cronの設定を、rubyの簡単な文法で扱えるようにしたライブラリです。
Rakeタスクファイルを作成
では進めていきましょう。
まずはRakeファイルを作成し、そこに実現したい処理を書いていきます。
記事の状態に関する処理なので名前はarticle_stateとしておきます。
rails g task article_state
こちらのコマンドを実行するとlib/tasks以下にarticle_state.rake
が作成されます。
namespace :article_state do end
この中に処理を書いていきます。
namespace :article_state do desc '公開日時が過去の日付の「公開待ち」記事があれば、ステータスを「公開」に変更する' # desc = description(説明) task update_article_state: :environment do # environmentはDBとのやりとりが必要な際に記述します # ここに処理を書きます end end
こんな感じです。
で、処理なんですが、
Article.where(state: :publish_wait).each do |article| if article.publishd_at <= Time.current article.state = 'published' end end
のようにしてもいいのですが、モデルのscope
を使うとスッキリ書けるようなので使ってみます。
models/article.rb enum state: { draft: 0, published: 1, publish_wait: 2 } scope :past_published, -> { where('published_at <= ?', Time.current) }
現在時刻と比べて公開日が過去になっている記事を取得して、past_published
という名前を付けています。
これを使って
namespace :article_state do desc '公開待ちの中で、公開日時が過去になっているものがあれば、ステータスを「公開」に変更されるようにする' task update_article_state: :environment do Article.publish_wait.past_published.find_each(&:published!) end end
このようにしました。
publish_wait
はenumの定義により使えるようになったものでstateがpublish_waitの記事を取得します。
past_published
は先ほど定義したscopeです。
これを連結して公開日が過去かつ公開待ちの記事を取得しています。
データの取得には、eachだとデータが大量にあった場合にメモリを圧迫するのでfind_each
を使っています。
こちらはデフォルトでは最大1000件のデータを取得して、処理が終わると次の1000件、という感じでメモリへの負荷が少ないです。
(&:published!)
の部分はまだ理解できていないのですが、{ |article| article.published! }
を省略した形のようです。
article.published!
が記事のstateを公開に変更しているのは分かるのですがあとがちょっと分かりません。
学習用に、あるアプリの改修という形で実装しているため分からないところが出てきますがご容赦ください
参考になりそうなサイトだけ貼っておきます。
Ruby: アンパサンドとコロン`&:`記法について調べてみた|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
gem wheneverの導入
Rakeタスクが出来たのでcronの設定をするのですが、まずはgemwhenever
をインストールします。
Gemfileに
gem 'whenever', require: false
としてbundle install
します。
require: falseとするのはこのGem自体がRailsアプリケーションに反映するものではなく、ターミナル(言わばOS)に反映させるものだから、だそうです。
bundle exec wheneverize .
このコマンドでconfig/schedule.rbというファイルが作成されるのでここにcronの設定を記述します。
# Rails.rootを使用するために必要 require File.expand_path(File.dirname(__FILE__) + '/environment') # cronを実行する環境変数 rails_env = ENV['RAILS_ENV'] || :development # cronを実行する環境変数をセット set :environment, rails_env # cronのログの吐き出し場所 set :output, "#{Rails.root}/log/cron.log" # 1時間毎にrakeタスクを実行 every 1.hour do rake 'article_state:update_article_state' end
cronに設定を反映させる
schedule.rbに設定の記述はしましたがまだ反映がされていません。
bundle exec whenever
で設定にエラーがないかを確認し、
bundle exec whenever --update-crontab
でcronに設定を反映します。
clone -l
で設定されているcronを確認できます。
最後に
難しかったですが使いこなせればできることの幅が広がりそうです。
間違いなどあればご指摘いただければと思います。
今回は以上です。ありがとうございました。
参考サイト
Railsでwheneverを使ってバッチを回す!(rakeタスク、cron) - Qiita
RakeタスクとCronを使って1時間ごとにタスクを実行する - WeBlog
library rake (Ruby 3.0.0 リファレンスマニュアル)