如何在ActiveRecord中设置默认值?


417

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


好像您自己回答了这个问题,有两种不同的方式:)
Adam Byrtek,

19
请注意,“标准”红宝石成语“self.status =有效,除非self.status”是“self.status || = ACTIVE”
迈克·伍德豪斯

1
Jeff Perrin的答案比当前标记为接受的答案要好得多。default_scope是设置默认值的不可接受的解决方案,因为它具有巨大的副作用,也可以更改查询的行为。
劳伦斯


2
鉴于所有对此问题的支持,我想说Ruby需要ActiveRecord的setDefaultValue方法
spartikus 2014年

Answers:


557

每个可用的方法都有一些问题,但是我认为定义after_initialize回调是解决问题的方法,原因如下:

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

弃用警告:不建议使用Base#after_initialize,请改用Base.after_initialize:method。(从/ 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的注释)


7
这无疑是实现此目标的最佳方法。这真是奇怪和不幸。建立模型属性默认值时明智的首选方法似乎是Rails应该已经内置的东西。唯一(可靠)的方法overriding initialize似乎真的很复杂,因为应该清楚并定义清楚。在搜索此处之前,我花了数小时在文档中进行爬网,因为我认为此功能已经在某个地方了,而我对此并不了解。
seaneshbaugh

106
关于此的一个注意事项-如果您具有要默认的布尔值字段,请不要这样做self.bool_field ||= true,因为即使您将字段明确初始化为false ,也将强制该字段为true。相反self.bool_field = true if self.bool_field.nil?
保罗·罗素

2
关于第2点,Model.new实际上与迁移中定义的默认值一起工作(仅对我而言),或更确切地说与表列的默认值一起工作。但是我认识到Jeff基于after_initialize回调的方法可能是最好的方法。只是一个问题:它适用于脏的但尚未保存的对象吗?在您的示例中,Person.new.number_was将返回0.0吗?
洛朗·法西

21
使用此方法时,请注意结合使用活动记录选择特定列。在这种情况下,只能在对象中找到查询中指定的属性,并且初始化代码将引发MissingAttributeError。您可以添加额外的检查,如下所示: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+-对于更早的版本,请使用self.attributes.has_key?,并且您需要输入字符串而不是符号。
克里夫·达林

6
使用关联执行此操作将渴望在查找时加载这些关联。开始initializereturn if !new_record?以避免出现性能问题。
凯尔·梅西

68

滑轨5+

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

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

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

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

3
啊,这是我要找的宝石!默认也可以使用proc,例如,默认:-> {Time.current.to_date}
schpet

1
确保将类型指定为第二个参数,否则将为类型,Value并且不会进行任何类型转换。
空值

令我高兴的是,它也适用于store_accessor,例如,store_accessor :my_jsonb_column, :locale您可以定义attribute :locale, :string, default: 'en'
Ryan Romanchuk

哦,那太好了,我需要使用默认值才能以表格形式显示,并且效果很好。谢谢卢卡斯。
保罗·沃森

仍然可以将它们设置为nil。如果它们不能是nilDB not null+ DB default + github.com/sshaw/keep_defaults是我的经验之路
sshaw

47

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

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

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


3
另一个问题是当您需要外键的默认值时。您不能将ID值硬编码到外键字段中,因为在不同的DB上,ID可能不同。
shmichael

2
另一个问题是,这样就不能初始化非持久性访问器(不是db列的属性)。
维克多·特隆(ViktorTrón)2011年

2
另一个问题是,您不一定必须在一个地方看到所有默认值。它们可能因不同的迁移而分散。
Declan

8
declan,有db / schema.rb
本杰明·阿特金

6
我想为以后的读者提一提:至少从我的阅读来看,这与AR的原理背道而驰。模型的逻辑应放在模型类之内,并且数据库应尽可能地无知。我的默认值构成有关模型的特定逻辑。
darethas 2013年

40

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

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
这是非常重要的一点。我必须想象,在大多数情况下,仅在保存新记录之前设置记录的默认值,而不是在加载保存的记录时设置默认值。
拉塞尔·席尔瓦

兄弟,你救了我的一天。
stephanfriedrich 2014年

2
怎么:before_create
富兰克林于

:before_create如何处理新的和保存的呼叫?我想先检查一下,然后再真正了解它。
约瑟夫·罗德

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家伙为此提供了一些不错的插件


请注意,此插件允许:default架构迁移中的值与一起使用Model.new
jchook 2014年

:defaultModel.newJeff在他的帖子中所说的相反,我可以在迁移中获得与“一起工作” 的价值。验证可在Rails 4.1.16中工作。
Magne


8

我用attribute-defaults宝石

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

类似的问题,但上下文都有一些不同:- 如何在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 

如果您希望每个对象都有一个根据用户输入计算得出的值: usebefore_save :default_values 您希望用户输入X,然后输入Y = X+'foo'?采用:

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

4

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

使用after_initialize方法。


2
通常,您是正确的,但您不应覆盖ActiveRecord模型中的初始化,因为它可能并不总是被调用。您应该改用该after_initialize方法。
路加·雷德帕特

仅使用default_scope设置默认值是错误的。after_initialize是正确的答案。
joaomilho 2011年

4

大家好,我最后做了以下工作:

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

奇迹般有效!


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/schema.rb只要想查看DB为任何模型设置的默认值,就查找for。

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

或者,要更改列而不创建新列,请执行以下任一操作:

class AddStatusToItem < ActiveRecord::Migration
  def change
    change_column_default :items, :scheduler_type, "hotseat"
  end
end

甚至更好:

class AddStatusToItem < ActiveRecord::Migration
  def change
    change_column :items, :scheduler_type, :string, default: "hotseat"
  end
end

查看官方RoR指南以了解列更改方法中的选项。

null: false不允许使用空值在DB,并且,作为一个额外的好处,同时也更新,所以这在以前是空的所有预先存在的数据库记录被设定的默认值这个领域也是如此。您可以根据需要在迁移中排除此参数,但我发现它非常方便!

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

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

1

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


在我的情况下,此解决方案和建议的替代方法不起作用:我有一个STI类层次结构,其中只有一个类具有该属性,以及将在数据库查询条件中使用的对应列。
cmoran92

1

在执行复杂的查找时,我遇到了after_initialize给出ActiveModel::MissingAttributeError错误的问题:

例如:

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

条件中的“搜索” .where为条件哈希

因此,我最终通过以这种方式覆盖initialize来完成此操作:

def initialize
  super
  default_values
end

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

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


我喜欢。我需要def initialize(*); super; default_values; end在Rails 5.2.0中进行。另外,即使在.attributes哈希中,也可以使用默认值。
spyle

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

  before_save{ self.status ||= ACTIVE }
end

2
嗯...起初似乎很巧妙,但是经过一番思考,我发现了一些问题。首先,所有默认值都不是单一的,而是分散在整个类中(想象搜索或更改它们)。其次,最糟糕的是,您以后不能再输入null值(甚至是错误的值!)。
paradoja

为什么需要将空值设置为默认值?您无需使用AR即可直接使用它。至于使用布尔型列时为false,那您是对的,这不是最好的方法。
Mike Breen

我不能代表其他人的编码习惯,因为我不会将我的getter / setter分散在类文件中,所以我没有遇到任何问题。另外,任何现代文本编辑器都应该使导航到方法(textmate中的shift-cmd-t)变得容易。
Mike Breen

@paradoja-我把它收回了,现在我也看到它在使用null的地方也出现了问题。不一定要使用null作为默认值,但是如果您确实想在某个时候将值更改为null。很好@paradoja,谢谢。
Mike Breen

我使用此方法,因为它可以很好地与动态生成的属性配合使用。
戴尔·坎贝尔,2010年


0

不建议使用after_initialize方法,请改用回调。

after_initialize :defaults

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

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


4
在Rails 3:after_initialize方法NOT弃用。实际上,您给出了IS不推荐使用宏样式回调示例。详细信息:guides.rubyonrails.org/...
Zabba

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方法,可能存在性能问题,因为:find:http ://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find返回的每个对象也会调用after_initialize


验证不是仅在保存之前进行吗?如果要在保存之前显示默认值怎么办?
nurettin

@nurettin这是一个很好的观点,我可以理解为什么有时您会想要这样做,但是OP没有提到这一点。您必须自己决定是否要在每个实例上设置默认值的开销,即使未保存默认值也是如此。另一种方法是保留一个虚拟对象,以供new操作重用。
开尔文

0

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

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

它仍然不会初始化未保存记录的值,但是它比使用自己的记录init或其他内容要干净一些,并且您还获得了aasm的其他好处,例如所有状态的作用域。



0

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

在某些棘手的情况下,几乎需要重写gem方法。

例子:

您的数据库默认值为NULL,您的模型/红宝石定义的默认值为“某个字符串”,但是出于任何原因,您实际上都想将值设置为nil:MyModel.new(my_attr: nil)

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

好的,因此您无需采用这种||=方法,而是切换到my_attr_changed?...

但是现在假设您的数据库默认值为“某些字符串”,您的模型/红宝石定义的默认值为“某些其他字符串”,但是在特定情况下,您想要将该值设置为“某些字符串”(db默认):MyModel.new(my_attr: 'some_string')

这将导致my_attr_changed?错误,因为值相匹配的数据库默认情况下,这反过来会触发你的Ruby定义的默认代码并将其值设置为“其他一些字符串” -再次,不是你想要的是什么。


由于这些原因,我认为仅使用after_initialize钩子无法正确实现。

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


0

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

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

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

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

 validates :new_team_signature, presence: true

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

希望有帮助!


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

-2

在rails 3中使用default_scope

API文档

ActiveRecord掩盖了数据库中定义的默认设置(模式)与应用程序中的默认设置(模型)之间的差异。在初始化期间,它将解析数据库架构并记录在此指定的任何默认值。以后,在创建对象时,它会分配那些模式指定的默认值,而不会影响数据库。

讨论区


如果您使用meta_where,则由于错误,default_scope可能无法将默认值分配给新的AR对象。
维克多·特隆(ViktorTrón)2011年


3
不要使用default_scope。这将使您所有的查询都将此条件添加到您设置的字段中。几乎没有您想要的东西。
布拉德(Brad)2012年

@brad,您提到的有趣,我完全同意,这很邪恶:)。请参阅stackoverflow.com/questions/10680845/…中的评论。
ViktorTrón2012年

-3

从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

惊讶的是这里没有建议他


在对象准备就绪之前,before_validation不会设置默认值。如果该过程需要在保留之前读取默认值,则这些值将无法准备好。
mmell

无论如何,您永远不会在验证检查期间设置默认值。它甚至都不是Hack。在初始化期间执行
Sachin
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.