问题
How can I create multiple associated objects automatically just after I save a new primary object?
For example
In Rails 4, I have three objects: Businesses, Budgets, and Categories.
#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
When I create a new Business, after saving the new Business, I would like to atomically create a Budget for each Category and give it value of $0. This way, when I go to show or edit a new Business, it will already have the associated Categories and Budgets, which can then be edited. Thus, upon creating a new Business, multiple new Budgets will be created, one for each Category, each with the value of 0.
I read this article: Rails 3, how add a associated record after creating a primary record (Books, Auto Add BookCharacter)
And I am wondering if I should use the after_create callback in the Business model and have the logic then exist in the Budgets controller (not exactly sure how to do this) or if I should add logic to the businesses_controller.rb in the 'new' call with something similar to:
@business = Business.new
@categories = Category.all
@categories.each do |category|
category.budget.build(:value => "0", :business_id => @business.id)
end
回答1:
In my experience, it's best to avoid using callbacks unless it relates to a given model's persistence. In this case, letting a budget set it's own default value when one isn't supplied is good use of a callback. That also removes some complexity from your logic.
class Budget
before_validate :set_value
...
private
def set_value
self.value ||= 0
end
end
For the rest, I would create custom classes, each with a single responsibility, to systematically generate a new business. Here's an example. Keep in mind that this is not meant to be copy and pasted, it's just to illustrate a concept:
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
This is nice because it separates concerns and is easily extensible, testable and doesn't use Rails magic like accepts_nested_attributes_for. For example, if in the future you decide that not all businesses need a budget in every category, you can easily pass the ones you want as an argument to BudgetGenerator.
You'll instantiate the BusinessGenerator class in the controller:
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
Some sticking points you might have with this approach include:
- Returning validation errors to your business form
- If the creation of a budget fails, you're stuck with a budget-less business. You can't wait to save business until after the budgets are created because there is no id to associate. Perhaps consider putting a transaction inside of the generator method.
回答2:
Regardless of Brent Eicher
's great advice, I've never experienced anything bad from using callbacks. If you don't mind using them, you could do the following (if you're setting the budget at 0
each time):
#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
--
Also, you need to make sure your budget
foreign keys are correct.
I see you have department_id
when Budget belongs_to Category
. You should make this category_id
or define the foreign_key:
#app/models/budget.rb
class Budget < ActiveRecord::Base
belongs_to :category, foreign_key: "department_id"
end
回答3:
I ended up adding the logic to the create method in the Business controller to loop through all Categories and create a budget just after save. Note that I was lazy and didn't put in any error handling. :
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
来源:https://stackoverflow.com/questions/33702536/rails-4-create-associated-object-on-save