🦓

[Rails]予約投稿

2023/07/29に公開

はじめに

前回の記事で投稿に公開予約機能を追加しましたが、手動で公開する必要があって不便です。
公開日時が過去で「公開予約」となっている投稿を更新しなくても、自動でステータスを「公開中」に変更できるようにしていきます。

rakewheneverにより1時間ごとに走らせ、ステータスを変更していきます。

環境

Rails 7.0.4.3
ruby 3.2.1

scopeを作成する

予約公開されたブログ一覧を取得するためにscopeを作成します。

app/models/blog.rb
class Blog < ApplicationRecord
...
    scope :scheduled, -> { joins(:status).where("statuses.published_at > ?", Time.current) }
    scope :scheduled_for_next_7_days, -> { joins(:status).where("statuses.published_at > ? AND statuses.published_at <= ?", Time.current, 7.days.from_now) }
    scope :to_be_published, -> { joins(:status).where(statuses: { name: 2 }).where("statuses.published_at <= ?", Time.current) }

end
irb(main):001:0> Blog.scheduled
  Blog Load (0.4ms)  SELECT "blogs".* FROM "blogs" INNER JOIN "statuses" ON "statuses"."statusable_type" = $1 AND "statuses"."statusable_id" = "blogs"."id" WHERE (statuses.published_at > '2023-07-29 22:53:01.229405')  [["statusable_type", "Blog"]]
=> 
[#<Blog:0x0000000116bd6048
  id: 17,
  title: "予約公開投稿",
  content: "テストテスト",
  user_id: 1,
  created_at: Sat, 29 Jul 2023 16:32:58.770636000 JST +09:00,
  updated_at: Sat, 29 Jul 2023 22:51:10.978199000 JST +09:00>]
irb(main):002:0> blog.status
  Status Load (0.5ms)  SELECT "statuses".* FROM "statuses" WHERE "statuses"."statusable_id" = $1 AND "statuses"."statusable_type" = $2 LIMIT $3  [["statusable_id", 17], ["statusable_type", "Blog"], ["LIMIT", 1]]
=> 
#<Status:0x0000000115c7aab8
 id: 52,
 name: "scheduled",
 statusable_type: "Blog",
 statusable_id: 17,
 created_at: Sat, 29 Jul 2023 20:38:45.674161000 JST +09:00,
 updated_at: Sat, 29 Jul 2023 21:58:20.810701000 JST +09:00,
 published_at: Sat, 01 Jan 2028 00:00:00.000000000 JST +09:00>
irb(main):003:0> blog.status.update(name:1)
  TRANSACTION (0.2ms)  BEGIN
  Status Exists? (0.4ms)  SELECT 1 AS one FROM "statuses" WHERE "statuses"."name" = $1 AND "statuses"."id" != $2 AND "statuses"."statusable_id" = $3 AND "statuses"."statusable_type" = $4 LIMIT $5  [["name", 1], ["id", 52], ["statusable_id", 17], ["statusable_type", "Blog"], ["LIMIT", 1]]
  Status Update (0.6ms)  UPDATE "statuses" SET "name" = $1, "updated_at" = $2 WHERE "statuses"."id" = $3  [["name", 1], ["updated_at", "2023-07-29 22:58:52.836107"], ["id", 52]]
  TRANSACTION (1.5ms)  COMMIT
=> true

rakeタスクを作成する

rakeは、タスクを自動化するためのビルドツールです。
rakeは特定の作業を自動化するために、タスク(Task)と呼ばれるRubyスクリプトの集合を定義します。

bin/rails generate task publish_blog
      create  lib/tasks/publish_blog.rake

scheduledの投稿をpublihsedに変更するので名前をchange_scheduled_to_publishedにします。

lib/tasks/publish_blog.rake
namespace :publish_blog do
    desc "予約公開のブログを公開する"
    # environmentはDBへアクセスする場合に追記する。今回だとstatus.nameカラムの中身が変わるので必要。
    task change_scheduled_to_published: :environment do
      models = [Blog, Article, Idea] # 複数のモデルに共通する場合使う
  
      models.each do |model|
        if model.reflect_on_association(:status)
          scheduled_posts = model.to_be_published
  
          scheduled_posts.each do |post|
            post.status.update(name: 2)
          end
  
          puts "Successfully updated #{scheduled_posts.count} scheduled #{model.name} posts to published."
        else
          puts "#{model.name} is not published. Skipping..."
        end
      end
    end
  end

reflect_on_associationは、RailsのActive Recordクラスで提供されるメソッドの一つです。

reflect_on_associationメソッドは、特定のクラスに対して関連付けられている他のクラスの情報を取得する際に使用します。具体的には、関連付けのタイプ(belongs_tohas_onehas_manyなど)、関連するクラス名、外部キーカラムなどの情報を取得できます。

irb(main):014:0> Blog.reflect_on_association(:status)
=> 
#<ActiveRecord::Reflection::HasOneReflection:0x00000001157bde80
 @active_record=
  Blog(id: integer, title: string, content: text, user_id: integer, created_at: datetime, updated_at: datetime),
 @klass=nil,
 @name=:status,
 @options={:as=>:statusable, :dependent=>:destroy, :autosave=>true},
 @plural_name="statuses",
 @scope=nil,
 @type="statusable_type">
irb(main):015:0> Blog.reflect_on_association(:status).macro
=> :has_one
irb(main):016:0> Blog.reflect_on_association(:status).class_name
=> "Status"
irb(main):017:0> Blog.reflect_on_association(:status).foreign_key
=> "statusable_id"

https://api.rubyonrails.org/classes/ActiveRecord/Reflection/ClassMethods.html

rakeタスクを実行する

bundle exec rake publish_blog:change_scheduled_to_published
	Successfully updated 0 scheduled Blog posts to published.

wheneverをインストールする

wheneverは、Railsアプリケーションにおいて定期的に実行したいタスクをスケジュールするためのGemです。wheneverを使用することで、cronジョブを簡単に設定できます。

Gemfile
gem 'whenever', require: false
bundle install

https://github.com/javan/whenever

スケジュールファイルを作成する

configディレクトリ配下にあるschedule.rbファイルを作成してくれます。

bundle exec wheneverize .
[add] writing `./config/schedule.rb'
[done] wheneverized!
config/schedule.rb
# Rails.rootを使用
require File.expand_path(File.dirname(__FILE__) + "/environment")

# cronを実行する環境変数(RAILS_ENVが指定されていないときはdevelopmentを使用)
rails_env = ENV['RAILS_ENV'] || :development

# cronの実行環境を指定(上記で作成した変数を指定)
set :environment, rails_env

# cronのログファイルの出力先指定
set :output, "#{Rails.root}/log/cron.log"

# パスを通すコマンド
job_type :rake, "source /Users/[ユーザー名]/.zshrc; export PATH=\"$HOME/.rbenv/bin:$PATH\"; eval \"$(rbenv init -)\"; cd :path && RAILS_ENV=:environment bundle exec rake :task :output"

# 一時間毎に実行する&タスク名の指定
every :hour do
  rake 'publish_blog:change_scheduled_to_published'
end

job_typeのカスタムは、rakeタスクをcronジョブとして設定する際に、環境のセットアップや正しいRubyのバージョンの使用を保証するために使用されます。これにより、タスクが予定通りに実行され、環境の問題を回避することができます。

  1. source /Users/[ユーザー名]/.zshrc: シェルの設定ファイル(.zshrc)を読み込むコマンドです。ここではZshシェルを使っていることを前提にしています。.zshrcには、環境変数やパスの設定などが含まれていることが一般的です。

  2. export PATH="$HOME/.rbenv/bin:$PATH": Rubyのバージョン管理ツールであるrbenvのパスを環境変数PATHに追加します。これにより、rbenvで管理しているRubyのバージョンが使えるようになります。

  3. eval "$(rbenv init -)": rbenvの初期化を行います。これにより、Railsアプリケーションがrbenvで管理しているRubyのバージョンを正しく認識できます。

  4. cd :path: カレントディレクトリをRailsアプリケーションのルートディレクトリに移動します。

  5. RAILS_ENV=:environment: :environmentの部分はwheneverを実行する際に指定した環境名(developmentproductionなど)に置き換わります。これにより、実行するRailsアプリケーションの環境を指定できます。

  6. bundle exec rake :task: :taskの部分は実行するタスク名に置き換わります。bundle exec rakeコマンドを使って、RailsアプリケーションのRakeタスクを実行します。

  7. :output: ジョブの標準出力とエラー出力の先を指定します。この部分もwheneverを実行する際に指定した設定に置き換わります。

wheneverを実行する

# wheneverの設定更新
bundle exec whenever --update-crontab 
[write] crontab file updated

# 設定内容にエラーが無いか確認
bundle exec whenever
0 * * * * /bin/bash -l -c 'source /Users/**/.zshrc; export PATH="$HOME/.rbenv/bin:$PATH"; eval "$(rbenv init -)"; cd /Users/chloe/workspace/rails_blog && RAILS_ENV=development bundle exec rake publish_blog:change_scheduled_to_published >> /Users/***/***/***/log/cron.log 2>&1'

## [message] Above is your schedule file converted to cron syntax; your crontab file was not updated.
## [message] Run `whenever --help' for more options.

# 設定されているcronを確認
crontab -l  

# Begin Whenever generated tasks for: /Users/***/***/***/config/schedule.rb at: 2023-07-29 23:27:56 +0900
0 * * * * /bin/bash -l -c 'source /Users/**/.zshrc; export PATH="$HOME/.rbenv/bin:$PATH"; eval "$(rbenv init -)"; cd /Users/chloe/workspace/rails_blog && RAILS_ENV=development bundle exec rake publish_blog:change_scheduled_to_published >> /Users/***/***/***/log/cron.log 2>&1'

# End Whenever generated tasks for: /Users/***/***/***/config/schedule.rb at: 2023-07-29 23:27:56 +0900

# crontabの設定を削除
bundle exec whenever --clear-crontab

終わり

予約投稿機能でした。
wheneverを使用することで、rakeタスクのスケジュールを効率的に管理できるので便利ですね。

Discussion