438

如何在 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 模型中的字段设置默认值?

4

29 回答 29

578

每个可用方法都有几个问题,但我认为定义after_initialize回调是可行的方法,原因如下:

  1. default_scope将为新模型初始化值,但这将成为您找到模型的范围。如果您只想将一些数字初始化为 0,那么这不是您想要的。
  2. 在您的迁移中定义默认值也有部分时间......正如已经提到的,当您只调用 Model.new 时,这将不起作用。
  3. 覆盖initialize可以工作,但不要忘记调用super
  4. 使用像 phusion 这样的插件有点荒谬。这是 ruby​​,我们真的需要一个插件来初始化一些默认值吗?
  5. after_initialize 从 Rails 3 开始不推荐使用覆盖。当我after_initialize在 rails 3.0.3 中覆盖时,我在控制台中收到以下警告:

弃用警告: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您将得到一个。您可以像这样防范这种情况:MissingAttributeErrorinitselect

    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 评论)

于 2011-02-26T15:14:11.000 回答
127

导轨 5+

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

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

您还可以将 lambda 传递给default参数。例子:

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

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

attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }
于 2017-04-19T01:40:29.107 回答
50

我们通过迁移将默认值放入数据库中(通过:default在每个列定义上指定选项)并让 Active Record 使用这些值来为每个属性设置默认值。

恕我直言,这种方法符合 AR 的原则:约定优于配置、DRY、表定义驱动模型,而不是相反。

请注意,默认值仍在应用程序 (Ruby) 代码中,尽管不在模型中,而是在迁移中。

于 2008-11-30T13:45:52.493 回答
41

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

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
于 2012-07-18T14:36:30.567 回答
18

只需执行以下操作即可改进 after_initialize 回调模式

after_initialize :some_method_goes_here, :if => :new_record?

如果您的 init 代码需要处理关联,这有一个重要的好处,因为如果您读取初始记录而不包括关联,则以下代码会触发微妙的 n+1。

class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end
于 2012-09-14T05:37:50.120 回答
16

Phusion 家伙为此提供了一些不错的插件

于 2008-11-30T09:42:56.740 回答
8

我用attribute-defaults宝石

从文档中:运行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"
于 2013-01-11T04:00:59.257 回答
8

比建议的答案更好/更清洁的潜在方法是覆盖访问器,如下所示:

def status
  self['status'] || ACTIVE
end

请参阅ActiveRecord::Base 文档中的“覆盖默认访问器”以及StackOverflow 中有关使用 self 的更多信息

于 2014-08-11T18:55:54.763 回答
8

类似的问题,但上下文略有不同: -如何为 Rails activerecord 模型中的属性创建默认值?

最佳答案:看你想要什么!

如果您希望每个对象都以一个值开头:使用after_initialize :init

您希望new.html表单在打开页面时具有默认值吗?使用https://stackoverflow.com/a/5127684/1536309

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 You want user to enterXthenY = X+'foo'? 采用:

class Task < ActiveRecord::Base
  before_save :default_values
  def default_values
    self.status ||= 'P'
  end
end
于 2016-12-22T22:05:59.183 回答
5

我也看到人们把它放在他们的迁移中,但我宁愿看到它在模型代码中定义。

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

在 Rails 5 之前,规范的 Rails 方式实际上是在迁移中设置它,db/schema.rb只要想查看数据库为任何模型设置的默认值,只需查看 for 即可。

与@Jeff Perrin 的回答状态相反(有点旧),迁移方法甚至会在 using 时应用默认值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 指南以了解列更改方法中的选项。

不允许在 DB 中使用null: falseNULL 值,并且作为额外的好处,它还会更新,以便所有先前为 null 的预先存在的 DB 记录也设置为该字段的默认值。如果您愿意,您可以在迁移中排除此参数,但我发现它非常方便!

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

class Item < ActiveRecord::Base
  attribute :scheduler_type, :string, default: 'hotseat'
end
于 2017-05-22T14:46:34.197 回答
4

这就是构造函数的用途!覆盖模型的initialize方法。

使用after_initialize方法。

于 2008-11-30T10:07:56.567 回答
4

伙计们,我最终做了以下事情:

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

奇迹般有效!

于 2010-07-27T06:36:21.427 回答
3

这已经回答了很长时间,但是我经常需要默认值并且不希望将它们放入数据库中。我提出了一个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
于 2016-02-14T21:35:41.097 回答
2

在进行复杂的查找时,我遇到了after_initialize给出错误的问题:ActiveModel::MissingAttributeError

例如:

@bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)

.where条件哈希中的“搜索”

所以我最终通过以这种方式覆盖初始化来做到这一点:

def initialize
  super
  default_values
end

private
 def default_values
     self.date_received ||= Date.current
 end

调用是必要的super,以确保ActiveRecord::Base在执行我的自定义代码之前正确初始化对象,即:default_values

于 2011-02-09T10:34:01.957 回答
1
class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end
于 2008-11-30T13:43:00.313 回答
1

after_initialize 方法已弃用,请改用回调。

after_initialize :defaults

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

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

于 2010-10-17T01:59:13.850 回答
1

after_initialize 解决方案的问题在于,无论您是否访问此属性,您都必须为从数据库中查找的每个对象添加 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')。在这种情况下,如果数据库支持,我认为您确实需要在数据库约束中设置默认值。

于 2012-09-08T00:43:17.027 回答
1

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

有一些棘手的场景几乎需要重写该 gem 所做的初始化方法。

例子:

您的 db 默认值为 NULL,您的模型/ruby 定义的默认值为“某个字符串”,但您实际上希望将值设置为 nil,无论出于何种原因:MyModel.new(my_attr: nil)

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

好的,所以不要采用这种||=方法,而是切换到my_attr_changed?...

但是现在想象您的数据库默认值是“某个字符串”,您的模型/ruby 定义的默认值是“其他字符串”,但是在某种情况下,您希望将值设置为“某个字符串”(数据库默认值):MyModel.new(my_attr: 'some_string')

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


由于这些原因,我认为仅使用 after_initialize 钩子无法正确完成此操作。

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

于 2015-09-18T05:02:02.637 回答
0

尽管在大多数情况下设置默认值会让人感到困惑和尴尬,但您也可以使用:default_scope。在这里查看squil 的评论

于 2010-07-21T12:57:32.653 回答
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

于 2011-03-30T21:24:23.197 回答
0

如果该列恰好是“状态”类型的列,并且您的模型适合使用状态机,请考虑使用aasm gem,之后您可以简单地执行

  aasm column: "status" do
    state :available, initial: true
    state :used
    # transitions
  end

它仍然不会初始化未保存记录的值,但它比滚动你自己的init或其他任何东西要干净一些,并且你可以获得 aasm 的其他好处,例如所有状态的范围。

于 2013-11-11T22:30:44.057 回答
0

https://github.com/keithrowell/rails_default_value

class Task < ActiveRecord::Base
  default :status => 'active'
end
于 2016-03-24T12:39:31.780 回答
0

这是我使用的一个解决方案,我有点惊讶尚未添加。

它有两个部分。第一部分是在实际迁移中设置默认值,第二部分是在模型中添加验证以确保存在为真。

add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'

所以你会看到这里已经设置了默认值。现在在验证中,您要确保字符串始终有一个值,所以只需执行

 validates :new_team_signature, presence: true

这将为您设置默认值。(对我来说,我有“欢迎加入团队”),然后它会更进一步,确保该对象始终存在一个值。

希望有帮助!

于 2019-02-08T15:47:26.750 回答
0
# db/schema.rb
create_table :store_listings, force: true do |t|
  t.string :my_string, default: "original default"
end

StoreListing.new.my_string # => "original default"

# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :my_string, :string, default: "new default"
end

StoreListing.new.my_string # => "new default"

class Product < ActiveRecord::Base
  attribute :my_default_proc, :datetime, default: -> { Time.now }
end

Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
sleep 1
Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600
于 2019-12-15T19:29:55.930 回答
0

在开发 Rails 6 应用程序时,我遇到了类似的挑战。

这是我解决它的方法

我有一张Users桌子和一张Roles桌子。表Users属于Roles表。我还有一个从表继承的AdminStudent模型。Users

然后,它要求我在创建用户时为角色设置一个默认值,比如admin具有 id = 的角色1或具有 id =student的角色2

class User::Admin < User
  before_save :default_values

  def default_values
    # set role_id to '1' except if role_id is not empty
    return self.role_id = '1' unless role_id.nil?
  end
end

这意味着admin在数据库中创建/保存用户之前,如果它不为空,则将其role_id设置为默认值。1

return self.role_id = '1' unless role_id.nil? 

是相同的:

return self.role_id = '1' unless self.role_id.nil?

和一样:

self.role_id = '1' if role_id.nil?

但第一个更干净,更精确。

就这样。

我希望这有帮助

于 2020-06-23T15:12:32.880 回答
0

使用这个有一段时间了。

# post.rb
class Post < ApplicationRecord
  attribute :country, :string, default: 'ID'
end
于 2020-10-22T22:26:17.353 回答
0

导轨 6.1+

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

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?
于 2021-09-13T13:07:10.653 回答
-2

在 Rails 3 中使用 default_scope

api文档

ActiveRecord 掩盖了在数据库中定义的默认设置(模式)和在应用程序中完成的默认设置(模型)之间的区别。在初始化期间,它解析数据库模式并记录那里指定的任何默认值。稍后,在创建对象时,它会分配那些模式指定的默认值,而不涉及数据库。

讨论

于 2011-02-03T21:50:12.790 回答
-2

从 api 文档http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html在您的模型中 使用该before_validation方法,它为您提供了为创建和更新调用创建特定初始化的选项,例如在本示例中(再次使用代码来自 api docs 示例)为信用卡初始化 number 字段。您可以轻松调整它以设置您想要的任何值

class CreditCard < ActiveRecord::Base
  # Strip everything but digits, so the user can specify "555 234 34" or
  # "5552-3434" or both will mean "55523434"
  before_validation(:on => :create) do
    self.number = number.gsub(%r[^0-9]/, "") if attribute_present?("number")
  end
end

class Subscription < ActiveRecord::Base
  before_create :record_signup

  private
    def record_signup
      self.signed_up_on = Date.today
    end
end

class Firm < ActiveRecord::Base
  # Destroys the associated clients and people when the firm is destroyed
  before_destroy { |record| Person.destroy_all "firm_id = #{record.id}"   }
  before_destroy { |record| Client.destroy_all "client_of = #{record.id}" }
end

很惊讶他没有在这里被推荐

于 2012-05-31T18:43:21.200 回答