Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby on Rails: コンソール使用時に特定のモデルを読み取り専用にするMEMO

APIから取得した結果を保存するModelやユーザー情報を扱うModel、中間結果を保存するテーブル等、直接更新してしまうと危険なのでコンソールから起動するときにはReadonlyにしたいなぁと思い色々やり方を考えてみたのでメモしておきます📝

rails console -sもありますが、つけ忘れたりするのと、トランザクションをコミットしないだけなのでトランザクション分離レベルによっては影響する可能性があるのかなぁと思いまして。Readonlyの例外はクエリ発行前に発生しそうなのでより安全なのかなと。

github.com

実際のコード

本体のコードは以下の通りでActiveRecord#readonly!を参考にreadonly?をtrueで返すModelReadonly::Readonlyを用意して、ModelReadonly::Service.call!を呼び出したときに引数で指定したModel + ファイルに設定したModelにModelReadonly::ReadonlyをincludeしてReadonlyにしています。

# frozen_string_literal: true

# NOTE: ActiveRecord#readonly!を参考にreadonly?でtrueを返却し読み取り専用にしてる。
# ※touch等は防げないので完全ではないため注意
# * https://github.com/rails/rails/blob/11c2c0ef2f5e3382fa95fc0e9e94835f74a31702/activerecord/lib/active_record/core.rb#L689-L691
# * https://github.com/rails/rails/blob/11c2c0ef2f5e3382fa95fc0e9e94835f74a31702/activerecord/lib/active_record/core.rb#L641-L644
module ModelReadonly::Readonly
  def readonly?
    true
  end
end

class ModelReadonly::Service
  class InvalidSettings < StandardError; end

  def self.call!(models: [], config_path: nil)
    raise InvalidSettings if models.empty? && config_path.nil?

    new(models: models + configured_models(config_path)).call
  end

  def self.configured_models(config_path)
    return [] if config_path.nil?

    configured_yml = YAML.load_file(config_path)
    configured_yml['readonly_models'].map(&:constantize)
  end

  def initialize(models: [])
    @models = models
  end

  attr_reader :models

  def call
    models.each do |model|
      model.class_eval { include ModelReadonly::Readonly }
    end
  end
end

あとはinitializer内で以下のような形で利用して上げればOKです🙆‍♂️

# frozen_string_literal: true

# NOTE: 本番環境では誤操作防止のため特定のモデルをコンソール起動時にReadonlyにする。
if Rails.env.production?
  Rails.application.configure do
    console { ModelReadonly::Service.call!(models: [Entry]) }
  end
end

ファイルを用意して以下のような形でもOKです🙆‍♂️

readonly_models:
- Entry
# frozen_string_literal: true

# NOTE: 本番環境では誤操作防止のため特定のモデルをコンソール起動時にReadonlyにする。
if Rails.env.production?
  Rails.application.configure do
    console do
      config_path = Rails.root.join('lib/model_readonly/readonly_models.yml')
      ModelReadonly::Service.call!(config_path: config_path)
    end
  end
end

以下のような形で更新しようとしたときにReadOnlyRecordの例外が発生します🚨

irb(main)> Entry.last.update(title: "foo")
Feed is marked as readonly (ActiveRecord::ReadOnlyRecord)

参考

hai3.net

railsguides.jp