如何在 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 模型中的字段设置默认值?
每种可用方法都存在一些问题,但我认为定义 after_initialize
回调是可行的方法,原因如下:
default_scope 将为新模型初始化值,但随后它将成为您找到模型的范围。如果您只想将一些数字初始化为 0,那么这不是您想要的。在您的迁移中定义默认值也有部分时间......正如已经提到的,当您只调用 Model.new 时,这将不起作用。覆盖初始化可以工作,但不要忘记调用 super!使用像 phusion 这样的插件有点荒谬。这是 ruby,我们真的需要一个插件来初始化一些默认值吗?从 Rails 3 开始,不推荐覆盖 after_initialize。当我在 rails 3.0.3 中覆盖 after_initialize 时,我在控制台中收到以下警告:
弃用警告: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
现在您只有一个地方可以查找模型的初始化。我一直在使用这种方法,直到有人提出更好的方法。
注意事项:
对于布尔字段:self.bool_field = true if self.bool_field.nil?有关更多详细信息,请参阅 Paul Russell 对此答案的评论 如果您只是为模型选择列的子集(即;在 Person.select(:firstname, :lastname).all 之类的查询中使用 select),您将收到 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 评论)
导轨 5+
您可以在模型中使用 attribute 方法,例如:
class Account < ApplicationRecord
attribute :locale, :string, default: 'en'
end
您还可以将 lambda 传递给 default
参数。例子:
attribute :uuid, :string, default: -> { SecureRandom.uuid }
第二个参数是类型,也可以是自定义类型类实例,例如:
attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }
Value
并且不会进行类型转换。
store_accessor :my_jsonb_column, :locale
,您可以定义 attribute :locale, :string, default: 'en'
我们通过迁移将默认值放入数据库中(通过在每个列定义上指定 :default
选项)并让 Active Record 使用这些值来设置每个属性的默认值。
恕我直言,这种方法符合 AR 的原则:约定优于配置、DRY、表定义驱动模型,而不是相反。
请注意,默认值仍在应用程序 (Ruby) 代码中,尽管不在模型中,而是在迁移中。
一些简单的情况可以通过在数据库模式中定义一个默认值来处理,但不能处理一些更棘手的情况,包括其他模型的计算值和键。对于这些情况,我这样做:
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
:before_create
怎么样?
只需执行以下操作即可改进 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
Phusion 的家伙对此有一些很好的plugin。
:default
值与 Model.new
一起“正常工作”。
:default
值与 Model.new
一起“正常工作”,这与 Jeff 在他的帖子中所说的相反。已验证在 Rails 4.1.16 中工作。
比建议的答案更好/更清洁的潜在方法是覆盖访问器,如下所示:
def status
self['status'] || ACTIVE
end
请参阅 the ActiveRecord::Base documentation 和 more from StackOverflow on using self 中的“覆盖默认访问器”。
attributes
返回的哈希中,状态仍将为 nil。在 Rails 5.2.0 中测试。
从文档中:运行 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"
类似的问题,但上下文略有不同:- 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
,然后 { 3}?利用:
class Task < ActiveRecord::Base
before_save :default_values
def default_values
self.status ||= 'P'
end
end
我也看到人们把它放在他们的迁移中,但我宁愿看到它在模型代码中定义。是否有规范的方法来为 ActiveRecord 模型中的字段设置默认值?
在 Rails 5 之前,规范的 Rails 方式实际上是在迁移中设置它,只要想查看数据库为任何模型设置的默认值,只需在 db/schema.rb
中查找。
与@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 guide 中的列更改方法选项。
null: false
不允许 DB 中的 NULL 值,并且,作为一个额外的好处,它还会更新,以便所有先前为 null 的预先存在的 DB 记录也设置为该字段的默认值。如果您愿意,您可以在迁移中排除此参数,但我发现它非常方便!
正如@Lucas Caton 所说,Rails 5+ 中的规范方式是:
class Item < ActiveRecord::Base
attribute :scheduler_type, :string, default: 'hotseat'
end
这就是构造函数的用途!覆盖模型的 initialize
方法。 罢工>
使用 after_initialize
方法。
after_initialize
方法。
伙计们,我最终做了以下事情:
def after_initialize
self.extras||={}
self.other_stuff||="This stuff"
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
在进行复杂查找时,我遇到了 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
def initialize(*); super; default_values; end
。此外,即使在 .attributes
哈希中,默认值也是可用的。
after_initialize 方法已弃用,请改用回调。
after_initialize :defaults
def defaults
self.extras||={}
self.other_stuff||="This stuff"
end
但是,在迁移中使用 :default 仍然是最干净的方法。
after_initialize
方法 不推荐使用。其实宏风格的回调你举了一个IS deprecated的例子。详细信息:guides.rubyonrails.org/…
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')。在这种情况下,如果数据库支持,我认为您确实需要在数据库约束中设置默认值。
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
我强烈建议使用“default_value_for”gem:https://github.com/FooBarWidget/default_value_for
有一些棘手的场景几乎需要重写该 gem 所做的初始化方法。
例子:
您的数据库默认值为 NULL,您的模型/ruby 定义的默认值为“某个字符串”,但您实际上想要出于任何原因将该值设置为 nil:MyModel.new(my_attr: nil)
这里的大多数解决方案都无法将值设置为 nil,而是将其设置为默认值。
好的,所以不要采用 ||=
方法,而是切换到 my_attr_changed?
...
但是现在假设您的数据库默认值是“某个字符串”,您的模型/ruby 定义的默认值是“某个其他字符串”,但是在某种情况下,您想要设置“一些字符串”的值(数据库默认值):MyModel.new(my_attr: 'some_string')
这将导致 my_attr_changed?
为 false,因为该值与 db 默认值匹配,这反过来会触发您的 ruby 定义的默认代码并将该值设置为“其他字符串”——同样,不是你想要什么。
由于这些原因,我认为仅使用 after_initialize 钩子无法正确完成此操作。
同样,我认为“default_value_for”gem 采用了正确的方法:https://github.com/FooBarWidget/default_value_for
我发现使用验证方法可以很好地控制设置默认值。您甚至可以为更新设置默认值(或验证失败)。如果您真的想要,您甚至可以为插入和更新设置不同的默认值。请注意,在 #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
new
操作重用。
如果该列恰好是“状态”类型的列,并且您的模型适合使用状态机,请考虑使用 aasm gem,之后您可以简单地执行
aasm column: "status" do
state :available, initial: true
state :used
# transitions
end
它仍然不会初始化未保存记录的值,但它比使用 init
或其他方式滚动您自己的值要干净一些,并且您可以获得 aasm 的其他好处,例如所有状态的范围。
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end
这是我使用的一个解决方案,我有点惊讶尚未添加。
它有两个部分。第一部分是在实际迁移中设置默认值,第二部分是在模型中添加验证以确保存在为真。
add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'
所以你会看到这里已经设置了默认值。现在在验证中,您要确保字符串始终有一个值,所以只需执行
validates :new_team_signature, presence: true
这将为您设置默认值。 (对我来说,我有“欢迎加入团队”),然后它会更进一步,确保该对象始终存在一个值。
希望有帮助!
# 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
表继承的 Admin
和 Student
模型。
然后要求我在创建用户时为角色设置一个默认值,例如具有 id = 1
的 admin
角色或具有 id = 2
的 student
角色。
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
导轨 6.1+
您现在可以在模型上使用 attribute 方法而无需设置类型。
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?
after_initialize
不是只针对新记录运行吗?
after_initialize
回调在从数据库加载现有记录后运行。
在 Rails 3 中使用 default_scope
ActiveRecord 掩盖了在数据库中定义的默认设置(模式)和在应用程序中完成的默认设置(模型)之间的区别。在初始化期间,它解析数据库模式并记录那里指定的任何默认值。稍后,在创建对象时,它会分配那些模式指定的默认值,而不涉及数据库。
default_scope
。这将使您的所有查询将此条件添加到您设置的字段中。这几乎不是你想要的。
从 api 文档 http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html 在您的模型中使用 before_validation
方法,它为您提供了为创建和更新调用创建特定初始化的选项,例如在此示例中(再次从 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
很惊讶他没有在这里被推荐
initialize
,对于应该清晰和明确定义的东西似乎真的很复杂。在在这里搜索之前,我花了几个小时浏览文档,因为我认为这个功能已经存在于某个地方,而我只是不知道它。self.bool_field ||= true
,因为即使您明确地将其初始化为 false,这也会强制该字段为 true。而是执行self.bool_field = true if self.bool_field.nil?
。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?
,你需要一个字符串而不是一个符号。return if !new_record?
开始initialize
以避免性能问题。