2008-11-30 3 views
372

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

я видел следующие примеры прибегая к помощи вокруг:

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

и

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

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

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

+0

Похоже, вы ответили себе на вопрос, в двух разных вариантах :) – 2008-11-30 10:48:10

+17

Обратите внимание, что «стандарт» Рубин идиома " self.status = ACTIVE, если self.status 'is' self.status || = ACTIVE ' – 2008-11-30 14:48:12

ответ

515

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

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

Deprecation ПРЕДУПРЕЖДЕНИЕ: Базовый # after_initialize устарел, используйте вместо этого метод Base.after_initialize:. (Вызывается из/Users/Me/MYAPP/приложение/модели/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. Для логических полей сделать:

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

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

  2. Если вы только выбор подмножества столбцов для модели (т. е. используя 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 Дарлинга комментарий ниже)

16

Ребята из Phusion имеют приятный plugin для этого.

4

Для чего предназначены конструкторы! Переопределите метод модели initialize.

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

+2

Обычно вы были бы правы, но вы никогда не должны переопределять инициализацию в модели ActiveRecord, поскольку это может не всегда вызываться. Вместо этого вы должны использовать метод `after_initialize`. – 2009-11-13 00:34:48

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

    before_save{ self.status ||= ACTIVE } 
end 
+2

Mmmhh ... кажется гениальным сначала, но, немного подумав, я вижу несколько проблем. Во-первых, все значения по умолчанию не находятся в одной точке, а разбросаны по классу (представьте, что они ищут или меняют их). Второе и худшее, yo не может поместить позднее значение null (или даже ложное!). – paradoja 2008-11-30 14:22:49

+0

Зачем вам нужно установить значение по умолчанию по умолчанию? вы получаете это из коробки с AR, не делая ничего вообще. Что касается false при использовании логического столбца, тогда вы правы, это не лучший подход. – 2008-11-30 14:44:33

+0

Я не могу говорить о других привычках кодирования. У меня не было проблемы, потому что я не разбрасываю свои геттеры/сеттеры вокруг файла класса. Кроме того, любой современный текстовый редактор должен облегчить навигацию к методу (shift-cmd-t в textmate). – 2008-11-30 14:48:25

43

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

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

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

+3

Другая проблема заключается в том, когда вы хотите использовать значение по умолчанию для внешнего ключа.Вы не можете жестко закодировать значение идентификатора в поле внешнего ключа, потому что в разных БД идентификатор может отличаться. – shmichael 2010-08-30 09:50:32

0

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

4

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

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

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

0

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

after_initialize :defaults 

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

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

+4

В Rails 3: ** метод `after_initialize` ** ** НЕ устарел **. Фактически, ** обратный вызов макрокоманды ** вы приводите пример ** IS устарел **. Подробности: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find – Zabba 2010-12-28 06:42:13

-1

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

api doc

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

discussion

+0

, если вы используете meta_where, default_scope может не работать для назначения значений по умолчанию новым объектам AR из-за ошибки. – 2011-03-01 20:56:06

0

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

например:

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

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

Поэтому я закончил это, переопределив инициализацию в t его путь:

def initialize 
    super 
    default_values 
end 

private 
def default_values 
    self.date_received ||= Date.current 
end 

super вызова необходимо убедиться, что объект правильно инициализация от ActiveRecord::Base, прежде чем делать свой подгоняет код, то есть: default_values ​​

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 также вызывается для каждого объекта, возвращенного: найти: http://guides.rubyonrails.org/active_record_validations_callbacks.html#after_initialize-and-after_find

-2

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

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

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_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 
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 поддерживает его.

16

after_initialize модель обратного вызова может быть улучшена просто выполнив следующие

after_initialize :some_method_goes_here, :if => :new_record? 

Это имеет нетривиальное пользу, если ваш код инициализации должен иметь дело с ассоциациями, как и следующий код вызывает тонкий п + 1 если вы прочитаете начальную запись, не включив ее.

class Account 

    has_one :config 
    after_initialize :init_config 

    def init_config 
    self.config ||= build_config 
    end 

end 
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" 
0

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

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

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

3

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


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

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

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

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

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

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

The 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 для этого.

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

1

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

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

Примеры:

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

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

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

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

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


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

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

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 
3

Подобные вопросы, но все они имеют несколько иной контекст: - How do I create a default value for attributes in Rails activerecord's model?

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

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

Если вы хотите каждый объект имеют значение, рассчитанное из пользовательского ввода: использование before_save :default_values Вы хотите пользователю ввести X и затем Y = X+'foo'? Использование:

class Task < ActiveRecord::Base 
    before_save :default_values 
    def default_values 
    self.status ||= 'P' 
    end 
end 
16

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

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

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

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

class Item < ActiveRecord::Base 
    attribute :scheduler_type, :string, default: 'hotseat' 
end 
Смежные вопросы