LoginSignup
2
2

More than 3 years have passed since last update.

Rails: rake taskを良い感じに書く方法

Last updated at Posted at 2020-09-27

良い感じ = 以下の二点

  1. テストしやすいこと
    • 工数削減のため
  2. (ある程度は)エンジニア間での書き方が統一できること
    • 例えばdryrunの指定方法が書き手によってまちまちだと、商用でのtask実行時に事故が起きる可能性もあるので統一可能であればした方が良い
    • ログのフォーマットが統一されていないと作業効率が悪い

などなど

コード

lib/tasks/issue_6885.rake
require_relative 'helpers/all_user_name_update_helper.rb'

namespace :issue_6885 do

  desc 'これはサンプルです'
  task all_user_name_update: :environment do
    helper = AllUserNameUpdateHelper.new

    helper.main
  end
end
lib/tasks/helpers/all_user_name_update_helper.rb
require_relative 'rake_helper_template'

class AllUserNameUpdateHelper < RakeHelperTemplate
  NEW_NAME = 'bar'

  def main
    template do |logger|
      all_users = get_all_user
      logger.info("対象ユーザー数: #{all_users.size}")

      if all_users.blank?
        logger.info('対象ユーザー数が0件だったため処理を終了します')
        return
      end

      all_users.each { |user| update_name(user, NEW_NAME) }
    end
  end

  private

  def get_all_user
    User.all
  end

  def update_name(user, new_name)
    user.update!(name: new_name)
    user
  end
end

lib/tasks/helpers/rake_helper_template.rb
class RakeHelperTemplate
  # 標準出力も行う
  def make_logger(log_file_path)
    logger = ActiveSupport::Logger.new(log_file_path)
    stdout_logger = ActiveSupport::Logger.new(STDOUT)
    broadcast_logger = ActiveSupport::Logger.broadcast(stdout_logger)
    logger.extend(broadcast_logger)
    logger.formatter = Logger::Formatter.new
    logger
  end

  # ログファイル名はtask名と同じにする(コロンは使わない方が良いので置換する)
  # rakeタスクの実行以外から呼び出されるケース(例: spec)を考慮しておく
  def make_log_file_path
    task_name = Rake.try(:application)&.top_level_tasks&.[](0)&.gsub(':', '_') || Rails.env
    log_file_name = "#{task_name}.log"
    Rails.root.join('log', log_file_name)
  end

  def template
    log_file_path = make_log_file_path

    # 明示的に文字列のfalseを渡さない限りは必ずdryrunにする
    is_dryrun = ENV['is_dryrun'] != 'false'

    logger = make_logger(log_file_path)
    logger.info("Start. is_dryrun: #{is_dryrun}")

    ActiveRecord::Base.transaction do
      yield(logger)
      raise ActiveRecord::Rollback if is_dryrun
    end

    logger.info("Finish. log_file_path: #{log_file_path}")
  end
end

実行例

dryrunにする場合
$ is_dryrun=false bundle exec rake issue_6885:all_user_name_update
I, [2020-09-27T13:33:27.229764 #13040]  INFO -- : Start. is_dryrun: true
I, [2020-09-27T13:33:27.556890 #13040]  INFO -- : 対象ユーザー数: 1
I, [2020-09-27T13:33:27.755933 #13040]  INFO -- : Finish. log_file_path: /app/log/issue_6885_all_user_name_update.log 

ポイント

  1. .rakeファイルはhelperクラスを呼び出すだけにして、rakeのDSL?的なお作法を気にせずに普段書き慣れているclassのメソッドをテストするようにした
  2. yieldを使ってdryrun, logger(の一部)を共通化し、rake taskを作成するたびに毎回定義しなくても済むようにした
    • mainメソッド(名前は何でも良い)には極力そのtaskの処理フローだけを定義するようにし、個別具体的な処理はテストしやすい粒度で別メソッドに切り出すようにする

テンプレートにしてしまえばall_user_name_update_helper.rbのようにやりたいことに集中できる!(^○^)

所感

  • rake taskの書き方は久々に書くと忘れてるのでこの記事は実は備忘録的な意味合いもあったり
  • yieldは正直わかりにくいので使いたくない派だけど、テンプレートのように毎回書いたり読んだりしないものであれば良いかなという考え
  • 他にも良い感じの書き方あるよという方いたら教えてください!
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2