我怎样才能在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
我已经看到下面的例子search:
def initialize super self.status = ACTIVE unless self.status end
和
def after_initialize return unless new_record? self.status = ACTIVE end
我也看到人们把它放在他们的迁移中,但我宁愿看它在模型代码中定义。
有没有规范的方式来设置ActiveRecord模型中的字段的默认值?
每种可用方法都有几个问题,但是我认为定义一个after_initialize
callback是出于以下原因:
-
default_scope
将初始化新模型的值,但是这将成为您find模型的范围。 如果你只是想将一些数字初始化为0,那么这不是你想要的。 - 在您的迁移中定义默认值也是时间的一部分…正如已经提到的,当您调用Model.new时,这将不起作用。
- 重写
initialize
可以工作,但不要忘记叫super
! - 使用像phusion的插件是有点可笑的。 这是ruby,我们真的需要一个插件来初始化一些默认值?
- 覆盖
after_initialize
从Rails 3 不推荐使用 。当我在rails 3.0.3中覆盖after_initialize
时,在控制台中出现以下警告:
DEPRECATION WARNING:Base#after_initialize已被弃用,请改用Base.after_initialize:方法。 (从/ Users / me / myapp / app / models / my_model:15中调用)
因此,我会说写一个after_initialize
callback,它可以让你默认的属性,除了让你设置像这样的关联的默认值:
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模型的一个列的子集(即;使用
select
像查询Person.select(:firstname, :lastname).all
MissingAttributeError
),你会得到一个MissingAttributeError
如果你的init
方法访问一个列MissingAttributeError
'不包括在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的评论)
我们通过迁移(通过在每个列定义上指定:default
选项)将缺省值放入数据库中,并让Active Record使用这些值来设置每个属性的缺省值。
恕我直言,这种方法符合AR的原则:约定优于configuration,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,但我不希望它被应用到只有那些新的或创build的对象。 我认为这个明显的用例没有提供after_newcallback,但是我已经通过确认对象是否已经被保留来表明它不是新的。
看到Brad Murray的回答,如果条件转移到callback请求,这个更清晰:
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
Phusion的人有一些很好的插件 。
after_initializecallback模式可以通过简单的做以下改进
after_initialize :some_method_goes_here, :if => :new_record?
如果你的初始化代码需要处理关联,这将有一个非平凡的好处,因为如果你读取了初始logging而不包括关联的话,下面的代码会触发一个微妙的n + 1。
class Account has_one :config after_initialize :init_config def init_config self.config ||= build_config end end
在Rails 5+中,可以使用模型中的属性方法,例如:
class Account < ApplicationRecord attribute :locale, :string, default: 'en' end
我使用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"
这是什么构造函数是! 覆盖模型的 initialize
方法。
使用after_initialize
方法。
Sup家伙,我最终做了以下几点:
def after_initialize self.extras||={} self.other_stuff||="This stuff" end
奇迹般有效!
比提出的答案更好,更清洁的方法是覆盖访问器,如下所示:
def status self['status'] || ACTIVE end
请参阅ActiveRecord :: Base文档中的 “覆盖默认访问器”,以及使用self自StackOverflow获取更多内容 。
首先要做的事情是:我不反对杰夫的回答。 这是有道理的,当你的应用程序是小的,你的逻辑简单。 我在这里试图了解在构build和维护更大的应用程序时如何成为问题。 我不build议在构build一个小的东西的时候先使用这个方法,但是要把它作为一种替代方法来记住:
这里的一个问题是这个logging上的默认值是否是业务逻辑。 如果是这样,我会谨慎的把它放在ORM模型中。 由于领域ryw提到是积极的 ,这听起来像商业逻辑。 例如,用户处于活动状态。
为什么我会谨慎把商业关注放在ORM模型中?
-
它打破了SRP 。 从ActiveRecord :: Baseinheritance的任何类已经做了很多不同的事情,其中主要是数据一致性(validation)和持久性(保存)。 把业务逻辑放在很小的地方,用AR :: Base打破SRP。
-
testing速度较慢。 如果我想testing在我的ORM模型中发生的任何forms的逻辑,我的testing必须初始化Rails才能运行。 在你的应用程序开始的时候,这不会太成问题,但会积累起来,直到你的unit testing需要很长时间才能运行。
-
它将会更多地破坏SRP,并以具体的方式。 现在说我们的业务需要我们发送电子邮件用户时有项目变得活跃? 现在我们将电子邮件逻辑添加到项目ORM模型,其主要职责是对项目build模。 它不应该关心电子邮件逻辑。 这是商业副作用的情况 。 这些不属于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
然后,这个地方,创build一个新的项目的方式是
SellableItemFactory.new
而且我的testing现在可以简单地validationItemFactory如果它没有值,就会在Item上设置活动。 没有Rails初始化需要,没有SRP打破。 当Item初始化变得更高级时(例如设置一个状态字段,一个默认的types等),ItemFactory可以添加这个。 如果我们最终得到两种types的默认值,我们可以创build一个新的BusinesCaseItemFactory来完成这个工作。
注意:在这里使用dependency injection也是有好处的,这样工厂就可以创build许多活动的东西,但为了简单起见,我把它放在了外面。 这里是: self.new(klass = Item,attributes = {})
类似的问题,但所有的上下文略有不同: – 如何创buildRails的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
如果你想让每个对象有一个从用户input计算出来的值:use before_save :default_values
你希望用户inputX
,然后Y = X+'foo'
? 使用:
class Task < ActiveRecord::Base before_save :default_values def default_values self.status ||= 'P' end end
after_initialize解决scheme的问题在于,无论您是否访问该属性,都必须为查find的每个对象添加一个after_initialize。 我build议一个懒惰的方法。
属性方法(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
这已经回答了很长时间,但我需要经常使用默认值,而不是将它们放在数据库中。 我创build一个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
我也看到人们把它放在他们的迁移中,但我宁愿看它在模型代码中定义。
有没有规范的方式来设置ActiveRecord模型中的字段的默认值?
在Rails 5之前,规范的Rails方法实际上是在迁移中设置它,只要查看db/schema.rb
可以随时查看DB为任何模型设置的默认值。
与@Jeff Perrin的答案(这有点旧)相反,迁移方法甚至会使用Model.new
时的默认值,这是由于某些Rails魔术。 validation在Rails 4.1.16中工作。
最简单的事情往往是最好的。 代码库中知识欠债和潜在的混淆点较less。 而且“正常工作”。
class AddStatusToItem < ActiveRecord::Migration def change add_column :items, :scheduler_type, :string, { null: false, default: "hotseat" } end end
null: false
不允许在数据库中使用NULL值,另外还会更新所有预先存在的数据库logging,并使用该字段的默认值进行设置。 如果您愿意,您可以在迁移中排除此参数,但我发现它非常方便!
Rails 5+的规范方式就像@Lucas Caton所说:
class Item < ActiveRecord::Base attribute :scheduler_type, :string, default: 'hotseat' end
尽pipe在大多数情况下,设置默认值会让人感到困惑,但也可以使用:default_scope
。 在这里查看squil的评论 。
不build议使用after_initialize方法,而是使用callback。
after_initialize :defaults def defaults self.extras||={} self.other_stuff||="This stuff" end
但是,在迁移中使用:default仍然是最干净的方法。
我发现使用validation方法提供了对设置默认值的很多控制。 您甚至可以设置更新的默认值(或未通过validation)。 如果你真的想要的话,甚至可以为插入和更新设置一个不同的默认值。 请注意,默认不会被设置,直到#有效? 叫做。
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
在执行复杂发现时,遇到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
如果列恰好是“状态”types的列,并且您的模型适合使用状态机,请考虑使用aasm gem ,之后您可以简单地执行
aasm column: "status" do state :available, initial: true state :used # transitions end
它仍然没有初始化未保存logging的值,但是比使用init
或其他types的自定义滚动条更简洁一点,而且还可以获得aasm的其他好处,例如所有状态的作用域。
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base default :status => 'active' end
我强烈build议使用“default_value_for”gem: https : //github.com/FooBarWidget/default_value_for
有一些棘手的情况几乎需要重写初始化方法,这是gem。
例子:
你的数据库默认是NULL,你的模型/ ruby定义的默认是“一些string”,但你真的要设置为零无论出于什么原因: MyModel.new(my_attr: nil)
这里的大多数解决scheme将无法将值设置为零,而是将其设置为默认值。
好的,所以不要采取||=
方法,而是切换到my_attr_changed?
…
但现在想象你的数据库默认是“一些string”,你的模型/ruby定义的默认是“其他string”,但在某些情况下,你想设置为“一些string”(数据库默认值): MyModel.new(my_attr: 'some_string')
这将导致my_attr_changed?
因为该值匹配数据库默认,这反过来将启动您的ruby定义的默认代码,并将值设置为“其他string” – 再次,而不是你想要的。
由于这些原因,我不认为这可以正确完成只有一个after_initialize钩子。
再次,我认为“default_value_for”gem正在采取正确的做法: https : //github.com/FooBarWidget/default_value_for
在rails 3中使用default_scope
api文档
ActiveRecord模糊了数据库(模式)中定义的默认值与应用程序(模型)中的默认值之间的区别。 在初始化过程中,它分析数据库模式并logging指定的默认值。 稍后,在创build对象时,它将分配这些模式指定的默认值而不触及数据库。
讨论
从api文档http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html在你的模型中使用;before_validation
方法,它给你创build创build和更新调用的特定初始化的选项,例如在这个例子中(再次代码从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
惊讶的是,他没有在这里build议