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模型?

229267 次浏览

Phusion有一些很好的插件

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

使用after_initialize方法。

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


before_save{ self.status ||= ACTIVE }
end

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

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

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

尽管在大多数情况下这样设置默认值是令人困惑和尴尬的,但你也可以使用:default_scope。看看下面是Squil的评论

伙计们,我最后做了以下事情:

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

效果好极了!

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

after_initialize :defaults


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

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

在rails 3中使用default_scope

api doc

ActiveRecord模糊了在数据库(模式)中定义的默认值和在应用程序(模型)中完成的默认值之间的区别。在初始化过程中,它解析数据库模式并记录其中指定的任何默认值。稍后,在创建对象时,它将分配那些模式指定的默认值,而不涉及数据库。

讨论< a href = " https://stackoverflow.com/questions/3975161/rails3-default-scope-and-default-column-value-in-migration " > < / >

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

例如:

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

.where中的"search"是条件哈希

所以我用这种方式重写了初始化:

def initialize
super
default_values
end


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

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

每个可用的方法都有几个问题,但我认为定义after_initialize回调是正确的方法,原因如下:

  1. default_scope将初始化新模型的值,但随后它将成为您找到模型的作用域。如果你只是想将一些数字初始化为0,那么这就是你想要的
  2. 在您的迁移中定义默认值在某些时候也是有效的……正如已经提到的,当你调用Model.new时,将工作。
  3. 重写initialize可以工作,但不要忘记调用super!
  4. 使用phusion这样的插件有点荒谬。这是ruby,我们真的需要一个插件来初始化一些默认值吗?
  5. 覆盖Rails 3的after_initialize 被弃用。当我在rails 3.0.3中覆盖after_initialize时,我在控制台中得到以下警告:

弃用警告:Base#after_initialize已弃用,请使用Base。After_initialize:方法代替。(调用from /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方法访问了一个没有包含在select子句中的列,你将得到一个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之前,语法是不同的(参见下面Cliff Darling的评论)

我发现使用验证方法对设置默认值提供了很多控制。您甚至可以为更新设置默认值(或失败验证)。如果需要的话,您甚至可以为插入和更新设置不同的默认值。 注意,默认值不会设置,直到#valid?被称为. < / p >
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 < / p >

从api文档http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html 在你的模型中使用before_validation方法,它为你提供了为create和update调用创建特定初始化的选项 例如,在这个例子中(同样摘自API文档的例子),数字字段被初始化为信用卡。你可以很容易地调整它来设置任何你想要的值

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

令人惊讶的是,这里没有提到他

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

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解决方案的问题是,无论是否访问该属性,都必须向从DB中查找的每个对象添加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')。在这种情况下,我认为您确实需要在数据库约束中设置默认值,如果数据库支持它的话。

可以通过简单地执行以下操作来改进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

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

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

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

它仍然没有初始化未保存记录的值,但它比使用init或其他方法来滚动自己的记录要干净一些,并且您可以获得aasm的其他好处,例如为所有状态设置范围。

一个比建议的答案更好/更干净的潜在方法是覆盖访问器,像这样:

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

请参见ActiveRecord::Base文档关于使用self的更多信息中的“覆盖默认访问器”。

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

有一些棘手的场景需要重写initialize方法,这个gem就是这样做的。

例子:

你的db默认值是NULL,你的模型/ruby定义的默认值是“一些字符串”,但你实际上想要设置值为nil,无论出于什么原因

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

好吧,所以你不采用||=方法,而是切换到my_attr_changed?

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

这将导致my_attr_changed?,因为该值与db默认值匹配,而db默认值将触发ruby定义的默认代码,并将值设置为“其他字符串”——同样,这不是你想要的。


由于这些原因,我不认为这可以通过一个after_initialize钩子正确地完成。

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

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

https://github.com/keithrowell/rails_default_value

class Task < ActiveRecord::Base
default :status => 'active'
end
类似的问题,但上下文略有不同: ——如何为Rails activerrecord 's模型中的属性创建默认值? < / p >

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

如果你想让每个对象以值开始:使用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'?用途:< / p >

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

Rails 5 +

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

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

你也可以传递一个lambda给default参数。例子:

attribute :uuid, :string, default: -> { SecureRandom.uuid }

第二个参数是类型,它也可以是一个自定义类型类实例,例如:

attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }
我也见过人们在迁徙中使用这种方法,但我宁愿看到它

.在模型代码中定义

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

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

与@Jeff Perrin的回答相反(这有点老了),迁移方法甚至在使用Model.new时应用默认值,这是由于一些Rails魔法。在Rails 4.1.16中验证。

最简单的东西往往是最好的。较少的知识负债和代码库中的潜在混淆点。而且它‘just works’。

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中使用NULL值,而且,作为一个额外的好处,它还会进行更新,以便所有先前为NULL的DB记录也会使用该字段的默认值设置。如果您愿意,您可以在迁移中排除这个参数,但我发现它非常方便!

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

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

这里有一个我用过的解决方案,我有点惊讶还没有加入。

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

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

您将看到这里已经设置了默认值。现在在验证中,你想要确保字符串总是有一个值,那么就这样做吧

 validates :new_team_signature, presence: true

这将为您设置默认值。(对我来说,我有“Welcome to the Team”),然后它将进一步确保该对象始终存在一个值。

希望有帮助!

# 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

在开发Rails 6应用程序时,我也遇到过类似的挑战。

下面是我的解决方法:

我有一个Users表和一个Roles表。Users表属于Roles表。我还有一个继承自Users表的AdminStudent模型。

然后,它要求我在创建用户时为角色设置默认值,比如id = 1admin角色或id = 2student角色。

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?

但是第一种方法更简洁,也更精确。

这是所有。

我希望这对你们有帮助

我已经用了一段时间了。

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

Rails 6.1 +

你现在可以在你的模型上使用属性方法而不设置类型。

attribute :status, default: ACTIVE

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

注意,将默认值提供给attribute不能引用类的实例(lambda将在类的上下文中执行,而不是在实例中)。因此,如果需要根据实例或关联动态地将默认值设置为某个值,则仍然必须使用替代方法,例如after_initialize回调。如前所述,如果引用关联,建议将此限制为新记录,以避免n+1个查询。

after_initialize :do_something_that_references_instance_or_associations, if: :new_record?