我如何在ActiveRecord设置默认值?

我看到Pratik的一篇文章,描述了一段丑陋而复杂的代码:http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model

class Item < ActiveRecord::Base  
  def initialize_with_defaults(attrs = nil, &block)
    initialize_without_defaults(attrs) do
      setter = lambda { |key, value| self.send("#{key.to_s}=", value) unless
        !attrs.nil? && attrs.keys.map(&:to_s).include?(key.to_s) }
      setter.call('scheduler_type', 'hotseat')
      yield self if block_given?
    end
  end
  alias_method_chain :initialize, :defaults
end

我在谷歌上看到了以下例子:

  def initialize 
    super
    self.status = ACTIVE unless self.status
  end

and

  def after_initialize 
    return unless new_record?
    self.status = ACTIVE
  end

我也见过有人把它放在迁移中,但我更愿意看到它在模型代码中定义。

是否有一个规范的方法来设置默认值的字段在ActiveRecord模型?


当前回答

一些简单的情况可以通过在数据库模式中定义默认值来处理,但这不能处理许多棘手的情况,包括其他模型的计算值和键。对于这些情况,我这样做:

after_initialize :defaults

def defaults
   unless persisted?
    self.extras||={}
    self.other_stuff||="This stuff"
    self.assoc = [OtherModel.find_by_name('special')]
  end
end

我决定使用after_initialize,但我不希望它应用于只发现那些新的或创建的对象。我认为几乎令人震惊的是,这个明显的用例没有提供一个after_new回调,但我已经通过确认对象是否已经被持久化来表明它不是新的。

看过Brad Murray的回答后,如果条件被移动到回调请求,这就更加清晰了:

after_initialize :defaults, unless: :persisted?
              # ":if => :new_record?" is equivalent in this context

def defaults
  self.extras||={}
  self.other_stuff||="This stuff"
  self.assoc = [OtherModel.find_by_name('special')]
end

其他回答

https://github.com/keithrowell/rails_default_value

class Task < ActiveRecord::Base
  default :status => 'active'
end

我强烈建议使用“default_value_for”gem: https://github.com/FooBarWidget/default_value_for

有一些棘手的场景需要重写initialize方法,这个gem就是这样做的。

例子:

你的db默认值是NULL,你的模型/ruby定义的默认值是“一些字符串”,但你实际上想要设置值为nil的原因:MyModel。新(my_attr: nil)

这里的大多数解决方案将无法将值设置为nil,而是将其设置为默认值。

好吧,所以你不用||=方法,而是切换到my_attr_changed?…

但是现在假设你的数据库默认值是"some string",你的模型/ruby定义的默认值是"some other string",但在某种情况下,你想将值设置为"some string"(数据库默认值):MyModel。新(my_attr:“some_string”)

这将导致my_attr_changed?为false是因为该值与db默认值匹配,而db默认值将触发ruby定义的默认代码,并将该值设置为“其他字符串”——同样,这不是您想要的。


由于这些原因,我不认为这可以通过一个after_initialize钩子正确地完成。

同样,我认为“default_value_for”gem采用了正确的方法:https://github.com/FooBarWidget/default_value_for

Rails 6.1 +

现在您可以在模型上使用属性方法,而无需设置类型。

attribute :status, default: ACTIVE

or

class Account < ApplicationRecord
  attribute :locale, default: 'en'
end

注意,提供默认的to属性不能引用类的实例(lambda将在类的上下文中执行,而不是在实例中执行)。因此,如果需要根据实例或关联动态地将默认值设置为一个值,则仍然必须使用替代方法,例如after_initialize回调。如前所述,如果引用关联,建议将此限制为新记录,以避免n+1个查询。

after_initialize :do_something_that_references_instance_or_associations, if: :new_record?

after_initialize解决方案的问题是,无论是否访问该属性,都必须向从DB中查找的每个对象添加after_initialize。我建议采用惰性加载方法。

属性方法(getter)当然是方法本身,因此您可以覆盖它们并提供默认值。喜欢的东西:

Class Foo < ActiveRecord::Base
  # has a DB column/field atttribute called 'status'
  def status
    (val = read_attribute(:status)).nil? ? 'ACTIVE' : val
  end
end

除非,就像有人指出的那样,你需要执行Foo.find_by_status('ACTIVE')。在这种情况下,我认为您确实需要在数据库约束中设置默认值,如果数据库支持它的话。

这个问题已经回答了很长时间了,但是我经常需要默认值,并且不喜欢将它们放在数据库中。我创建了一个DefaultValues关注点:

module DefaultValues
  extend ActiveSupport::Concern

  class_methods do
    def defaults(attr, to: nil, on: :initialize)
      method_name = "set_default_#{attr}"
      send "after_#{on}", method_name.to_sym

      define_method(method_name) do
        if send(attr)
          send(attr)
        else
          value = to.is_a?(Proc) ? to.call : to
          send("#{attr}=", value)
        end
      end

      private method_name
    end
  end
end

然后像这样在我的模型中使用它:

class Widget < ApplicationRecord
  include DefaultValues

  defaults :category, to: 'uncategorized'
  defaults :token, to: -> { SecureRandom.uuid }
end