如何在Rails中设置默认值?

117

我正在尝试寻找在Rails中设置对象默认值的最佳方式。

我能想到的最好办法是在控制器的new方法中设置默认值。

请问是否有其他更好的做法或者对此方法是否可行有任何建议?


1
这些对象是什么?它们如何被使用?它们是在渲染视图时使用还是用于控制器逻辑? - Gishu
3
如果你在谈论一个ActiveRecord对象,我必须告诉你,“默认值”问题没有合理的解决方案,只有不合理的黑科技。Rails的作者似乎认为这个功能不值得(令人惊讶,因为只有Rails社区这样的)。 - Mauricio
由于已接受答案和大多数答案都集中在ActiveRecords上,我们假设原问题是关于AcitveRecords的。因此可能重复的链接为https://dev59.com/mHRC5IYBdhLWcg3wVvjL - Ciro Santilli OurBigBook.com
可能是如何在ActiveRecord中设置默认值?的重复问题。 - Lucas Caton
17个回答

107

在Ruby语言中,“正确”是一个很危险的词。通常有多种方法来完成同一件事情。如果你知道你将总是想要为该表上的该列设置默认值,那么在数据库迁移文件中设置它们是最简单的方法:

class SetDefault < ActiveRecord::Migration
  def self.up
    change_column :people, :last_name, :type, :default => "Doe"
  end

  def self.down
    # You can't currently remove default values in Rails
    raise ActiveRecord::IrreversibleMigration, "Can't remove the default"
  end
end

由于ActiveRecord自动发现您的表和列属性,这将导致在任何标准Rails应用程序中使用它的任何模型中设置相同的默认值。
但是,如果您只想在特定情况下设置默认值 - 比如,它是一个继承模型,与其他一些共享表格 - 那么另一种优雅的方式是在创建模型对象时直接在您的Rails代码中执行它。
class GenericPerson < Person
  def initialize(attributes=nil)
    attr_with_defaults = {:last_name => "Doe"}.merge(attributes)
    super(attr_with_defaults)
  end
end

当你执行GenericPerson.new()时,它会始终将 "Doe" 属性传递到 Person.new(),除非你使用其他属性进行覆盖。


2
试一下。它将在使用.new类方法调用的新模型对象上运行。那篇博客文章中关于ActiveRecord直接调用.allocate的讨论是关于从数据库加载现有数据的模型对象。在我看来,这种方式对于ActiveRecord来说是一个可怕的想法。但这不是重点。 - SFEley
2
如果你回过头再读一遍原帖的问题,然后按顺序阅读我的评论,可能会更有意义。如果不行的话......好吧,问题已经得到了解答。祝你有美好的一天。 - SFEley
8
更适合进行降级迁移: change_column_default :people, :last_name, nil - Nolan Amy
9
请注意,对于更新的 Rails 版本,在默认参数之前需要添加一个额外的 type 参数。在此页面上搜索 ":type"。 - Ben Wheeler
3
今天我遇到了这个问题 - 对于Rails 4,您还需要指定列类型:change_column :people, :last_name, :string, default: "Doe" - GoBusto
显示剩余7条评论

60

根据SFEley的答案,这里是较新版本Rails的更新/修复版本:

class SetDefault < ActiveRecord::Migration
  def change
    change_column :table_name, :column_name, :type, default: "Your value"
  end
end

2
请注意,此方法不适用于Rails 4.2.x(未在后续版本中进行测试)。 由于change_column可能会对结构造成较大影响,因此无法推断出相反的操作。 如果您不确定,请立即运行db:migratedb:rollback进行测试。 虽然接受的答案与结果相同,但至少可以假定! - gfd
1
这对我来说是正确的答案。它适用于Rails 5.1,但您需要像@GoBusto上面描述的那样执行updown操作。 - Fabrizio Bertoglio

23

首先,你无法重载initialize(*args)方法,因为它并不会在所有情况下被调用。

最好的选择是将默认值放入迁移中:

add_column :accounts, :max_users, :integer, :default => 10

其次,最好的办法是将默认值放入模型中,但这仅适用于最初为nil的属性。您可能会遇到像我一样在处理boolean列时出现问题:

def after_initialize
  if new_record?
    max_users ||= 10
  end
end

你需要new_record?以避免默认值覆盖从数据库加载的值。

你需要||=来防止Rails覆盖传递给初始化方法的参数。


12
小注 -- 你想做两件事:.... 1)不要调用你的方法名为 after_initialize。你想使用 after_initiation :your_method_name .... 2 使用 self.max_users ||= 10 - Jesse Wolgamott
5
对于布尔值,只需要这样做:如果prop为空,则将prop设置为true。 - Francis Potter
2
使用 after_initialize do 而不是 def after_initialize - fotanus
为了保险起见,使用 self.max_users。 - Ken Ratanachai S.

16
您可以在迁移中尝试使用change_column_default(已在Rails 3.2.8中测试):
class SetDefault < ActiveRecord::Migration
  def up
    # Set default value
    change_column_default :people, :last_name, "Smith"
  end

  def down
    # Remove default
    change_column_default :people, :last_name, nil
  end
end

change_column_default Rails API 文档


1
被接受的答案更完整,但我喜欢这个干净且有文档记录的...谢谢! - gfd

8

7
如果您正在引用ActiveRecord对象,则有(不止)两种方法可以执行此操作:

1.在数据库中使用:default参数

例如:

class AddSsl < ActiveRecord::Migration
  def self.up
    add_column :accounts, :ssl_enabled, :boolean, :default => true
  end

  def self.down
    remove_column :accounts, :ssl_enabled
  end
end

更多信息请查看:http://api.rubyonrails.org/classes/ActiveRecord/Migration.html

2. 使用回调函数

例如:before_validation_on_create

更多信息请查看:http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html#M002147


2
在数据库中使用默认值的问题不是它们直到对象保存才被设置吗?如果我想构造一个已经填充了默认值的新对象,我需要在初始化器中设置它们。 - Rafe
布尔值不能默认为1或0 - 它们必须设置为true或false(请参见Silasj的答案)。 - Jamon Holmgren
@JamonHolmgren 感谢您的评论,我已经更正了答案 :) - Vlad Zloteanu

7
在Ruby on Rails v3.2.8中,使用after_initialize ActiveRecord回调函数,你可以调用模型中的一个方法来为新对象分配默认值。

对于每个由finder找到和实例化的对象,都会触发after_initialize回调,同时也会在实例化新对象后触发after_initialize(参见ActiveRecord Callbacks)。

因此,在我看来,它应该是这样的:
class Foo < ActiveRecord::Base
  after_initialize :assign_defaults_on_new_Foo
  ...
  attr_accessible :bar
  ...
  private
  def assign_defaults_on_new_Foo
    # required to check an attribute for existence to weed out existing records
    self.bar = default_value unless self.attribute_whose_presence_has_been_validated
  end
end

对于这个实例,如果实例之前在保存/更新时没有包含已验证的存在性属性,则 Foo.bar = default_value。然后将使用 default_value 与您的视图一起使用,以使用 bar 属性的 default_value 渲染表单。

最好情况下,这是 hacky 的...

编辑 - 使用 'new_record?' 检查是否从新调用进行实例化

不要检查属性值,而是使用 Rails 中的内置方法 new_record?。因此,上面的示例应该如下所示:

class Foo < ActiveRecord::Base
  after_initialize :assign_defaults_on_new_Foo, if: 'new_record?'
  ...
  attr_accessible :bar
  ...
  private
  def assign_defaults_on_new_Foo
    self.bar = default_value
  end
end

这样更加简洁明了。啊,Rails的魔力——它比我还聪明。


这个并不像我预期的那样工作 - 它应该只在新建时设置默认值,但它也会为每个找到和实例化的对象设置属性的默认值(即从数据库加载的记录)。你可以在分配默认值之前检查对象属性是否存在值,但这不是一个非常优雅或健壮的解决方案。 - erroric
1
为什么你在这里推荐使用 after_initialize 回调函数?Rails 的回调文档中有一个示例,在 before_create 中设置默认值而无需额外的条件检查。 - Dfr
@Dfr - 我可能错过了,你能给我一个链接以便我进行查看,然后更新答案吗? - erroric
是的,在这里http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html 的第一个例子,类Subscription - Dfr
5
我们使用after_initialize回调而非before_create回调的原因是为了在用户创建新对象时为用户设置默认值(以在视图中使用)。before_create回调在用户获得新对象、提供输入并将对象提交给控制器进行创建之后才会被调用。然后,控制器会检查任何before_create回调函数。这似乎有些违反直觉,但这是一个命名问题——before_create指的是create操作。实例化一个新对象并不会create该对象。 - erroric
新记录的想法对我来说效果不错,尽管它看起来像这样:after_initialize :assign_defaults_on_new_Foo, :if => Proc.new{|f| f.new_record? } - ideaoforder

5
对于Rails 3.2.6及以上版本中的布尔字段,在您的迁移中可以使用以下代码:
def change
  add_column :users, :eula_accepted, :boolean, default: false
end

在这里,将默认值设置为10是行不通的,因为它是一个布尔字段。它必须是truefalse的值。

4
生成一个迁移并使用change_column_default,这非常简洁且可逆。
class SetDefaultAgeInPeople < ActiveRecord::Migration[5.2]
  def change
    change_column_default :people, :age, { from: nil, to: 0 }
  end
end

1

我需要设置一个默认值,就像在数据库中指定默认列值一样。这样它的行为就是这样的。

a = Item.new
a.published_at # => my default value

a = Item.new(:published_at => nil)
a.published_at # => nil

因为after_initialize回调是在从参数设置属性之后调用的,所以无法知道属性是否为nil是因为从未设置还是故意设置为nil。因此,我不得不稍微探究一下,并想出了这个简单的解决方案。

class Item < ActiveRecord::Base
  def self.column_defaults
    super.merge('published_at' => Time.now)
  end
end

对我来说非常有效。(Rails 3.2.x)


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