如何在 ActiveRecord 中设置默认值?
-
11-07-2019 - |
题
如何在 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 即可工作。
- 压倒一切
initialize
可以工作,但别忘了打电话super
! - 使用像 phusion 这样的插件变得有点荒谬。这是 ruby,我们真的需要一个插件来初始化一些默认值吗?
- 压倒一切
after_initialize
已弃用 从 Rails 3 开始。当我覆盖after_initialize
在 Rails 3.0.3 中,我在控制台中收到以下警告:
弃用警告: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?
有关更多详细信息,请参阅保罗·拉塞尔对此答案的评论
如果您仅为模型选择列的子集(即;使用
select
在类似的查询中Person.select(:firstname, :lastname).all
)你会得到一个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 的评论)
其他提示
我们把在数据库中的默认值通过迁移(通过指定每一列上定义:default
选项),并让活动记录使用这些值来设置每个属性的缺省值。
IMHO,这种方法与AR的原则相一致:配置约定,DRY,表定义驱动模式,而不是周围的其他方法
。请注意,该缺省值仍处于申请(红宝石)代码,虽然不是在模型,但在迁移(多个)。
导轨5 +
可以使用属性您的模型内的方法,例如:
class Account < ApplicationRecord
attribute :locale, :string, default: 'en'
end
您也可以传递一个lambda到default
参数。例如:
attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }
一些简单的情况下,可以通过定义在数据库模式缺省处理,但是不处理许多棘手的病例,包括计算的值和其他模型密钥。对于这些情况我这样做:
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回调不提供这种明显的使用情况,但我已经通过确认对象是否已经持续表明它是不是新发做的。
在看到布拉德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回调模式可通过简单地操作的方式来改善以下
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
在的Phusion球员有一些很好插件获得此
比所提出的答案一个更好的/清洁潜力的方法是覆盖存取,是这样的:
def status
self['status'] || ACTIVE
end
从文档:
运行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"
类似的问题,但都有稍微不同的背景: - 如何创建一个在导轨的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
<强> 如果想要每个对象 强> 有从用户输入而计算出的值:使用before_save :default_values
你要用户输入X
然后Y = X+'foo'
?使用方法:
class Task < ActiveRecord::Base
before_save :default_values
def default_values
self.status ||= 'P'
end
end
<击>这是什么构造函数是!覆盖模型的initialize
方法击>
使用after_initialize
方法。
燮好,我最终做以下内容:
def after_initialize
self.extras||={}
self.other_stuff||="This stuff"
end
就像一个魅力!
首先要做的事情是:我不反对杰夫的回答。当您的应用程序很小并且逻辑很简单时,这是有意义的。我在这里试图深入了解在构建和维护更大的应用程序时它如何成为一个问题。我不建议在构建小东西时首先使用这种方法,但请记住它作为替代方法:
这里的一个问题是记录上的这种默认是否是业务逻辑。如果是的话,我会谨慎地将其放入 ORM 模型中。由于 ryw 提到的字段是 积极的, ,这听起来确实像业务逻辑。例如。用户处于活跃状态。
为什么我要谨慎地将业务问题放入 ORM 模型中?
它打破了 建议零售价. 。任何继承自 ActiveRecord::Base 的类都已经在执行 很多 不同的事情,其中最主要的是数据一致性(验证)和持久性(保存)。将业务逻辑(无论多么小)放入 AR::Base 中会破坏 SRP。
测试起来比较慢。如果我想测试 ORM 模型中发生的任何形式的逻辑,我的测试必须初始化 Rails 才能运行。在应用程序开始时这不会是太大的问题,但会累积到单元测试需要很长时间才能运行为止。
它将以具体的方式进一步打破 SRP。假设我们的业务现在要求我们在项目激活时向用户发送电子邮件?现在,我们将电子邮件逻辑添加到 Item ORM 模型中,该模型的主要职责是对 Item 进行建模。它不应该关心电子邮件逻辑。这是一个案例 商业副作用. 。这些不属于 ORM 模型。
多元化很难。我见过成熟的 Rails 应用程序,其中包含数据库支持的 init_type 之类的东西:字符串字段,其唯一目的是控制初始化逻辑。这是为了修复结构性问题而污染数据库。我相信还有更好的方法。
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, 属性 = {})
这已经回答了很长一段时间,但我经常需要默认值,并且不希望把他们在数据库中。我创建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的方式,Rails的前5名,实际上是将其设置在迁移,只是看在db/schema.rb
想见正在由DB任何模型设置什么默认值时。
相反的是@Jeff佩林回答状态(这是一个有点老),迁移的方法使用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,并且,作为一个额外的好处,它也更新所有预先存在的DB的记录被设置有用于这个字段的默认值也是如此。如果你愿意,你可以排除这个参数在迁移,但我发现它非常好用!
在滑轨5+的规范的方法是,如@Lucas卡顿表示:
class Item < ActiveRecord::Base
attribute :scheduler_type, :string, default: 'hotseat'
end
与after_initialize解决方案的问题是,你有一个after_initialize添加到您查找出来的数据库,每一个单独的对象,无论是否访问此属性或没有。我建议延迟加载的方法。
属性的方法(吸气剂)是当然方法本身,这样就可以覆盖它们并提供一个默认。是这样的:
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
给予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
class Item < ActiveRecord::Base
def status
self[:status] or ACTIVE
end
before_save{ self.status ||= ACTIVE }
end
我强烈建议使用 “default_value_for” 宝石: https://github.com/FooBarWidget/default_value_for
有该非常需要重写initialize方法,一些棘手的场景其中该宝石一样。
示例:
你的数据库默认为NULL,你的模型/红宝石定义的默认值是“一些字符串”,但实际上你的要的设定值为零无论出于何种原因:MyModel.new(my_attr: nil)
大多数解决方案,这里将无法将该值设为零,并将而是设置为默认值。
这是好了,而不是采取||=
方法,切换到my_attr_changed?
...
BUT 现在想象你的数据库默认值是“一些字符串”,你的模型/红宝石定义的默认值是“其他一些字符串”,但在一定的情况下,你的需要的值设置为“一些字符串”(分贝默认值):MyModel.new(my_attr: 'some_string')
这将导致my_attr_changed?
是假,因为该值的数据库默认情况下,这反过来会触发你的Ruby定义的默认代码并将其值设置为“其他字符串”匹配 - 同样,不是你想要的。
由于这些原因,我不认为这是可以适当配合只是一个after_initialize钩来完成的。
再次我认为“default_value_for”宝石走的是正确的方法: https://github.com/FooBarWidget / default_value_for
虽然这样做对设置的默认值是混乱的,在大多数情况下尴尬的,你可以使用:default_scope
为好。这里请查看 squil的评论。
after_initialize方法已过时,使用回调来代替。
after_initialize :defaults
def defaults
self.extras||={}
self.other_stuff||="This stuff"
end
然而,使用:默认在您的迁移仍然是最彻底的方法。
我已经发现当使用验证方法提供了很多在设置默认值的控制。你甚至可以设置默认值(或验证失败)更新。你甚至设置插入VS如果你真的想更新不同的默认值。 注意,默认将不会被设置到#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也通过由返回的每个对象称作:发现: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find
如果列恰好是一个“状态”类型列,和你的模型适合于使用状态机,可以考虑使用的 AASM宝石,之后就可以简单地做
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
这将完成设置为你的默认值。 (对我来说我有“欢迎来到队”),然后它会更进一步的保证总是有一个目前该对象的值。
希望帮助!
从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
奇怪,他没有被这里建议