Как я могу установить значения по умолчанию в ActiveRecord? - PullRequest
402 голосов
/ 30 ноября 2008

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

Я вижу сообщение от Пратика, которое описывает уродливый, сложный кусок кода: 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?

Ответы [ 26 ]

4 голосов
/ 25 июля 2015

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


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

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

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

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

  3. Это сломает SRP еще больше, и конкретными способами. Скажем, теперь наш бизнес требует, чтобы мы отправляли электронные письма пользователям, когда там Предмет станет активным? Теперь мы добавляем логику электронной почты в модель Item ORM, основной задачей которой является моделирование Item. Это не должно заботиться о логике электронной почты. Это случай бизнес-побочных эффектов . Они не принадлежат модели 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 устанавливает активный элемент, если он не имеет значения. Не требуется инициализация Rails, не прерывается SRP. Когда инициализация элемента становится более сложной (например, установка поля состояния, типа по умолчанию и т. Д.), ItemFactory может добавить это. Если мы получим два типа значений по умолчанию, мы можем создать новый BusinesCaseItemFactory для этого.

ПРИМЕЧАНИЕ: здесь также может быть полезно использовать внедрение зависимостей, чтобы позволить фабрике создавать много активных объектов, но я упустил это для простоты. Вот оно: self.new (класс = Элемент, атрибуты = {})

4 голосов
/ 27 июля 2010

Слушайте, ребята, в итоге я сделал следующее:

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

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

3 голосов
/ 15 февраля 2016

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

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

Есть ли канонический способ установить значение по умолчанию для полей в Модель 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+, как сказал @Lucas Caton:

class Item < ActiveRecord::Base
  attribute :scheduler_type, :string, default: 'hotseat'
end
1 голос
/ 09 февраля 2011

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

1 голос
/ 30 ноября 2008
class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end
1 голос
/ 08 сентября 2012

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

1 голос
/ 18 сентября 2015

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

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

Примеры:

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

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

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

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

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


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

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

0 голосов
/ 08 февраля 2019

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

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

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

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

 validates :new_team_signature, presence: true

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

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

0 голосов
/ 21 июля 2010

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

...