我怎样才能在ActiveRecord中设置默认值?

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

我已经看到下面的例子search:

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

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

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

有没有规范的方式来设置ActiveRecord模型中的字段的默认值?

每种可用方法都有几个问题,但是我认为定义一个after_initializecallback是出于以下原因:

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

DEPRECATION WARNING:Base#after_initialize已被弃用,请改用Base.after_initialize:方法。 (从/ Users / me / myapp / app / models / my_model:15中调用)

因此,我会说写一个after_initializecallback,它可以让你默认的属性,除了让你设置像这样的关联的默认值:

  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模型的一个列的子集(即;使用select像查询Person.select(:firstname, :lastname).all MissingAttributeError ),你会得到一个MissingAttributeError如果你的init方法访问一个列MissingAttributeError '不包括在select子句中。 你可以像这样防范这种情况:

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

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

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

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

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

 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,但我不希望它被应用到只有那些新的或创build的对象。 我认为这个明显的用例没有提供after_newcallback,但是我已经通过确认对象是否已经被保留来表明它不是新的。

看到Brad Murray的回答,如果条件转移到callback请求,这个更清晰:

 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 

Phusion的人有一些很好的插件 。

after_initializecallback模式可以通过简单的做以下改进

 after_initialize :some_method_goes_here, :if => :new_record? 

如果你的初始化代码需要处理关联,这将有一个非平凡的好处,因为如果你读取了初始logging而不包括关联的话,下面的代码会触发一个微妙的n + 1。

 class Account has_one :config after_initialize :init_config def init_config self.config ||= build_config end end 

在Rails 5+中,可以使用模型中的属性方法,例如:

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

我使用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" 

这是什么构造函数是! 覆盖模型的initialize方法。

使用after_initialize方法。

Sup家伙,我最终做了以下几点:

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

奇迹般有效!

比提出的答案更好,更清洁的方法是覆盖访问器,如下所示:

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

请参阅ActiveRecord :: Base文档中的 “覆盖默认访问器”,以及使用self自StackOverflow获取更多内容 。

首先要做的事情是:我不反对杰夫的回答。 这是有道理的,当你的应用程序是小的,你的逻辑简单。 我在这里试图了解在构build和维护更大的应用程序时如何成为问题。 我不build议在构build一个小的东西的时候先使用这个方法,但是要把它作为一种替代方法来记住:


这里的一个问题是这个logging上的默认值是否是业务逻辑。 如果是这样,我会谨慎的把它放在ORM模型中。 由于领域ryw提到是积极的 ,这听起来像商业逻辑。 例如,用户处于活动状态。

为什么我会谨慎把商业关注放在ORM模型中?

  1. 它打破了SRP 。 从ActiveRecord :: Baseinheritance的任何类已经做了很多不同的事情,其中​​主要是数据一致性(validation)和持久性(保存)。 把业务逻辑放在很小的地方,用AR :: Base打破SRP。

  2. testing速度较慢。 如果我想testing在我的ORM模型中发生的任何forms的逻辑,我的testing必须初始化Rails才能运行。 在你的应用程序开始的时候,这不会太成问题,但会积累起来,直到你的unit testing需要很长时间才能运行。

  3. 它将会更多地破坏SRP,并以具体的方式。 现在说我们的业务需要我们发送电子邮件用户时有项目变得活跃? 现在我们将电子邮件逻辑添加到项目ORM模型,其主要职责是对项目build模。 它不应该关心电子邮件逻辑。 这是商业副作用的情况 。 这些不属于ORM模型。

  4. 很难多样化。 我已经看到成熟的Rails应用程序,像数据库支持的init_type:string字段,其唯一目的是控制初始化逻辑。 这是污染数据库来解决结构性问题。 我相信有更好的办法。

PORO的方式:虽然这是更多的代码,它可以让你保持你的ORM模型和业务逻辑分开。 这里的代码是简化的,但应该显示的想法:

 class SellableItemFactory def self.new(attributes = {}) record = Item.new(attributes) record.active = true if record.active.nil? record end end 

然后,这个地方,创build一个新的项目的方式是

 SellableItemFactory.new 

而且我的testing现在可以简单地validationItemFactory如果它没有值,就会在Item上设置活动。 没有Rails初始化需要,没有SRP打破。 当Item初始化变得更高级时(例如设置一个状态字段,一个默认的types等),ItemFactory可以添加这个。 如果我们最终得到两种types的默认值,我们可以创build一个新的BusinesCaseItemFactory来完成这个工作。

注意:在这里使用dependency injection也是有好处的,这样工厂就可以创build许多活动的东西,但为了简单起见,我把它放在了外面。 这里是: self.new(klass = Item,attributes = {})

类似的问题,但所有的上下文略有不同: – 如何创buildRails的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 

如果你想让每个对象有一个从用户input计算出来的值:use before_save :default_values你希望用户inputX ,然后Y = X+'foo' ? 使用:

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

after_initialize解决scheme的问题在于,无论您是否访问该属性,都必须为查find的每个对象添加一个after_initialize。 我build议一个懒惰的方法。

属性方法(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')。 在这种情况下,我认为如果数据库支持的话,你真的需要在你的数据库约束中设置默认值。

 class Item < ActiveRecord::Base def status self[:status] or ACTIVE end before_save{ self.status ||= ACTIVE } end 

这已经回答了很长时间,但我需要经常使用默认值,而不是将它们放在数据库中。 我创build一个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 

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

有没有规范的方式来设置ActiveRecord模型中的字段的默认值?

在Rails 5之前,规范的Rails方法实际上是在迁移中设置它,只要查看db/schema.rb可以随时查看DB为任何模型设置的默认值。

与@Jeff Perrin的答案(这有点旧)相反,迁移方法甚至会使用Model.new时的默认值,这是由于某些Rails魔术。 validation在Rails 4.1.16中工作。

最简单的事情往往是最好的。 代码库中知识欠债和潜在的混淆点较less。 而且“正常工作”。

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

null: false不允许在数据库中使用NULL值,另外还会更新所有预先存在的数据库logging,并使用该字段的默认值进行设置。 如果您愿意,您可以在迁移中排除此参数,但我发现它非常方便!

Rails 5+的规范方式就像@Lucas Caton所说:

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

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

不build议使用after_initialize方法,而是使用callback。

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

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

我发现使用validation方法提供了对设置默认值的很多控制。 您甚至可以设置更新的默认值(或未通过validation)。 如果你真的想要的话,甚至可以为插入和更新设置一个不同的默认值。 请注意,默认不会被设置,直到#有效? 叫做。

 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

在执行复杂发现时,遇到after_initializeActiveModel::MissingAttributeError错误的问题:

例如:

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

.where中的“search”是条件的散列

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

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

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

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

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

它仍然没有初始化未保存logging的值,但是比使用init或其他types的自定义滚动条更简洁一点,而且还可以获得aasm的其他好处,例如所有状态的作用域。

https://github.com/keithrowell/rails_default_value

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

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

有一些棘手的情况几乎需要重写初始化方法,这是gem。

例子:

你的数据库默认是NULL,你的模型/ ruby​​定义的默认是“一些string”,但你真的设置为零无论出于什么原因: MyModel.new(my_attr: nil)

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

好的,所以不要采取||=方法,而是切换到my_attr_changed?

现在想象你的数据库默认是“一些string”,你的模型/ruby定义的默认是“其他string”,但在某些情况下,你设置为“一些string”(数据库默认值): MyModel.new(my_attr: 'some_string')

这将导致my_attr_changed? 因为该值匹配数据库默认,这反过来将启动您的ruby定义的默认代码,并将值设置为“其他string” – 再次,而不是你想要的。


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

再次,我认为“default_value_for”gem正在采取正确的做法: https : //github.com/FooBarWidget/default_value_for

在rails 3中使用default_scope

api文档

ActiveRecord模糊了数据库(模式)中定义的默认值与应用程序(模型)中的默认值之间的区别。 在初始化过程中,它分析数据库模式并logging指定的默认值。 稍后,在创build对象时,它将分配这些模式指定的默认值而不触及数据库。

讨论

从api文档http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html在你的模型中使用;before_validation方法,它给你创build创build和更新调用的特定初始化的选项,例如在这个例子中(再次代码从api docs例子中得到)号码字段被初始化为一张信用卡。 你可以很容易地适应这个设置任何你想要的值

 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 

惊讶的是,他没有在这里build议