Rails 4 создает связанный объект при сохранении
Как я могу автоматически создать несколько связанных объектов сразу после сохранения нового первичного объекта?
Например
В Rails 4 у меня есть три объекта: Бизнес, Бюджеты и Категории.
#app/models/business.rb
class Business < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
#app/models/budget.rb
class Budget < ActiveRecord::Base
#attrs id, business_id, department_id, value
belongs_to :business
belongs_to :category
end
#app/models/category.rb
class Category < ActiveRecord::Base
#attrs id, name
has_many :budgets
end
Когда я создаю новый Бизнес, после сохранения нового Бизнеса я хотел бы атомарно создать Бюджет для каждой Категории и присвоить ему значение $0. Таким образом, когда я иду, чтобы показать или отредактировать новый Бизнес, у него уже будут связанные Категории и Бюджеты, которые затем могут быть отредактированы. Таким образом, при создании нового Бизнеса будет создано несколько новых Бюджетов, по одному для каждой Категории, каждый со значением 0.
Я прочитал эту статью: Rails 3, как добавить связанную запись после создания первичной записи (Books, Auto Add BookCharacter)
И мне интересно, должен ли я использовать обратный вызов after_create в бизнес-модели и иметь логику, существующую в контроллере Budgets (не совсем уверен, как это сделать), или мне следует добавить логику в business_controller.rb в "новом" позвонить с чем-то похожим на:
@business = Business.new
@categories = Category.all
@categories.each do |category|
category.budget.build(:value => "0", :business_id => @business.id)
end
3 ответа
Я закончил тем, что добавил логику в метод create в контроллере Business для циклического прохождения всех категорий и создания бюджета сразу после сохранения. Обратите внимание, что я был ленив и не вставлял обработки ошибок.:
def create
@business = Business.new(params[:business])
@results = @business.save
@categories = Categories.all
@categories.each do |category|
category.budgets.create(:amount => "0", :business_id => @business.id)
end
respond_to do |format|
...
end
end
По моему опыту, лучше избегать использования обратных вызовов, если это не связано с постоянством данной модели. В этом случае предоставление бюджету возможности установить собственное значение по умолчанию, если оно не указано, является хорошим вариантом использования обратного вызова. Это также устраняет некоторые сложности из вашей логики.
class Budget
before_validate :set_value
...
private
def set_value
self.value ||= 0
end
end
В остальном, я бы создал пользовательские классы, каждый из которых несет единую ответственность, чтобы систематически создавать новый бизнес. Вот пример. Имейте в виду, что это не предназначено для копирования и вставки, это просто для иллюстрации концепции:
class BusinessGenerator < Struct.new(:business_params)
attr_reader :business
def generate
create_business
create_budgets
end
private
def create_business
@business = Business.create!(business_params)
end
def create_budgets
BudgetGenerator.new(@business).create
end
end
class BudgetGenerator < Struct.new(:business)
def generate
categories.each do |c|
business.budgets.create!(category: c)
end
end
private
def categories
Category.all
end
end
Это хорошо, потому что он разделяет проблемы и легко расширяем, тестируем и не использует магию Rails, как acceptpts_nested_attributes_for. Например, если в будущем вы решите, что не всем предприятиям нужен бюджет в каждой категории, вы можете легко передать те, которые вы хотите, в качестве аргумента в BudgetGenerator.
Вы создадите экземпляр класса BusinessGenerator в контроллере:
class BusinessController < ActiveRecord::Base
...
def create
generator = BusinessGenerator.new(business_params)
if generator.generate
flash[:success] = "Yay"
redirect_to generator.business
else
render :new
end
end
...
end
Некоторые проблемы, которые могут возникнуть при таком подходе:
- Возврат ошибок проверки в вашу бизнес-форму
- Если создание бюджета не удается, вы застряли в бизнесе без бюджета. Вы не можете ждать, чтобы сохранить бизнес, пока не будут созданы бюджеты, потому что нет идентификатора, который нужно связать. Возможно, рассмотрите возможность размещения транзакции внутри метода генератора.
Невзирая на Brent Eicher
Отличный совет, я никогда не испытывал ничего плохого от использования обратных вызовов. Если вы не возражаете против их использования, вы можете сделать следующее (если вы устанавливаете бюджет на 0
каждый раз):
#app/models/business.rb
class Business < ActiveRecord::Base
before_create :build_budgets
private
def build_budgets
Category.all.each do |category|
self.budgets.build(category: category, value: "0")
end
end
end
-
Кроме того, вы должны убедиться, что ваш budget
внешние ключи верны.
Я вижу, у вас есть department_id
когда Budget belongs_to Category
, Вы должны сделать это category_id
или определите Foreign_key:
#app/models/budget.rb
class Budget < ActiveRecord::Base
belongs_to :category, foreign_key: "department_id"
end