如何在ActiveRecord中设置默认值?

问题描述 投票:401回答:26

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

ruby-on-rails rails-activerecord
26个回答
544
投票

每种可用方法都存在几个问题,但我认为定义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. 如果您只选择模型的列子集(即;在select等查询中使用Person.select(:firstname, :lastname).all),如果MissingAttributeError方法访问未包含在init子句中的列,则会得到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的评论)

4
投票

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

使用after_initialize方法。


4
投票

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

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

奇迹般有效!


4
投票

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


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

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

  1. 它打破了SRP。从ActiveRecord :: Base继承的任何类已经做了很多不同的事情,其中​​主要是数据一致性(验证)和持久性(保存)。使用AR :: Base将业务逻辑(尽管很小)打破了SRP。
  2. 测试速度较慢。如果我想测试我的ORM模型中发生的任何形式的逻辑,我的测试必须初始化Rails才能运行。这在您的应用程序开始时不会出现太多问题,但会累积到您的单元测试需要很长时间才能运行。
  3. 它将以更具体的方式打破SRP。假设我们的业务现在要求我们在项目变为活动时向用户发送电子邮件?现在我们将向Item ORM模型添加电子邮件逻辑,其主要职责是为Item建模。它不应该关心电子邮件逻辑。这是商业副作用的一个例子。这些不属于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上设置活动状态。无需Rails初始化,无SRP中断。当项目初始化变得更高级时(例如,设置状态字段,默认类型等),ItemFactory可以添加此项。如果我们最终得到两种类型的默认值,我们可以创建一个新的BusinesCaseItemFactory来执行此操作。

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


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

3
投票

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

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

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

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

1
投票

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


1
投票

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


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

  before_save{ self.status ||= ACTIVE }
end

1
投票

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

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

例子:

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

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

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

但是现在想象你的db默认是“some string”,你的model / ruby​​定义的默认值是“some some string”,但在某种情况下,你想要将值设置为“some string”(db default):MyModel.new(my_attr: 'some_string')

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


由于这些原因,我不认为只使用after_initialize挂钩就可以正常完成。

同样,我认为“default_value_for”宝石正在采取正确的方法:https://github.com/FooBarWidget/default_value_for


0
投票

虽然在大多数情况下这样做可以设置默认值,但是你也可以使用:default_scope。看看squil's comment here


47
投票

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

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

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


0
投票

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

after_initialize :defaults

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

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


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


0
投票

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

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

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


0
投票

https://github.com/keithrowell/rails_default_value

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

0
投票

这是我用过的一个解决方案,我有点意外还没有添加。

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

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

所以你会在这里看到默认设置已经设置好了。现在在验证中,您希望确保字符串始终存在值,所以就这样做

 validates :new_team_signature, presence: true

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

希望有所帮助!


-1
投票

在rails 3中使用default_scope

api doc

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

discussion


-2
投票

来自api文档http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html在模型中使用before_validation方法,它为您提供了创建和更新调用的特定初始化的选项,例如在此示例中(再次从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

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


43
投票

Rails 5+

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

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

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

attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }

39
投票

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

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

17
投票

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

16
投票

Phusion的家伙有一些不错的plugin


8
投票

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

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

请参阅the ActiveRecord::Base documentationmore from StackOverflow on using self中的“覆盖默认访问器”。


8
投票

我用的是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"

7
投票

类似的问题,但都有不同的背景: - How do I create a default value for attributes in Rails activerecord's model?

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

如果您希望每个对象都以值开头:使用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
© www.soinside.com 2019 - 2024. All rights reserved.