提问



如何在ActiveRecord中设置默认值?


我看到Pratik的一篇文章描述了一段丑陋,复杂的代码:http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model[73]


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

最佳参考


每种可用方法都存在几个问题,但我认为定义after_initialize回调是出于以下原因的方法:



  1. default_scope将初始化新模型的值,但随后将成为您找到模型的范围。如果您只想将某些数字初始化为0,那么不就是您想要的。

  2. 定义迁移中的默认值也可以部分工作......正如已经提到的那样,当你只是调用Model.new时,这将不。

  3. 覆盖initialize可以工作,但不要忘记拨打super!

  4. 使用像phusion这样的插件有点荒谬。这是红宝石,我们真的需要一个插件来初始化一些默认值吗?

  5. 从Rails 3开始,覆盖after_initialize 已弃用。当我在rails 3.0.3中覆盖after_initialize时,我在控制台中收到以下警告:




  弃用警告:不推荐使用Base#after_initialize,请改用Base.after_initialize:方法。 (来自/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. 如果您只选择模型的列子集(即;在Person.select(:firstname, :lastname).all等查询中使用select,如果您的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的评论)


其它参考1


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


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


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

其它参考2


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


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回调,但是我已经通过确认该对象是否已经持久化表明它不是新的来做到了。


看过布拉德·默里的回答,如果条件转移到回调请求,这甚至更清晰:


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

其它参考3


在Rails 5+中,您可以在模型中使用属性方法,例如:[74]


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

其它参考4


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


after_initialize :some_method_goes_here, :if => :new_record?


如果您的初始化代码需要处理关联,这会带来非常重要的好处,因为如果您在不包含关联的情况下读取初始记录,则以下代码会触发细微的n + 1。


class Account

  has_one :config
  after_initialize :init_config

  def init_config
    self.config ||= build_config
  end

end

其它参考5


Phusion的家伙有一些不错的插件。[75]

其它参考6


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


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


请参阅ActiveRecord :: Base文档中的覆盖默认访问器以及使用self时StackOverflow中的更多内容。[76]

其它参考7


我使用attribute-defaults宝石[78]


从文档:
运行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


类似的问题,但都有不同的背景:
- 如何在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
您希望用户输入X然后再输入Y = X+'foo'?使用:


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

其它参考9


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


使用after_initialize方法。

其它参考10


苏家伙,我最后做了以下事情:


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


奇迹般有效!

其它参考11


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





这里的一个问题是记录的默认值是否为业务逻辑。如果是的话,我会谨慎地把它放在ORM模型中。由于字段ryw提及Activity,这听起来像业务逻辑。例如。用户是Activity的。


为什么我要谨慎将业务问题放在ORM模型中?



  1. 打破了SRP。从ActiveRecord :: Base继承的任何类已经在做不同事物的 lot ,其中主要是数据一致性(验证)和持久性(保存)。使用AR :: Base将业务逻辑(尽管很小)打破SRP。[81]

  2. 测试速度较慢。如果我想测试我的ORM模型中发生的任何形式的逻辑,我的测试必须初始化Rails才能运行。这在您的应用程序开始时不会出现太多问题,但会累积,直到您的单元测试需要很长时间才能运行。

  3. 它将以更具体的方式打破SRP。说我们的业务现在要求我们在项目变为Activity时向用户发送电子邮件?现在我们正在向Item ORM模型添加电子邮件逻辑,其主要职责是对项目建模。它不应该关心电子邮件逻辑。这是一个案例业务副作用。这些不属于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


然后,有了这个,创建一个新项目的方法将是


SellableItemFactory.new


而且我的测试现在可以简单地验证ItemFactory如果没有值则在Item上设置为Activity状态。无需Rails初始化,无SRP中断。当项目初始化变得更高级时(例如,设置状态字段,默认类型等),ItemFactory可以添加此项。如果我们最终得到两种类型的默认值,我们可以创建一个新的BusinesCaseItemFactory来执行此操作。


注意:在这里使用依赖注入以允许工厂构建许多Activity的东西也是有益的,但为了简单起见,我把它留了下来。这是: self.new(klass=Item,attributes={})

其它参考12


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

其它参考13



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

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



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


与@Jeff Perrin回答的状态(有点旧)相反,由于某些Rails魔法,迁移方法甚至会在使用Model.new时应用默认值。已验证在Rails 4.1.16中工作。


最简单的事情往往是最好的。减少知识债务和代码库中潜在的混淆点。它只是有效。


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


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


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


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

其它参考14


after_initialize解决方案的问题在于,无论是否访问此属性,都必须向从数据库中查找的每个对象添加after_initialize。我建议采用一种懒惰的方法。


属性方法(getters)当然是方法本身,因此您可以覆盖它们并提供默认值。就像是:


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

其它参考15


在执行复杂查找时,我遇到了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

其它参考16


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

  before_save{ self.status ||= ACTIVE }
end

其它参考17


我强烈建议使用default_value_forgem:https://github.com/FooBarWidget/default_value_for [82]


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


例子:


您的db默认值为NULL,您的model/ruby​​定义的默认值为some string,但实际上想要将值设置为nil,无论出于何种原因:MyModel.new(my_attr: nil)


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


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


但是现在想象你的db默认是some string,你的模型/ruby​​定义的默认值是some some string,但在某种情况下,你想要来设置某些字符串的值(db默认值):MyModel.new(my_attr: 'some_string')


这将导致my_attr_changed? false ,因为该值与db default相匹配,而db default又会触发ruby定义的默认代码并将值设置为其他字符串 - 再次,不是你想要的。





出于这些原因,我认为只需使用after_initialize钩子就可以完成。


同样,我认为default_value_forgem正在采取正确的方法:https://github.com/FooBarWidget/default_value_for [83]

其它参考18


虽然在大多数情况下这样做可以设置默认值,但是也可以使用:default_scope。在这里查看squil的评论。[84]

其它参考19


不推荐使用after_initialize方法,而是使用回调。


after_initialize :defaults

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


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

其它参考20


我发现使用验证方法可以对设置默认值进行大量控制。您甚至可以为更新设置默认值(或验证失败)。如果您真的想要,甚至可以为插入与更新设置不同的默认值。
请注意,在调用#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[85]

其它参考21


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


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


它仍然没有初始化未保存记录的值,但它比用init或其他任何东西滚动自己更清晰,并且你获得了aasm的其他好处,例如所有状态的范围。

其它参考22


https://github.com/keithrowell/rails_default_value[87]


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

其它参考23


在rails 3中使用default_scope


api doc [88]


ActiveRecord模糊了数据库(模式)中定义的默认值和应用程序(模型)中的默认值之间的差异。在初始化期间,它解析数据库模式并记录其中指定的任何默认值。稍后,在创建对象时,它会分配这些模式指定的默认值,而不会触及数据库。


讨论

其它参考24


来自api docs http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
使用模型中的before_validation方法,它为您提供了为创建和更新调用创建特定初始化的选项
例如在此示例中(再次从api docs示例中获取代码),数字字段初始化为信用卡。你可以很容易地调整它来设置你想要的任何值[90]


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


感到惊讶的是,这里没有他的建议