如何在ActiveRecord中设置默认值?

时间:2008-11-30 06:27:29

标签: ruby-on-rails rails-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

我已经看到以下示例谷歌搜索:

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

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

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

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

29 个答案:

答案 0 :(得分:542)

每种可用方法都存在一些问题,但我认为定义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时,我在控制台中收到以下警告:
  6.   

    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. 如果您只选择模型的列子集(即;在select等查询中使用Person.select(:firstname, :lastname).all),则MissingAttributeError如果您{ {1}}方法访问未包含在init子句中的列。你可以这样防范这种情况:

      select

      和布尔列......

      self.number ||= 0.0 if self.has_attribute? :number

      另请注意,Rails 3.2之前的语法不同(请参阅下面的Cliff Darling的评论)

答案 1 :(得分:46)

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

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

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

答案 2 :(得分:42)

Rails 5 +

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

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

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

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

答案 3 :(得分: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

答案 4 :(得分: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

答案 5 :(得分:16)

The Phusion的家伙有一些不错的plugin

答案 6 :(得分:8)

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

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

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

答案 7 :(得分: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"

答案 8 :(得分: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

答案 9 :(得分:4)

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

使用after_initialize方法。

答案 10 :(得分:4)

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

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

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

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

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

  3. 它将以更具体的方式打破SRP。假设我们的业务现在要求我们在项目变为活动时向用户发送电子邮件?现在我们将向Item ORM模型添加电子邮件逻辑,其主要职责是为Item建模。它不应该关心电子邮件逻辑。这是业务副作用的案例。这些不属于ORM模型。

  4. 很难实现多样化。我见过成熟的Rails应用程序,例如数据库支持的init_type:string字段,其唯一目的是控制初始化逻辑。这会污染数据库以修复结构问题。我相信有更好的方法。

  5. 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 = {})

答案 11 :(得分:4)

Sup人,我最终做了以下事情:

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

像魅力一样!

答案 12 :(得分: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

答案 13 :(得分: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

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

答案 15 :(得分: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

答案 16 :(得分:1)

我强烈建议使用&#34; default_value_for&#34; gem:https://github.com/FooBarWidget/default_value_for

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

示例:

您的db默认值为NULL,您的模型/ ruby​​定义的默认值为&#34;某些字符串&#34;,但您实际上希望将值设置为nil,无论出于何种原因:{{ 1}}

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

好的,所以不要采用MyModel.new(my_attr: nil)方法,而是切换到||= ...

但是现在想象你的db默认是&#34;某个字符串&#34;,你的模型/ ruby​​定义的默认值是&#34;其他一些字符串&#34;,但是在某个字符串下方案,你想要将值设置为&#34;某些字符串&#34; (db默认值):my_attr_changed?

这将导致MyModel.new(my_attr: 'some_string') false ,因为该值与db default相匹配,而db default又会触发ruby定义的默认代码并将值设置为&#34; some其他字符串&#34; - 再次,不是你想要的。

出于这些原因,我不认为只需使用after_initialize钩子就可以实现这一点。

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

答案 17 :(得分:1)

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

  before_save{ self.status ||= ACTIVE }
end

答案 18 :(得分:0)

使用了一段时间。

# post.rb
class Post < ApplicationRecord
  attribute :country, :string, default: 'ID'
end

答案 19 :(得分:0)

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

这是我的解决方法

我有一个Users表和一个Roles表。 Users表属于Roles表。我也有一个Admin表中继承的StudentUsers模型。

然后,无论何时创建用户,都要求我为该角色设置一个默认值,例如admin角色的ID = 1student角色的ID = 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?

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

仅此而已。

我希望这会有所帮助

答案 20 :(得分: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

答案 21 :(得分:0)

这是我使用过的一种解决方案,令我有些惊讶,但尚未添加。

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

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

因此,您将在此处看到默认设置。现在,在验证中,您要确保字符串始终有一个值,所以只需

 validates :new_team_signature, presence: true

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

希望有帮助!

答案 22 :(得分:0)

虽然在大多数情况下,设置默认值会让人感到困惑和尴尬,但您也可以使用:default_scope。查看squil's comment here

答案 23 :(得分:0)

https://github.com/keithrowell/rails_default_value

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

答案 24 :(得分:0)

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

after_initialize :defaults

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

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

答案 25 :(得分:0)

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

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

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

答案 26 :(得分: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

答案 27 :(得分:-1)

在rails 3中使用default_scope

api doc

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

discussion

答案 28 :(得分:-2)

来自api docs 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

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