关于ruby on rails:如何在ActiveRecord中设置默认值?

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模型中,是否有一种规范化的方法来设置字段的默认值?


每个可用的方法都有几个问题,但我认为定义after_initialize回调是一种可行的方法,原因如下:

  • default_scope将初始化新模型的值,但这将成为您找到模型的范围。如果您只想将一些数字初始化为0,那么这不是您想要的。
  • 在迁移中定义默认值也会在一段时间内起作用…正如前面提到的,当您只调用model.new时,这将不起作用。
  • 覆盖initialize可以工作,但不要忘记调用super
  • 使用类似phfusion的插件有点可笑。这是Ruby,我们真的需要一个插件来初始化一些默认值吗?
  • 从Rails 3开始,不赞成重写after_initialize。当我在Rails 3.0.3中覆盖after_initialize时,我在控制台中收到以下警告:
  • 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)

    因此,我想说,编写一个after_initialize回调,它除了允许您对这样的关联设置默认值外,还允许您使用默认属性:

    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:

  • 对于布尔字段,请执行以下操作:

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

    更多详细信息,请参阅Paul Russell对此答案的评论。

  • 如果您只为模型选择列的子集(即在像Person.select(:firstname, :lastname).all这样的查询中使用select),那么如果您的init方法访问的列没有包含在select子句中,您将得到MissingAttributeError。你可以这样防范这个案子:

    self.number ||= 0.0 if self.has_attribute? :number

    对于布尔列…

    self.bool_field = true if (self.has_attribute? :bool_value) && self.bool_field.nil?

    还要注意,在Rails3.2之前的语法是不同的(请参见下面的CliffDarling的评论)。


  • 我们通过迁移(通过在每个列定义上指定:default选项)将默认值放入数据库中,并让活动记录使用这些值为每个属性设置默认值。

    imho,这种方法符合ar:convention over configuration、dry的原则,表定义驱动模型,而不是相反。

    请注意,默认值仍然在应用程序(Ruby)代码中,虽然不在模型中,但在迁移中。


    轨道5 +

    您可以在模型中使用属性方法,例如:

    1
    2
    3
    class Account < ApplicationRecord
      attribute :locale, :string, default: 'en'
    end

    还可以将lambda传递给default参数。例子:

    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有一些不错的插件。


    我用的是attribute-defaults宝石

    从文档中:运行sudo gem install attribute-defaults并将require 'attribute_defaults'添加到应用程序中。

    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的模型中为属性创建默认值?

    最佳答案:取决于你想要什么!

    如果希望每个对象都以一个值开头:使用after_initialize :init

    打开页面时是否希望new.html表单具有默认值?使用https://stackoverflow.com/a/5127684/1536309

    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

    如果希望每个对象都有一个根据用户输入计算的值:使用before_save :default_values。您想让用户输入X,然后输入Y = X+'foo'?用途:

    1
    2
    3
    4
    5
    6
    class Task < ActiveRecord::Base
      before_save :default_values
      def default_values
        self.status ||= 'P'
      end
    end


    这就是构造函数的作用!覆盖模型的initialize方法。.

    使用after_initialize方法。


    伙计们,我最后做了以下事情:

    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=)


    这个问题已经回答了很长一段时间,但是我经常需要默认值,而不希望将它们放在数据库中。我创建了一个DefaultValues关注点:

    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方法实际上是在迁移中设置它,只要查看db/schema.rb中是否有任何时候想要查看DB为任何模型设置的默认值。

    与@jeff perrin answer所说的(有点旧)相反,迁移方法甚至会在使用Model.new时应用默认值,这是由于一些Rails的魔力。已验证在轨道4.1.16中的工作。

    最简单的事情往往是最好的。减少知识债务和代码库中潜在的混淆点。它"只是起作用"。

    1
    2
    3
    4
    5
    class AddStatusToItem < ActiveRecord::Migration
      def change
        add_column :items, :scheduler_type, :string, { null: false, default:"hotseat" }
      end
    end

    null: false不允许在数据库中使用空值,而且作为一个附加的好处,它还更新了所有预先存在的数据库记录,并使用该字段的默认值进行设置。如果您愿意,可以在迁移中排除这个参数,但我发现它非常方便!

    正如@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


    我遇到了与after_initialize的问题,在做复杂的发现时给ActiveModel::MissingAttributeError的错误:

    如:

    1
    @bottles = Bottle.includes(:supplier, :substance).where(search).order("suppliers.name ASC").paginate(:page => page_no)

    .where中的"搜索"是条件的散列

    因此,我以这种方式重写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

    在执行自定义代码(即:默认值)之前,需要执行super调用,以确保从ActiveRecord::Base正确初始化对象。


    后初始化解决方案的问题是,无论是否访问此属性,都必须向从数据库中查找的每个对象添加后初始化。我建议采用一种懒惰的加载方式。

    属性方法(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,原因是:MyModel.new(my_attr: nil)

    这里的大多数解决方案将无法将值设置为零,而是将其设置为默认值。

    好吧,所以你不采用||=方法,而改用my_attr_changed?方法……

    但是现在假设您的数据库默认值是"some string",您的模型/ruby定义的默认值是"some other string",但是在某种情况下,您希望将该值设置为"some string"(数据库默认值):MyModel.new(my_attr: 'some_string')

    这将导致my_attr_changed?为false,因为该值与db default匹配,而db default又会激发Ruby定义的默认代码,并将该值设置为"其他字符串"——同样,这不是您想要的。

    基于这些原因,我认为仅仅使用一个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

    这将为您设置默认值。(对于我来说,我有"欢迎加入团队"),然后它将进一步确保该对象始终存在一个值。

    希望有帮助!


    尽管在大多数情况下,设置默认值会让人困惑和尴尬,但也可以使用:default_scope。在这里查看斯奎尔的评论。


    如果列恰好是"status"类型的列,并且您的模型将自己借给了状态机的使用,那么可以考虑使用aasm gem,之后您可以简单地执行此操作。

    1
    2
    3
    4
    5
      aasm column:"status" do
        state :available, initial: true
        state :used
        # transitions
      end

    它仍然不能初始化未保存记录的值,但是它比使用init或其他任何工具滚动自己的值要干净一些,而且您还可以从aasm中获得其他好处,比如针对所有状态的作用域。


    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在模型中使用before_validation方法,它为创建和更新调用提供了创建特定初始化的选项。例如,在本例中(同样是从api docs示例中获取的代码),对信用卡的数字字段进行初始化。您可以很容易地对此进行调整,以设置所需的任何值。

    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

    奇怪的是这里没有人建议他