Как установить значения по умолчанию в ActiveRecord?

Как установить значение по умолчанию в 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

Я видел следующие примеры googling вокруг:

  def initialize 
    super
    self.status = ACTIVE unless self.status
  end

и

  def after_initialize 
    return unless new_record?
    self.status = ACTIVE
  end

Я также видел, как люди помещали его в свою миграцию, но я бы предпочел, чтобы он был определен в коде модели.

есть канонический способ установки значения по умолчанию для полей в модели ActiveRecord?

25 ответов


есть несколько проблем с каждым из доступных методов, но я считаю, что определение after_initialize обратный путь по следующим причинам:

  1. default_scope инициализирует значения для новых моделей, но затем это станет областью, в которой вы найдете модель. Если вы просто хотите инициализировать некоторые цифры 0, то это не то, что вы хотите.
  2. определение значений по умолчанию в миграции также работает часть времени... Как уже упоминалось, что это будет не работать, когда вы просто называете модель.новый.
  3. переопределение initialize может работать, но не забудьте позвонить super!
  4. использование плагина, такого как phusion, становится немного смешным. Это Руби, нам действительно нужен плагин только для инициализации некоторых значений по умолчанию?
  5. переопределение after_initialize осуждается по состоянию на Rails 3. Когда я переопределяю after_initialize в rails 3.0.3 я получаю следующее предупреждение в консоль:

предупреждение об устаревании: база#after_initialize устарела, пожалуйста, используйте базу.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. для логических полей do:

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

    см. комментарий пола Рассела к этому ответу для получения более подробной информации

  2. если вы выбираете только подмножество столбцов для модели (т. е. используете select запрос Person.select(:firstname, :lastname).all) вы получите MissingAttributeError если init метод обращается к a столбец, который не был включен в 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 (см. комментарий Клиффа Дарлинга ниже)


мы помещаем значения по умолчанию в базу данных через миграции (указав :default опция для каждого определения столбца) и пусть Active Record использует эти значения для установки значения по умолчанию для каждого атрибута.

IMHO, этот подход согласован с принципами AR: convention over configuration, DRY, определение таблицы управляет моделью, а не наоборот.

обратите внимание, что значения по умолчанию все еще находятся в коде приложения (Ruby), хотя и не в модели, но в миграции(ы).


некоторые простые случаи можно обработать, определив значение по умолчанию в схеме базы данных, но это не обрабатывает ряд более сложных случаев, включая вычисленные значения и ключи других моделей. Для этих случаев я делаю следующее:

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 не предусмотрен для этого очевидного случая использования, но я сделал это подтверждение того, что объект уже сохранен, указывает на то, что он не является новым.

увидев ответ Брэда Мюррея, это еще чище, если условие перемещается в запрос обратного вызова:

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

в Rails 5+, Вы можете использовать атрибут метод в вашей модели, напр.:

class Account < ApplicationRecord
  attribute :locale, :string, default: 'en'
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 при использовании self.


Я использую 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"

похожие вопросы, но все они имеют немного другой контекст: - как создать значение по умолчанию для атрибутов в модели Rails activerecord?

Лучший Ответ: зависит от того, что вы хотите!

если вы хотите, чтобы каждый объект начните со значения: use 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 

если вы хотите, чтобы каждый объект есть значение, вычисленное из пользовательского ввода: 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

вот для чего нужны конструкторы! Переопределите модель initialize метод.

использовать after_initialize метод.


Sup ребята, я закончил тем, что сделал следующее:

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

работает как шарм!


во-первых: я не согласен с ответом Джеффа. Это имеет смысл, когда ваше приложение мало и ваша логика проста. Я здесь пытаюсь дать представление о том, как это может быть проблемой при построении и поддержании крупных приложений. Я не рекомендую использовать этот подход сначала при создании чего-то небольшого, но иметь это в виду как альтернативный подход:


вопрос в том, является ли это значение по умолчанию для записей бизнес-логикой. Если бы это было так, я бы осторожно положить его в модель ORM. Так как поле ryw упоминает активный, это звучит как бизнес-логики. Е. Г. пользователь активный.

почему я должен быть осторожным, чтобы поместить деловые проблемы в модель ORM?

  1. Это разбивает SRP. Любой класс, наследующий от ActiveRecord:: Base, уже делает много различных вещей, главным из которых является согласованность данных (проверки) и постоянство (сохранение). Сдачи бизнес-логика, как бы мала она ни была, в с AR::Base breaks SRP.

  2. медленнее тестировать. Если я хочу проверить любую форму логики, происходящую в моей модели ORM, мои тесты должны инициализировать Rails для запуска. Это не будет слишком большой проблемой в начале вашего приложения, но будет накапливаться, пока ваши модульные тесты не займут много времени.

  3. оно сломает SRP даже больше вниз по линии, и в конкретных путях. Скажи "наше дело" сейчас. требует от нас, чтобы пользователи электронной почты, когда там элемент стал активным? Теперь мы добавляем логику электронной почты к модели ORM элемента, чья основная ответственность заключается в моделировании элемента. Он не должен заботиться о логике электронной почты. Это случай бизнес-побочные эффекты. Они не относятся к модели ORM.

  4. трудно разнообразить. Я видел зрелые приложения Rails с такими вещами, как база данных, поддерживаемая полем init_type: string, единственной целью которого является управление инициализацией логика. Это загрязняет базу данных, чтобы устранить структурную проблему. Думаю, есть способы получше.

путь 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 наборы активен для элемента, если он не имеет значения. Нет необходимости в инициализации рельсов, нет разрыва SRP. Когда инициализация элемента становится более продвинутой (например, установите поле состояния, тип по умолчанию и т. д.) ItemFactory может иметь это добавлено. Если мы получим два типа значений по умолчанию, мы можем создать новый BusinesCaseItemFactory для этого.

Примечание: также может быть полезно использовать инъекцию зависимостей здесь, чтобы позволить фабрике строить много активных вещей, но я оставил ее для простоты. Вот оно есть:self.new (klass = Item, attributes = {})


на это уже давно ответили, но мне часто нужны значения по умолчанию и я предпочитаю не помещать их в базу данных. Я создаю 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 для того, чтобы увидеть, какие значения по умолчанию устанавливаются БД для любой модели.

вопреки тому, что @Jeff Perrin состояния ответов (которые немного устарели), подход миграции даже будет применяться по умолчанию при использовании Model.new, из-за некоторых рельсов магии. Проверена работа в рельсах 4.1.16.

самая простая вещь часто является лучшей. Меньше долгов знаний и потенциальных точек путаницы в коде. И это 'просто работает'.

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

на null: false запрещает значения NULL в БД, и, в качестве дополнительного преимущества, он также обновляет все ранее существующие записи БД устанавливается со значением по умолчанию для этого и поле тоже. Вы можете исключить этот параметр в миграции, если хотите, но я нашел его очень удобным!

канонический путь в Rails 5+, как сказал @ Lucas Caton:

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 ("активный"). В в этом случае, я думаю, вам действительно нужно установить значение по умолчанию в ограничениях базы данных, если БД поддерживает его.


я столкнулся с проблемами с after_initialize дав ActiveModel::MissingAttributeError ошибки при выполнении сложных находок:

например:

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

"поиск" в .where хэш условий

поэтому я закончил тем, что сделал это, переопределив initialize следующим образом:

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, что делает этот драгоценный камень.

примеры:

ваш db по умолчанию равен NULL, ваша модель / ruby-default - "некоторая строка", но вы на самом деле хочу чтобы установить значение nil по любой причине:MyModel.new(my_attr: nil)

большинство решения здесь не смогут установить значение nil и вместо этого установят его по умолчанию.

Итак, вместо ||= подход, вы переключаетесь на my_attr_changed?...

но теперь представьте, что ваш db по умолчанию - "некоторая строка", ваша модель/ruby-default - "некоторая другая строка", но при определенном сценарии вы хочу чтобы установить значение " some string "(по умолчанию db):MyModel.new(my_attr: 'some_string')

в результате my_attr_changed? будучи false потому что значение соответствует значению db по умолчанию, которое, в свою очередь, запустит ваш код по умолчанию 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
, используя :по умолчанию В перенос-это еще отличный способ.

Я обнаружил, что использование метода проверки обеспечивает большой контроль над настройкой по умолчанию. Вы даже можете установить значения по умолчанию (или сбой проверки) для обновлений. Вы даже установили другое значение по умолчанию для inserts vs updates, если вы действительно этого хотели. Обратите внимание, что значение по умолчанию не будет установлено до #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


Если столбец является столбцом типа "статус", и ваша модель поддается использованию государственных машин, рассмотрите возможность использования аасм, после чего вы можете просто сделать

  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

используйте default_scope в rails 3

api doc

ActiveRecord скрывает разницу между дефолтом, определенным в базе данных (схеме), и дефолтом, выполненным в приложении (модели). Во время инициализации он анализирует схему базы данных и отмечает любые значения по умолчанию, указанные там. Позже, при создании объектов, он назначает указанные схемой значения по умолчанию, не касаясь базы данных.

обсуждение


из документов 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

удивлен, что его не было предложено здесь