如何在 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 回调是一种可行的方法,原因如下:

  1. default_scope 将为新模型初始化值,但这将成为您找到模型的范围。如果你只想将一些数字初始化为 0 那么这是 不是 你想要什么。
  2. 在迁移中定义默认值有时也有效......正如已经提到的,这将 不是 只需调用 Model.new 即可工作。
  3. 压倒一切 initialize 可以工作,但别忘了打电话 super!
  4. 使用像 phusion 这样的插件变得有点荒谬。这是 ruby​​,我们真的需要一个插件来初始化一些默认值吗?
  5. 压倒一切 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    

现在你有 只有一个 寻找模型初始化的地方。我一直在使用这种方法,直到有人想出更好的方法为止。

注意事项:

  1. 对于布尔字段执行以下操作:

    self.bool_field = true if self.bool_field.nil?

    有关更多详细信息,请参阅保罗·拉塞尔对此答案的评论

  2. 如果您仅为模型选择列的子集(即;使用 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

请参阅在 ActiveRecord的::基地文档更从StackOverflow的上使用自

我使用 attribute-defaults宝石

从文档: 运行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 模型中?

  1. 它打破了 建议零售价. 。任何继承自 ActiveRecord::Base 的类都已经在执行 很多 不同的事情,其中​​最主要的是数据一致性(验证)和持久性(保存)。将业务逻辑(无论多么小)放入 AR::Base 中会破坏 SRP。

  2. 测试起来比较慢。如果我想测试 ORM 模型中发生的任何形式的逻辑,我的测试必须初始化 Rails 才能运行。在应用程序开始时这不会是太大的问题,但会累积到单元测试需要很长时间才能运行为止。

  3. 它将以具体的方式进一步打破 SRP。假设我们的业务现在要求我们在项目激活时向用户发送电子邮件?现在,我们将电子邮件逻辑添加到 Item ORM 模型中,该模型的主要职责是对 Item 进行建模。它不应该关心电子邮件逻辑。这是一个案例 商业副作用. 。这些不属于 ORM 模型。

  4. 多元化很难。我见过成熟的 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

然而,使用:默认在您的迁移仍然是最彻底的方法。

如果列恰好是一个“状态”类型列,和你的模型适合于使用状态机,可以考虑使用的 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

这将完成设置为你的默认值。 (对我来说我有“欢迎来到队”),然后它会更进一步的保证总是有一个目前该对象的值。

希望帮助!

使用default_scope在导轨3

API文档

ActiveRecord的掩盖在数据库中(模式)默认定义并且在应用程序(模型)默认完成之间的差。在初始化过程中,它解析数据库架构和注意事项中指定没有任何缺省值。后来,在创建对象时,它分配那些架构指定的默认值而不触及该数据库。

讨论

从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

奇怪,他没有被这里建议

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top