How can I set default values in ActiveRecord?
如何在ActiveRecord中设置默认值?
我看到一篇来自Pratik的文章,描述了一段丑陋、复杂的代码:http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model
1 2 3 4 5 6 7 8 9 10 11 | 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 |
我看到了以下搜索示例:
1 2 3 4 | def initialize super self.status = ACTIVE unless self.status end |
和
1 2 3 4 | def after_initialize return unless new_record? self.status = ACTIVE end |
我也看到过人们把它放在迁移中,但我更愿意看到它在模型代码中定义。
在ActiveRecord模型中,是否有一种规范化的方法来设置字段的默认值?
每个可用的方法都有几个问题,但我认为定义
DEPRECATION WARNING: Base#after_initialize has been deprecated, please use Base.after_initialize :method instead. (called from /Users/me/myapp/app/models/my_model:15)
因此,我想说,编写一个
1 2 3 4 5 6 7 8 9 | 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 |
现在您只有一个地方来查找模型的初始化。我一直在用这个方法直到有人想出更好的方法。
Caveats:
对于布尔字段,请执行以下操作:
更多详细信息,请参阅Paul Russell对此答案的评论。
如果您只为模型选择列的子集(即在像
对于布尔列…
还要注意,在Rails3.2之前的语法是不同的(请参见下面的CliffDarling的评论)。
我们通过迁移(通过在每个列定义上指定
imho,这种方法符合ar:convention over configuration、dry的原则,表定义驱动模型,而不是相反。
请注意,默认值仍然在应用程序(Ruby)代码中,虽然不在模型中,但在迁移中。
轨道5 +
您可以在模型中使用属性方法,例如:
1 2 3 | class Account < ApplicationRecord attribute :locale, :string, default: 'en' end |
还可以将lambda传递给
1 | attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid } |
一些简单的情况可以通过在数据库模式中定义一个默认值来处理,但这并不能处理许多复杂的情况,包括计算值和其他模型的键。对于这些情况,我会这样做:
1 2 3 4 5 6 7 8 9 | 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的回答后,如果将条件转移到回调请求,则情况会更清楚:
1 2 3 4 5 6 7 8 | 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 |
通过执行以下操作,可以改进后初始化回调模式
1 | after_initialize :some_method_goes_here, :if => :new_record? |
如果您的init代码需要处理关联,这有一个非常重要的好处,因为如果您在不包括关联的情况下读取初始记录,下面的代码会触发一个微妙的n+1。
1 2 3 4 5 6 7 8 9 10 | class Account has_one :config after_initialize :init_config def init_config self.config ||= build_config end end |
phusion有一些不错的插件。
我用的是
从文档中:运行
1 2 3 4 5 6 7 8 9 | 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" |
比建议的答案更好/更干净的潜在方法是覆盖访问器,如下所示:
1 2 3 | def status self['status'] || ACTIVE end |
请参阅ActiveRecord::Base文档中的"覆盖默认访问器",以及有关使用self的StackOverflow中的更多内容。
类似的问题,但都有略微不同的背景:-如何在Rails ActiveRecord的模型中为属性创建默认值?
最佳答案:取决于你想要什么!
如果希望每个对象都以一个值开头:使用
打开页面时是否希望
1 2 3 4 5 6 7 8 9 10 | 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 2 3 4 5 6 | class Task < ActiveRecord::Base before_save :default_values def default_values self.status ||= 'P' end end |
这就是构造函数的作用!覆盖模型的
使用
伙计们,我最后做了以下事情:
1 2 3 4 | def after_initialize self.extras||={} self.other_stuff||="This stuff" end |
真是魅力四射!
首先,我不反对杰夫的回答。当你的应用程序很小,逻辑也很简单时,这是有意义的。我在这里试图深入了解在构建和维护一个更大的应用程序时,它是如何成为一个问题的。我不建议在构建小的东西时首先使用这种方法,但要记住它是一种替代方法:
这里的一个问题是记录上的这个默认值是否是业务逻辑。如果是,我会谨慎地将其放入ORM模型中。由于RYW提到的字段是活动的,这听起来像是业务逻辑。例如,用户处于活动状态。
为什么我要谨慎地将业务关注点放在ORM模型中?
它打破了SRP。从ActiveRecord::Base继承的任何类都已经做了很多不同的事情,其中主要是数据一致性(验证)和持久性(保存)。把业务逻辑放在ar::base中,不管它有多小,都会破坏srp。
测试速度较慢。如果我想测试ORM模型中发生的任何形式的逻辑,我的测试必须初始化Rails才能运行。在应用程序的开始阶段,这不会是一个太多的问题,但会累积到单元测试需要很长时间才能运行。
它将以具体的方式,更进一步地破坏SRP。说我们的业务现在需要我们在有活动项目时向用户发送电子邮件?现在,我们正在向项目ORM模型添加电子邮件逻辑,其主要职责是为项目建模。它不应该关心电子邮件逻辑。这是一个商业副作用的案例。这些不属于ORM模型。
很难实现多样化。我见过成熟的Rails应用程序,比如数据库支持的init_type:string字段,它的唯一目的是控制初始化逻辑。这会污染数据库以修复结构问题。我相信有更好的方法。
Poro方式:虽然这是更多的代码,但它允许您将ORM模型和业务逻辑分开。这里的代码是简化的,但应该显示出以下想法:
1 2 3 4 5 6 7 | class SellableItemFactory def self.new(attributes = {}) record = Item.new(attributes) record.active = true if record.active.nil? record end end |
这样,创建新项目的方法将是
1 | SellableItemFactory.new |
我的测试现在可以简单地验证,如果itemFactory没有值,它会在item上设置active。无需轨道初始化,无SRP中断。当项初始化变得更高级(例如设置状态字段、默认类型等)时,项工厂可以添加此项。如果我们最终得到两种类型的默认值,我们可以创建一个新的BusinessAcaseitemFactory来实现这一点。
注意:在这里使用依赖注入还可以使工厂构建许多活动的东西,但是为了简单起见,我省略了它。这里是:self.new(klass=item,attributes=)
这个问题已经回答了很长一段时间,但是我经常需要默认值,而不希望将它们放在数据库中。我创建了一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 |
然后在我的模型中使用它,就像这样:
1 2 3 4 5 6 | class Widget < ApplicationRecord include DefaultValues defaults :category, to: 'uncategorized' defaults :token, to: -> { SecureRandom.uuid } end |
I've also seen people put it in their migration, but I'd rather see it
defined in the model code.Is there a canonical way to set default value for fields in
ActiveRecord model?
在Rails 5之前,规范的Rails方法实际上是在迁移中设置它,只要查看
与@jeff perrin answer所说的(有点旧)相反,迁移方法甚至会在使用
最简单的事情往往是最好的。减少知识债务和代码库中潜在的混淆点。它"只是起作用"。
1 2 3 4 5 | class AddStatusToItem < ActiveRecord::Migration def change add_column :items, :scheduler_type, :string, { null: false, default:"hotseat" } end end |
正如@lucas caton所说,Rails 5+的标准方法是:
1 2 3 | class Item < ActiveRecord::Base attribute :scheduler_type, :string, default: 'hotseat' end |
1 2 3 4 5 6 7 | class Item < ActiveRecord::Base def status self[:status] or ACTIVE end before_save{ self.status ||= ACTIVE } end |
我遇到了与
如:
1 | @bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no) |
因此,我以这种方式重写initialize来结束它:
1 2 3 4 5 6 7 8 9 | def initialize super default_values end private def default_values self.date_received ||= Date.current end |
在执行自定义代码(即:默认值)之前,需要执行
后初始化解决方案的问题是,无论是否访问此属性,都必须向从数据库中查找的每个对象添加后初始化。我建议采用一种懒惰的加载方式。
属性方法(getter)当然是方法本身,因此您可以重写它们并提供默认值。类似:
1 2 3 4 5 6 | 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通过状态("active")。在这种情况下,如果数据库支持的话,我认为您真的需要在数据库约束中设置默认值。
我强烈建议使用"gem的默认值":https://github.com/foobarwidget/default_value_for
有些棘手的场景需要重写初始化方法,而gem就是这样做的。
实例:
您的db默认值为空,您的model/ruby定义的默认值为"some string",但实际上您想将该值设置为nil,原因是:
这里的大多数解决方案将无法将值设置为零,而是将其设置为默认值。
好吧,所以你不采用
但是现在假设您的数据库默认值是"some string",您的模型/ruby定义的默认值是"some other string",但是在某种情况下,您希望将该值设置为"some string"(数据库默认值):
这将导致
基于这些原因,我认为仅仅使用一个after-initialize钩子就不能正确地完成这个任务。
同样,我认为"gem的默认值"采用了正确的方法:https://github.com/foobarwidget/defaultu valueu for
我发现使用验证方法可以对设置默认值提供很多控制。您甚至可以设置更新的默认值(或验证失败)。如果您真的想,您甚至为inserts和updates设置了不同的默认值。请注意,在有效之前,不会设置默认值?被称为。
1 2 3 4 5 6 7 8 9 10 11 12 | 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
不推荐使用初始化方法后,请改用回调。
1 2 3 4 5 6 | after_initialize :defaults def defaults self.extras||={} self.other_stuff||="This stuff" end |
但是,在迁移中使用:默认仍然是最干净的方法。
这里有一个我用过的解决方案,我有点惊讶还没有加入。
它有两部分。第一部分是在实际迁移中设置默认值,第二部分是在模型中添加验证以确保存在为真。
1 | add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team' |
在这里您将看到默认值已经设置好了。现在,在验证中,您要确保字符串始终有一个值,所以只需执行
1 | validates :new_team_signature, presence: true |
这将为您设置默认值。(对于我来说,我有"欢迎加入团队"),然后它将进一步确保该对象始终存在一个值。
希望有帮助!
尽管在大多数情况下,设置默认值会让人困惑和尴尬,但也可以使用
如果列恰好是"status"类型的列,并且您的模型将自己借给了状态机的使用,那么可以考虑使用aasm gem,之后您可以简单地执行此操作。
1 2 3 4 5 | aasm column:"status" do state :available, initial: true state :used # transitions end |
它仍然不能初始化未保存记录的值,但是它比使用
https://github.com/keithrwell/rails_默认值
1 2 3 | class Task < ActiveRecord::Base default :status => 'active' end |
在Rails 3中使用默认范围
API文档
ActiveRecord掩盖了数据库(模式)中定义的默认与应用程序(模型)中完成的默认之间的差异。在初始化过程中,它解析数据库模式并记录其中指定的任何默认值。稍后,在创建对象时,它会在不接触数据库的情况下分配那些模式指定的默认值。
讨论
从api文档http://api.rubyonrails.org/classes/activerecord/callbacks.html在模型中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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 |
奇怪的是这里没有人建议他