動かざることバグの如し

近づきたいよ 君の理想に

Railsでモデルのカラムのデフォルト値をセットする方法

環境

データベースに記述する

そもそもRails側でやらなくても、データベースにデフォルト値をセットする機能が備わっているのでそっちを使う。

マイグレーションで書くとこんな感じ

create_table :posts do |t|
  t.string :name, default: "nanashi"
  t.timestamps
end

試してみる。しっかりとデフォルト値が設定されているのがわかる。

Post.new.name
=> "nanashi"

メリット

  • データベース側で保証されるため安全で確実
  • アプリケーションのロジックに依存しない
  • パフォーマンスへの影響が少ない

デメリット

  • 動的な値を設定できない
  • マイグレーションが必要
  • 既存レコードへの影響を考慮する必要がある

attributeを使う

Rails5からActiveRecordにattributes APIというのが追加された。

これを使うとモデル内にデフォルト値をセットできる。

class Post < ApplicationRecord
  attribute :name, :string, default: 'nanashi'
end

試してみる

Post.new.name
=> "nanashi"

詳しいドキュメントは以下

ちなみにデフォルト値を現在時刻にしたい場合、default: Time.current としてはいけない。

Time.current(Time.nowも)はオブジェクトが初期化される時点での時刻で固定されてしまうため、attributeのデフォルト値として使用すると意図した動作にならない。 代わりに遅延評価されるラムダ式を使用し、Time.currentを呼び出すことで、オブジェクトが生成される都度、その時点での現在時刻を取得できる。

# NG
class Screenshot < ApplicationRecord
  attribute :ts, default: Time.current
end

# OK
class Screenshot < ApplicationRecord
  attribute :ts, default: -> { Time.current }
end

メリット

  • モデル内で完結するためコードの見通しが良い
  • 動的な値を設定可能
  • 型変換も同時に行える

デメリット

  • アプリケーションメモリを消費する
  • データベース側では保証されない
  • Rails 5以降でしか使用できない

after_initializeを使う

RailsActiveRecordにはいくつもコールバックが用意されていて、after_initialize()でデフォルト値を入れてしまおうというやり方である。

class Post < ApplicationRecord
  after_initialize :set_default, if: :new_record?

  private
  def set_default
    self.name ||= "nanashi"
  end
end

なぜ new_record? が必要かというと、なにもafter_initializeが走るのはモデルで新しいレコードのインスタンスを生成したときだけではなく、Post.all とかで既存のDBから取得したときも当然インスタンスがレコード分だけ生成されるのでafter_initalizeは走ってしまうからである。

ちなみに他の before_validation とか before_save のコールバックでデフォルト値をいれようとするのは禁じ手である

メリット

  • 複雑な条件分岐による値設定が可能
  • 他のモデルの状態に応じた値設定ができる
  • 既存の値を上書きしない制御が容易

デメリット

  • コールバックの実行順序を意識する必要がある
  • パフォーマンスへの影響が大きい
  • コードが複雑になりやすい

バリデーション時にデフォルト値を設定

やってはいけない。

参考リンク