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个回答

5

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

在 ActiveRecord 模型中设置字段的默认值的标准方法是什么?

Rails 5 之前的标准方法实际上是在迁移中设置它,并且只需查看 db/schema.rb 即可了解数据库为任何模型设置的默认值。

与 @Jeff Perrin 的回答(有点过时)相反,迁移方法甚至会在使用 Model.new 时应用默认值,这归功于 Rails 的一些魔法。已在 Rails 4.1.16 中验证工作正常。

最简单的事情通常是最好的。代码库中的知识负债和潜在混淆点更少。而且它“只是起作用”。

class AddStatusToItem < ActiveRecord::Migration
  def change
    add_column :items, :scheduler_type, :string, { null: false, default: "hotseat" }
  end
end

或者,如果要更改列而不创建新列,则执行以下操作之一:

class AddStatusToItem < ActiveRecord::Migration
  def change
    change_column_default :items, :scheduler_type, "hotseat"
  end
end

或者更好的是:

class AddStatusToItem < ActiveRecord::Migration
  def change
    change_column :items, :scheduler_type, :string, default: "hotseat"
  end
end

查看官方的RoR指南,了解列更改方法的选项。

null: false禁止在数据库中使用NULL值,并且作为附加好处,它还会更新所有先前为null的预存在数据库中的记录,将其设置为该字段的默认值。如果您希望,可以在迁移中排除此参数,但我发现它非常方便!

Rails 5+中的规范方式是,如@Lucas Caton所说:

class Item < ActiveRecord::Base
  attribute :scheduler_type, :string, default: 'hotseat'
end

4

构造函数的作用就是这个!重写模型的initialize方法。

使用after_initialize方法。


2
通常情况下你是正确的,但是你不应该覆盖 ActiveRecord 模型中的 initialize 方法,因为它可能不会被调用。相反,你应该使用 after_initialize 方法。 - Luke Redpath
仅仅为了设置默认值而使用 default_scope 是绝对不正确的。正确的方式是使用 after_initialize。 - joaomilho

4

大家好,我最终做了以下事情:

def after_initialize 
 self.extras||={}
 self.other_stuff||="This stuff"
end

运行得非常好!

3

这个问题已经有很长时间的答案了,但是我经常需要默认值,而且不愿意把它们放在数据库中。我创建了一个 DefaultValues concern:

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

2
我遇到了关于after_initialize的问题,当进行复杂查询时会出现ActiveModel::MissingAttributeError错误:
例如:
@bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)

"search"在.where中是条件的哈希。

所以我最终通过以下方式重写initialize来完成它:

def initialize
  super
  default_values
end

private
 def default_values
     self.date_received ||= Date.current
 end
super调用是必要的,以确保对象在执行自定义代码(如:default_values)之前从ActiveRecord::Base正确初始化。

1
我喜欢它。在Rails 5.2.0中,我需要执行def initialize(*); super; default_values; end。此外,默认值即使在.attributes哈希中也是可用的。 - spyle

1

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

有一些棘手的情况几乎需要重写 initialize 方法,而该 gem 正是这样做的。

例如:

您的数据库默认值为 NULL,您的模型/ Ruby 定义的默认值为 "some string",但出于某种原因,您实际上想将该值设置为 nil:MyModel.new(my_attr: nil)

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

好的,所以您可以放弃 ||= 方法,改用 my_attr_changed?...

但是 现在想象一下,您的数据库默认值为 "some string",您的模型/ Ruby 定义的默认值为 "some other string",但在某种情况下,您希望将该值设置为 "some string"(数据库默认值):MyModel.new(my_attr: 'some_string')

这将导致my_attr_changed?false,因为该值与数据库默认值相匹配,这将触发您定义的 Ruby 默认代码并将该值设置为"some other string"-- 这不是您想要的结果。
由于这些原因,我认为仅凭after_initialize钩子无法实现适当的功能。再次,我认为“default_value_for”宝石采取了正确的方法:https://github.com/FooBarWidget/default_value_for

1

after_initialize 方法已被弃用,请使用回调函数代替。

after_initialize :defaults

def defaults
  self.extras||={}
  self.other_stuff||="This stuff"
end

然而,在您的迁移中使用:default仍然是最干净的方法。


4
在Rails 3中,after_initialize方法并没有被弃用。实际上,你给出的宏式回调例子是被弃用的。详情请参见:http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find - Zabba

1
class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end

2
嗯...一开始看起来很巧妙,但经过思考后,我发现有几个问题。首先,所有默认值不在一个单独的点中,而是分散在整个类中(想象一下搜索或更改它们)。其次,最糟糕的是,您无法在以后放置空值(甚至是false值!)。 - paradoja
为什么需要将空值设置为默认值?使用AR时,您无需进行任何操作即可获得该功能。至于布尔列的false,则不是最佳选择。 - Mike Breen
我不能代表其他人的编码习惯。我没有遇到问题,因为我不会在类文件中随意散布我的getter/setter。此外,任何现代文本编辑器都应该可以轻松导航到一个方法(在TextMate中是shift-cmd-t)。 - Mike Breen
@paradoja - 我收回之前的说法,我现在明白使用null也会出现问题。不一定要将null作为默认值,但如果你确实想在某个时候将值更改为null,那就会出现问题。@paradoja,你发现得好,谢谢。 - Mike Breen
我使用这种方法,因为它能很好地处理动态生成的属性。 - Dale Campbell

1
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')。在这种情况下,我认为您确实需要在数据库约束中设置默认值(如果DB支持)。

这个解决方案和提出的替代方案在我的情况下都不起作用:我有一个 STI 类层次结构,只有一个类具有该属性,并且对应的列将用于数据库查询条件。 - cmoran92

0

我发现使用验证方法可以对设置默认值提供很多控制。您甚至可以为更新设置默认值(或验证失败)。如果您真的想这样做,您甚至可以为插入和更新设置不同的默认值。 请注意,在调用#valid?之前,默认值不会被设置。

class MyModel
  validate :init_defaults

  private
  def init_defaults
    if new_record?
      self.some_int ||= 1
    elsif some_int.nil?
      errors.add(:some_int, "can't be blank on update")
    end
  end
end

关于定义 after_initialize 方法,可能会存在性能问题,因为 after_initialize 也会被 :find 返回的每个对象调用: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find

验证难道不是只在保存之前发生吗?如果您想在保存之前显示默认值怎么办? - nurettin
@nurettin 这是一个很好的观点,我可以理解为什么有时候你会想要这样做,但是 OP 没有提到这是一个要求。你必须自己决定是否想要在每个实例上设置默认值的开销,即使它没有被保存。另一种选择是保留一个虚拟对象供“new”操作重用。 - Kelvin

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