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

Ответ 1

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

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

ПРЕДУПРЕЖДЕНИЕ О ДЕПРЕКАЦИИ: база # 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 nil
      self.address ||= build_address #let you set a default association
    end
  end    

Теперь у вас есть только одно место для поиска инициализации ваших моделей. Я использую этот метод, пока кто-то не придумает лучшего.

Предостережения:

  • Для булевых полей do:

    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 ниже)

Ответ 2

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

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

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

Ответ 3

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

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

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

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

Ответ 4

Рельсы 5+

Вы можете использовать метод атрибута в своих моделях, например:

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

Вы также можете передать лямбду в параметр по default. Пример:

attribute :uuid, UuidType.new, default: -> { SecureRandom.uuid }

Ответ 5

Обратный шаблон обратного вызова 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

Ответ 6

У парней Phusion есть хороший плагин для этого.

Ответ 7

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

Ответ 8

Еще лучший/более чистый потенциальный путь, чем предлагаемые ответы, - это перезаписать аксессор, например:

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

См. "Перезаписывание аксессуаров по умолчанию" в документация ActiveRecord:: Base и fooobar.com/questions/18100/....

Ответ 9

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

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

Если вы хотите, чтобы каждый объект начинался со значения: используйте after_initialize :init

Вы хотите, чтобы форма new.html имела значение по умолчанию при открытии страницы? используйте fooobar.com/questions/17970/...

class Person < ActiveRecord::Base
  has_one :address
  after_initialize :init

  def init
    self.number  ||= 0.0           #will set the default value only if it nil
    self.address ||= build_address #let 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

Ответ 10

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

Используйте метод after_initialize.

Ответ 11

Ребята, я закончил делать следующее:

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

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

Ответ 12

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


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

Почему я должен с осторожностью относиться к проблемам бизнеса в модели ORM?

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

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

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

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

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

Ответ 13

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

Ответ 14

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

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

Канонический Rails путь, прежде чем Rails 5, фактически должен был установить его в переносе и просто посмотреть в db/schema.rb для всякий раз, когда вам нужно посмотреть, какие значения по умолчанию задаются БД для любой модели.

В отличие от того, что говорит @Jeff Perrin (что немного устарело), ​​подход к миграции даже применит значение по умолчанию при использовании Model.new из-за некоторой магии Rails. Проверено, работает в Rails 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+ есть, как сказал @Лукас Катон:

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

Ответ 15

class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end

Ответ 16

Я столкнулся с проблемами с 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 ​​

Ответ 17

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

Ответ 18

Я настоятельно рекомендую использовать "default_value_for" gem: https://github.com/FooBarWidget/default_value_for

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

Примеры:

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

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

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

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

Это приведет к тому, что my_attr_changed? будет false, потому что значение соответствует умолчанию db, которое, в свою очередь, приведет к вашему стандарту по умолчанию, определенному рубином, и установит значение в "некоторая другая строка" - опять же, не то, что вы хотели.


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

Опять же, я думаю, что камень "default_value_for" использует правильный подход: https://github.com/FooBarWidget/default_value_for

Ответ 19

Несмотря на то, что для установки значений по умолчанию в большинстве случаев сложно и неудобно, вы также можете использовать :default_scope. Отметьте комментарий squil здесь.

Ответ 20

Метод after_initialize устарел, вместо этого используйте обратный вызов.

after_initialize :defaults

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

однако использование : по умолчанию в ваших миграциях по-прежнему является самым чистым способом.

Ответ 21

Я обнаружил, что использование метода проверки дает большой контроль над настройками по умолчанию. Вы можете даже устанавливать значения по умолчанию (или отказоустойчивость) для обновлений. Вы даже устанавливаете другое значение по умолчанию для вставок 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 также вызывается каждым объектом, возвращаемым: find: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find

Ответ 22

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

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

Он по-прежнему не инициализирует значение для несохраненных записей, но немного чище, чем перематывает свой собственный с помощью init или что-то еще, и вы извлекаете другие преимущества aasm, такие как области для всех ваших статусов.

Ответ 24

Вот решение, которое я использовал, что я был немного удивлен, еще не было добавлено.

Есть две части к этому. Первая часть устанавливает значение по умолчанию для фактической миграции, а вторая часть добавляет проверку в модель, гарантирующую, что присутствие является истинным.

add_column :teams, :new_team_signature, :string, default: 'Welcome to the Team'

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

 validates :new_team_signature, presence: true

Что это будет делать, это установить значение по умолчанию для вас. (для меня у меня есть "Добро пожаловать в команду"), а затем он пойдет на шаг вперед, гарантируя, что для этого объекта всегда присутствует значение.

Надеюсь, это поможет!

Ответ 25

использовать default_scope в рельсах 3

api doc

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

обсуждение

Ответ 26

Из api docs 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

Удивлен, что его здесь не предложили.