Rails 4 Create Associated Object on Save

↘锁芯ラ 提交于 2020-01-05 21:13:13

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!