如何在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模型中的字段设置默认值?
答案 0 :(得分:542)
每种可用方法都存在一些问题,但我认为定义after_initialize
回调是出于以下原因的方法:
default_scope
将初始化新模型的值,但随后将成为您找到模型的范围。如果你只想将一些数字初始化为0,那么不是你想要的是什么。initialize
可以有效,但不要忘记致电super
!after_initialize
。当我在rails 3.0.3中覆盖after_initialize
时,我在控制台中收到以下警告: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
现在您有只有一个位置来查找模型的初始化。我正在使用这种方法,直到有人想出一个更好的方法。
注意事项:
对于布尔字段,执行:
self.bool_field = true if self.bool_field.nil?
有关详细信息,请参阅Paul Russell对此答案的评论
如果您只选择模型的列子集(即;在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)
您可以在模型中使用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 documentation和more from StackOverflow on using self中的“覆盖默认访问者”。
答案 7 :(得分:8)
从文档中:
运行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模型中?
它打破SRP。从ActiveRecord :: Base继承的任何类已经在做不同事情的 lot ,其中主要是数据一致性(验证)和持久性(保存)。使用AR :: Base将业务逻辑(尽管很小)打破SRP。
测试速度较慢。如果我想测试我的ORM模型中发生的任何形式的逻辑,我的测试必须初始化Rails才能运行。这在您的应用程序开始时不会出现太多问题,但会累积到您的单元测试需要很长时间才能运行。
它将以更具体的方式打破SRP。假设我们的业务现在要求我们在项目变为活动时向用户发送电子邮件?现在我们将向Item ORM模型添加电子邮件逻辑,其主要职责是为Item建模。它不应该关心电子邮件逻辑。这是业务副作用的案例。这些不属于ORM模型。
很难实现多样化。我见过成熟的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 = {})
答案 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
表中继承的Student
和Users
模型。
然后,无论何时创建用户,都要求我为该角色设置一个默认值,例如admin
角色的ID = 1
或student
角色的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
ActiveRecord模糊了数据库(模式)中定义的默认值与应用程序(模型)中的默认值之间的差异。在初始化期间,它解析数据库模式并记录在那里指定的任何默认值。稍后,在创建对象时,它会分配这些模式指定的默认值,而不会触及数据库。
答案 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
感到惊讶的是他没有被建议