首页 文章

如何在ActiveRecord中设置默认值?

提问于
浏览 1870
393

如何在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模型中的字段设置默认值?

25 回答

  • 531
    class Item < ActiveRecord::Base
      def status
        self[:status] or ACTIVE
      end
    
      before_save{ self.status ||= ACTIVE }
    end
    
  • 43

    在遇到复杂的发现时,我遇到了 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

  • 39

    比建议的答案更好/更清晰的潜在方式是覆盖访问者,如下所示:

    def status
      self['status'] || ACTIVE
    end
    

    请参阅the ActiveRecord::Base documentationmore from StackOverflow on using self中的"Overwriting default accessors" .

  • 28

    只需执行以下操作即可改进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
    
  • 17

    如果列恰好是'status'类型列,并且您的模型适合使用状态机,请考虑使用aasm gem,之后您可以简单地执行

    aasm column: "status" do
        state :available, initial: true
        state :used
        # transitions
      end
    

    它仍然没有比使用 init 或其他任何东西滚动你自己更清洁,并且你获得了aasm的其他好处,例如所有状态的范围 .

  • 16

    苏家伙,我最后做了以下事情:

    def after_initialize 
     self.extras||={}
     self.other_stuff||="This stuff"
    end
    

    奇迹般有效!

  • 8

    每种可用方法都存在几个问题,但我认为定义 after_initialize 回调是出于以下原因的方法:

    • default_scope 将初始化新模型的值,但随后将成为您找到模型的范围 . 如果您只想将某些数字初始化为0,那么这不是您想要的 .

    • 在迁移中定义默认值也可以在部分时间内工作...正如已经提到的那样,当您只调用Model.new时,这将不起作用 .

    • 覆盖 initialize 可以正常工作,但不要忘记拨打 super

    • 使用像phusion这样的插件有点荒谬 . 这是红宝石,我们真的需要一个插件来初始化一些默认值吗?

    • 覆盖 after_initialize is deprecated 从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 回调,它允许你默认属性,除了让你设置关联的默认值,如下所示:

    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
    

    现在您有 just one 位置来查找模型的初始化 . 我正在使用这种方法,直到有人想出一个更好的方法 .

    注意事项:

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

    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?

    另请注意,Rails 3.2之前的语法不同(请参阅下面的Cliff Darling的评论)

  • 8

    https://github.com/keithrowell/rails_default_value

    class Task < ActiveRecord::Base
      default :status => 'active'
    end
    
  • 7

    我们通过迁移(通过在每个列定义上指定 :default 选项)将默认值放在数据库中,并让Active Record使用这些值来设置每个属性的默认值 .

    恕我直言,这种方法符合AR的原则:约定优于配置,DRY,表定义驱动模型,而不是相反 .

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

  • 4

    Phusion的家伙有一些不错的plugin .

  • 4

    after_initialize解决方案的问题在于,无论是否访问此属性,都必须向从数据库中查找的每个对象添加after_initialize . 我建议采用一种懒惰的方法 .

    属性方法(getters)当然是方法本身,因此您可以覆盖它们并提供默认值 . 就像是:

    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') . 在这种情况下,如果数据库支持,我认为你真的需要在数据库约束中设置默认值 .

  • 4

    我强烈建议使用"default_value_for" gem:https://github.com/FooBarWidget/default_value_for

    有一些棘手的场景几乎需要覆盖初始化宝石所做的方法 .

    例子:

    您的db默认值为NULL,您的model / ruby定义的默认值为"some string",但您实际上想要将值设置为nil,无论出于何种原因: MyModel.new(my_attr: nil)

    此处的大多数解决方案都无法将值设置为nil,而是将其设置为默认值 .

    好的,所以不要采用 ||= 方法,而是切换到 my_attr_changed? ...

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

    这将导致 my_attr_changed?false ,因为该值与db default相匹配,而db default又将触发ruby定义的默认代码并将值设置为"some other string" - 再次,而不是您想要的值 .


    由于这些原因,我不认为只使用after_initialize钩子就可以实现这一点 .

    再一次,我认为"default_value_for"宝石正在采取正确的方法:https://github.com/FooBarWidget/default_value_for

  • 3

    我也看到人们把它放在他们的迁移中,但我宁愿看到它在模型代码中定义 . 是否有规范方法为ActiveRecord模型中的字段设置默认值?

    在Rails 5之前,规范的Rails方式实际上是在迁移中设置它,只要想看看DB为任何模型设置的默认值,只需查看 db/schema.rb 即可 .

    与@Jeff Perrin回答的状态(有点旧)相反,由于某些Rails魔法,迁移方法甚至在使用 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中的NULL值,并且作为额外的好处,它还会更新所有预先存在的DB记录,并使用此字段的默认值进行设置 . 如果您愿意,可以在迁移中排除此参数,但我发现它非常方便!

    正如@Lucas Caton所说,Rails 5中的规范方式是:

    class Item < ActiveRecord::Base
      attribute :scheduler_type, :string, default: 'hotseat'
    end
    
  • 3

    不推荐使用after_initialize方法,而是使用回调 .

    after_initialize :defaults
    
    def defaults
      self.extras||={}
      self.other_stuff||="This stuff"
    end
    

    但是,在迁移中使用 :default 仍然是最干净的方法 .

  • 1

    在Rails 5中,您可以在模型中使用attribute方法,例如:

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

    我用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"
    
  • 1

    类似的问题,但都有不同的背景: - How do I create a default value for attributes in Rails activerecord's model?

    最佳答案: Depends on What You Want!

    If you want every object to 以值开头:使用 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
    

    If you want every object to 具有根据用户输入计算的值:use before_save :default_values 您希望用户输入 X ,然后输入 Y = X+'foo' ?使用:

    class Task < ActiveRecord::Base
      before_save :default_values
      def default_values
        self.status ||= 'P'
      end
    end
    
  • 1

    在rails 3中使用default_scope

    api doc

    ActiveRecord模糊了数据库(模式)中定义的默认值和应用程序(模型)中的默认值之间的差异 . 在初始化期间,它解析数据库模式并记录其中指定的任何默认值 . 稍后,在创建对象时,它会分配这些模式指定的默认值,而不会触及数据库 .

    discussion

  • 0

    这就是构造函数的用途!覆盖模型的初始化方法 .

    使用 after_initialize 方法 .

  • 0

    首先要做的是:我不同意杰夫的回答 . 当您的应用程序很小而且逻辑简单时,这是有道理的 . 我在这里试图在构建和维护更大的应用程序时深入了解它是如何成为一个问题 . 我不建议在构建小的东西时首先使用这种方法,但要记住它作为替代方法:


    这里的一个问题是记录的默认值是否为业务逻辑 . 如果是的话,我会谨慎地把它放在ORM模型中 . 由于字段ryw提及是活跃的,这听起来像商业逻辑 . 例如 . 用户是活动的 .

    为什么我要谨慎将业务问题放在ORM模型中?

    • 它打破SRP . 从ActiveRecord :: Base继承的任何类已经做了很多不同的事情,其中主要是数据一致性(验证)和持久性(保存) . 使用AR :: Base将业务逻辑(尽管很小)打破SRP .

    • 测试速度较慢 . 如果我想测试我的ORM模型中发生的任何形式的逻辑,我的测试必须初始化Rails才能运行 . 这在您的应用程序开始时不会出现太多问题,但会累积,直到您的单元测试需要很长时间才能运行 .

    • 它将以更具体的方式打破SRP . 假设我们的业务现在要求我们在项目变为活动时向用户发送电子邮件?现在我们正在向Item ORM模型添加电子邮件逻辑,其主要职责是为Item建模 . 它不应该关心电子邮件逻辑 . 这是商业副作用的一个例子 . 这些不属于ORM模型 .

    • 很难实现多样化 . 我见过成熟的Rails应用程序,例如数据库支持的init_type:string字段,其唯一目的是控制初始化逻辑 . 这会污染数据库以修复结构问题 . 我相信有更好的方法 .

    The PORO way: 虽然这是一些代码,但它允许您将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,attributes = {})

  • 0

    这已经回答了很长时间,但我经常需要默认值,而不想将它们放在数据库中 . 我创建了一个 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
    
  • 0

    虽然在大多数情况下,为设置默认值而这样做会让人感到困惑和尴尬,但您也可以使用 :default_scope . 看看squil's comment here .

  • 0

    我发现使用验证方法可以很好地控制设置默认值 . 您甚至可以为更新设置默认值(或失败验证) . 如果你真的想要,你甚至可以为插入和更新设置不同的默认值 . 请注意,在#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也被返回的每个对象调用:find:http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find

  • -1

    一些简单的情况可以通过在数据库模式中定义默认值来处理,但是不能处理许多棘手的情况,包括计算值和其他模型的键 . 对于这些情况,我这样做:

    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的回答,如果条件转移到回调请求,这甚至更清晰:

    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
    
  • -2

    来自api文档http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html在模型中使用 before_validation 方法,它为您提供了为创建和更新调用创建特定初始化的选项,例如在此示例中(再次从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
    

    感到惊讶的是,这里没有他的建议

相关问题