Как установить значения по умолчанию в 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, такие как области для всех ваших статусов.
Ответ 23
https://github.com/keithrowell/rails_default_value
class Task < ActiveRecord::Base
default :status => 'active'
end
Ответ 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
Удивлен, что его здесь не предложили.