Rails:如何在ActiveRecord中设置默认值?

452
如何在 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

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

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

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


看起来你自己回答了这个问题,而且还有两种不同的变体 :) - Adam Byrtek
26
请注意,“self.status = ACTIVE unless self.status”的“标准”Ruby习惯用法是“self.status ||= ACTIVE”。 - Mike Woodhouse
1
Jeff Perrin的答案比当前标记为接受的答案好得多。default_scope不是设置默认值的可接受解决方案,因为它具有巨大的副作用,会改变查询的行为。 - lawrence
请参阅https://dev59.com/_W865IYBdhLWcg3wHK3u - Viktor Trón
2
鉴于这个问题的所有赞数,我认为Ruby需要一个用于ActiveRecord的setDefaultValue方法。 - spartikus
最佳答案:https://dev59.com/mHRC5IYBdhLWcg3wVvjL#41292328 - Blair Anderson
29个回答

586

每种可用的方法都存在一些问题,但我认为定义一个after_initialize回调是下面的理由:

  1. default_scope将为新模型初始化值,但那将成为您找到模型的范围。如果您只想将一些数字初始化为0,则这不是您想要的。
  2. 在迁移中定义默认值也是有效的(在旧版本的Rails中,当您只调用Model.new时,这将无效)。
  3. 重写initialize可以工作,但不要忘记调用super
  4. 使用像phusion这样的插件有点荒谬。这是Ruby,我们真的需要一个插件来初始化一些默认值吗?
  5. 自Rails 3起,覆盖after_initialize已被弃用。当我在rails 3.0.3中覆盖after_initialize时,控制台会收到以下警告:
警告:Base#after_initialize已被弃用,请改用Base.after_initialize:method。 (在/Users/me/myapp/app/models/my_model:15中调用)
因此,我建议编写一个after_initialize回调,它允许您设置默认属性,并且可以像下面这样在关联上设置默认值:
  class Person < ActiveRecord::Base
    has_one :address
    after_initialize :init

    def init
      self.number  ||= 0.0           #will set the default value only if it's nil
      self.address ||= build_address #let's you set a default association
    end
  end    

现在,你只需要在一个地方查找模型的初始化。我会一直使用这种方法,直到有更好的方法出现。

注意事项:

  1. 对于布尔字段,请执行以下操作:

    self.bool_field = true if self.bool_field.nil?

    有关详细信息,请参见Paul Russell在此答案中的评论

  2. 如果您仅为模型选择了子集列(即,在查询中使用select,如Person.select(:firstname, :lastname).all),则如果您的init方法访问未包含在select子句中的列,则会出现MissingAttributeError。您可以采取以下措施来防止这种情况:

    self.number ||= 0.0 if self.has_attribute? :number

    对于布尔列...

    self.bool_field = true if (self.has_attribute? :bool_value) && self.bool_field.nil?

    还要注意,在Rails 3.2之前,语法是不同的(请参见Cliff Darling下面的评论)


7
这似乎绝对是实现这一目标的最佳方式,这真的很奇怪和不幸。建立模型属性默认值的合理首选方法似乎应该是Rails早就内置的东西。唯一其他(可靠的)方法是覆盖“initialize”,但这似乎对于本应清晰明确的事情而言过于复杂。我在搜索之前已经花了几个小时查阅文档,因为我认为这个功能已经存在于某个地方,只是我不知道它在哪里。 - seaneshbaugh
117
对此需要注意的是,如果你有一个布尔类型的字段需要设置默认值,不要使用self.bool_field ||= true的方式,因为这会强制将该字段设为true,即使你明确地将它初始化为false也是如此。相反,应该使用 self.bool_field = true if self.bool_field.nil? 的方式。 - Paul Russell
22
注意:当使用此方法结合Active Record选择特定列时,请注意仅能在查询中指定的属性将被找到,并且初始化代码将抛出MissingAttributeError。您可以添加额外的检查,如下所示:self.number ||= 0.0 if self.has_attribute?(:number) 对于布尔值字段:self.bool_field = true if (self.has_attribute?(:bool_value)) && self.bool_field.nil?。这适用于Rails 3.2及以上版本。对于早期版本,请使用self.attributes.has_key?,并使用字符串而不是符号。 - Cliff Darling
7
使用关联对象进行操作会在查询时预加载这些关联对象。为避免性能问题,请在 initialize 方法中添加 return if !new_record? 语句。 - Kyle Macey
2
第二点不再适用。我猜Rails的开发人员已经实现了它。在迁移中定义:default时,Model.new现在将实际上使用默认值创建对象。在Rails 4.1.16中验证工作正常。 - Magne
显示剩余9条评论

146

Rails 5+

您可以在模型中使用 attribute 方法,例如:

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

你也可以将一个 lambda 函数传递给 default 参数。例如:

attribute :uuid, :string, default: -> { SecureRandom.uuid }

第二个参数是类型,它也可以是自定义类型类实例,例如:

attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }

4
这就是我一直在寻找的宝石!默认值也可以采用一个过程(proc),例如:default: -> { Time.current.to_date }。 - schpet
2
请确保将类型作为第二个参数指定,否则类型将为“Value”,不会进行任何类型转换。 - null
1
令我欣喜的是,这也适用于store_accessor。例如,给定store_accessor:my_jsonb_column,:locale,然后您可以定义attribute:locale,:string,default:'en' - Ryan Romanchuk
哦,太棒了,我需要在表单中显示默认值,这个方法非常好用。谢谢你,Lucas。 - Paul Watson
2
假设您在数据库中有一些现有对象,这些对象是在添加此代码之前创建的(例如,如果您要添加一个新列并希望默认值为该列),则它们在加载时将不会获得此值。但是,如果您使用after_initialize方法,则可以实现这一点。 - phil
显示剩余2条评论

50
我们通过迁移将默认值存储在数据库中(通过在每个列定义上指定:default选项),并让Active Record使用这些值来设置每个属性的默认值。
在我看来,这种方法符合AR的原则:惯例优于配置、DRY、表定义驱动模型,而不是相反。
请注意,虽然默认值仍然存储在应用程序(Ruby)代码中,但不是在模型中,而是在迁移中。

3
另一个问题是当你需要为外键设置默认值时。你不能在外键字段中硬编码ID值,因为在不同的数据库中,ID可能会不同。 - shmichael
2
另一个问题是,这种方式无法初始化非持久访问器(即不是数据库列的属性)。 - Viktor Trón
2
另一个问题是你不能在一个地方看到所有默认值。它们可能分散在不同的迁移中。 - declan
9
Declan,这里有db/schema.rb。 - Benjamin Atkin
7
我想提醒未来的读者:根据我所读到的内容,这与AR的原则相反。模型的逻辑应该在模型类中体现,数据库应该尽可能地无知。对我而言,默认值构成了有关模型的具体逻辑。 - darethas
显示剩余4条评论

42

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

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

4
这是一个非常重要的观点。我想大多数情况下,在持久化新记录之前设置记录的默认值,而不是在加载已持久化的记录时进行设置。 - Russell Silva
谢谢,老兄,你救了我的一天。 - stephanfriedrich
2
before_create 怎么样? - Franklin Yu
:before_create 如何处理单独的 new 和 save 调用?在切换到它之前,我想要检查并真正理解它。 - Joseph Lord

19

通过简单地执行以下操作,可以改进after_initialize回调模式:

after_initialize :some_method_goes_here, :if => :new_record?

如果您的初始化代码需要处理关联数据,那么以下代码将触发一个微妙的n+1问题,因此使用“包含关联”(include)选项读取初始记录可以带来非常实质的好处。

class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end

16

Phusion的开发人员为此提供了一些不错的插件


注意,此插件允许模式迁移中的 :default 值与 Model.new 一起“正常工作”。 - jchook
我可以通过 Model.new 来让迁移中的 :default 值“正常工作”,这与 Jeff 在他的文章中所说的相反。经过验证,适用于 Rails 4.1.16。 - Magne

8

Rails 6.1+

现在你可以在不设置类型的情况下,在你的模型上使用attribute方法。

attribute :status, default: ACTIVE

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

注意:为attribute提供默认值时无法引用类的实例(lambda表达式将在类的上下文中执行,而不是实例中执行)。因此,如果您需要根据实例或关联动态设置默认值,则仍然需要使用替代方法,例如after_initialize回调。如前所述,建议仅限于新记录,以避免在引用关联时进行n+1查询。
after_initialize :do_something_that_references_instance_or_associations, if: :new_record?

after_initialize 只对新记录运行吗? - Ayush Poddar
2
你可能会惊讶地发现,在从数据库加载现有记录后,after_initialize回调函数会运行。 - staxim
如果您只需要它运行新记录,可以这样做:after_initialize :do_something, if: :new_record? - Joshua Pinter

8

我使用attribute-defaults gem

根据文档: 运行 sudo gem install attribute-defaults 并将 require 'attribute_defaults' 添加到您的应用程序中。

class Foo < ActiveRecord::Base
  attr_default :age, 18
  attr_default :last_seen do
    Time.now
  end
end

Foo.new()           # => age: 18, last_seen => "2014-10-17 09:44:27"
Foo.new(:age => 25) # => age: 25, last_seen => "2014-10-17 09:44:28"

8

在从attributes返回的哈希中,状态仍将为nil。已在Rails 5.2.0中进行测试。 - spyle

8

相似的问题,但都有稍微不同的背景: - 如何在Rails activerecord的模型中为属性创建默认值?

最佳答案:取决于你想要什么!

如果你想让每个对象都从一个值开始:使用 after_initialize :init

你想让new.html表单在打开页面时具有默认值吗? 使用https://dev59.com/mHRC5IYBdhLWcg3wVvjL#5127684

class Person < ActiveRecord::Base
  has_one :address
  after_initialize :init

  def init
    self.number  ||= 0.0           #will set the default value only if it's nil
    self.address ||= build_address #let's you set a default association
  end
  ...
end 

如果您希望每个对象都有一个从用户输入计算出的值:使用before_save :default_values

您希望用户输入X,然后Y = X+'foo'吗?使用:

class Task < ActiveRecord::Base
  before_save :default_values
  def default_values
    self.status ||= 'P'
  end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接