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

366

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

  • 0
    Похоже, вы сами ответили на вопрос, в двух разных вариантах :)
  • 19
    Обратите внимание, что «стандартная» идиома Ruby для self.status = ACTIVE, если self.status не является self.status || = ACTIVE.
Показать ещё 4 комментария
Теги:
rails-activerecord

25 ответов

481
Лучший ответ

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

  • 7
    Это определенно кажется лучшим способом для достижения этой цели. Что действительно странно и неудачно. Разумный предпочтительный метод установки значений по умолчанию для атрибутов модели при создании выглядит так, как будто Rails уже должен был встроить. Единственный другой (надежный) способ, переопределяющий initialize , просто кажется действительно замысловатым для чего-то, что должно быть ясным и четко определенным. Я провел часы, просматривая документацию, прежде чем искать здесь, потому что я предполагал, что эта функция уже была где-то там, и я просто не знал об этом.
  • 101
    Одно замечание по этому self.bool_field ||= true - если у вас есть логическое поле, которое вы хотите использовать по умолчанию, не делайте self.bool_field ||= true , так как это заставит поле иметь значение true, даже если вы явно инициализируете его как false. Вместо этого сделайте self.bool_field = true if self.bool_field.nil? ,
Показать ещё 13 комментариев
43

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

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

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

  • 3
    Другая проблема - когда вы хотите использовать значение по умолчанию для внешнего ключа. Вы не можете жестко закодировать значение идентификатора в поле внешнего ключа, потому что на разных БД идентификатор может отличаться.
  • 2
    Еще одна проблема заключается в том, что таким образом вы не можете инициализировать непостоянные средства доступа (атрибуты, которые не являются столбцами БД).
Показать ещё 5 комментариев
37

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

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
    Это действительно важный момент. Я должен представить, что в большинстве случаев установка значения по умолчанию для записи выполняется только перед сохранением новой записи, а не при загрузке сохраненной записи.
  • 0
    Спасибо, чувак, ты спас мой день.
Показать ещё 2 комментария
16

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

  • 0
    Обратите внимание, что этот плагин позволяет :default значения по :default в миграциях схемы «просто работать» с Model.new .
  • 0
    Я могу получить значения :default в миграциях, чтобы «просто работать» с Model.new , вопреки тому, что Джефф сказал в своем посте. Проверенная работа в Rails 4.1.16.
14

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

В Rails 5+ вы можете использовать метод attribute в ваших моделях, например:

class Account < ApplicationRecord
  attribute :locale, :string, default: 'en'
end
  • 2
    аааа, это драгоценный камень, который я искал! default также может использовать proc, например default: -> {Time.current.to_date}
  • 0
    Убедитесь, что в качестве второго аргумента указан тип, в противном случае типом будет Value и никакое приведение типов не будет выполнено.
8

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

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

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

См. "Перезаписывание аксессуаров по умолчанию" в документация ActiveRecord:: Base и https://stackoverflow.com/questions/10127393/why-use-self-to-access-activerecord-rails-model-properties.

  • 0
    Статус все равно будет равен нулю в хэше, возвращаемом из attributes . Проверено в рельсах 5.2.0.
4

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

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

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

4

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

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

  • 2
    Обычно вы были бы правы, но вы никогда не должны переопределять инициализацию в модели ActiveRecord, так как это не всегда может быть вызвано. after_initialize этого вы должны использовать метод after_initialize .
  • 0
    Использование default_scope просто для установки значения по умолчанию, безусловно, неправильно. after_initialize - правильный ответ.
3

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


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

  • 0
    Большое спасибо за ваш комментарий. Не могли бы вы уточнить: (i) как / почему размещение этой логики инициализации в Item.rb (то есть в классе ApplicationRecord) нарушает SRP? Потому что когда мы инициализируем запись item мы хотим, чтобы каждый из них каждый раз инициализировался значениями, указанными внутри класса. (2) где бы мы поместили SellableItemFactory в приложение rails - т.е. в какую папку, чтобы мы могли впоследствии использовать его?
2

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

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

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

Вы хотите, чтобы форма new.html имела значение по умолчанию при открытии страницы? используйте https://stackoverflow.com/questions/328525/how-can-i-set-default-values-in-activerecord

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
1

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

Существует ли канонический способ установки значения по умолчанию для полей в Модель 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
1

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

Проблема с решениями 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 поддерживает его.

  • 0
    Это решение и предложенная альтернатива не работают в моем случае: у меня есть иерархия классов STI, где только один класс имеет этот атрибут, и соответствующий столбец, который будет использоваться в условиях запроса БД.
1
class Item < ActiveRecord::Base
  def status
    self[:status] or ACTIVE
  end

  before_save{ self.status ||= ACTIVE }
end
  • 2
    Мммм ... сначала кажется гениальным, но, подумав немного, я вижу несколько проблем. Во-первых, все значения по умолчанию находятся не в одной точке, а разбросаны по классу (представьте, что вы ищете их или меняете). Второе и худшее, позже вы не можете установить нулевое значение (или даже ложное!).
  • 0
    почему вам нужно установить нулевое значение по умолчанию? вы получаете это из коробки с AR, ничего не делая вообще. Что касается false при использовании логического столбца, то вы правы, это не лучший подход.
Показать ещё 3 комментария
0

https://github.com/keithrowell/rails_default_value

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

Я настоятельно рекомендую использовать "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

0

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

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

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

0

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

  • 0
    проверка происходит только перед сохранением? Что делать, если вы хотели показать значения по умолчанию перед сохранением?
  • 0
    @nurettin Это хороший момент, и я понимаю, почему вы иногда этого хотите, но ОП не упомянул это как требование. Вы должны решить для себя, хотите ли вы использовать дополнительные настройки по умолчанию для каждого экземпляра, даже если он не сохранен. Альтернативой является сохранение фиктивного объекта для повторного использования new действия.
0

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

  • 0
    Мне это нравится. Мне нужно было сделать def initialize(*); super; default_values; end в Rails 5.2.0. Кроме того, значение по умолчанию доступно даже в хэш-функции .attributes .
0

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

api doc

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

обсуждение

  • 0
    если вы используете meta_where, default_scope может не работать для назначения значений по умолчанию для новых объектов AR из-за ошибки.
  • 0
    эта проблема meta_where была исправлена [ metautilitary.lighthouseapp.com/projects/53011/tickets/…
Показать ещё 3 комментария
0

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

after_initialize :defaults

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

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

  • 4
    В Rails 3: метод after_initialize НЕ рекомендуется . На самом деле обратный вызов в стиле макроса, который вы приводите в качестве примера IS, устарел . Подробности: guides.rubyonrails.org/…
0

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

-3

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

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

  • 0
    before_validation не установит значения по умолчанию, пока объект не будет готов к сохранению. Если процессу необходимо прочитать значения по умолчанию перед сохранением, значения не будут готовы.
  • 0
    Вы никогда не устанавливаете значения по умолчанию во время проверок проверки в любом случае. Это даже не любой взлом. Сделайте это во время инициализации

Ещё вопросы

Сообщество Overcoder
Наверх
Меню