ruby-on-rails - view教程 - ruby on rails教程




我如何在ActiveRecord中设置默认值? (17)

我如何在ActiveRecord中设置默认值?

我看到一篇来自Pratik的文章,描述了一个丑陋,复杂的代码块: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model : 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模型中字段的默认值?


我也看到有人将其用于迁移,但我更希望看到它在模型代码中定义。

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

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

与@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

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

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

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

Phusion的人有一些很好的plugin


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

after_initialize :defaults

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

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


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


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

使用after_initialize方法。


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

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

只需执行以下操作即可改进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

在Rails 5+中,您可以在模型中使用attribute方法,例如:

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

在执行复杂发现时,遇到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


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

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

它仍然不会初始化未保存记录的值,但是它比使用init或其他任何方式来滚动自己的内容要简单一些,并且还可以获得aasm的其他好处,例如所有状态的范围。


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

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

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


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

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

有一些棘手的场景需要重写初始化方法,这是宝石所做的。

例子:

你的数据库默认值是NULL,你的模型/ ruby​​定义的默认值是“some string”,但你实际上将值设置为nil: MyModel.new(my_attr: nil)

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

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

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

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

由于这些原因,我认为这只能通过一个after_initialize钩子来完成。

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


每种可用方法都有几个问题,但我相信定义一个after_initialize回调是出于以下原因的方式:

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

DEPRECATION WARNING: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 MissingAttributeError )这样的查询中使用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的评论)


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

最佳答案: 取决于你想要什么!

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

你希望new.html表单在打开页面时具有默认值? 使用https://.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

这已经回答了很长时间,但我经常需要默认值,并且不希望将它们放入数据库中。 我创建一个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

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

  before_save{ self.status ||= ACTIVE }
end




rails-activerecord