Rails 4 在保存时创建关联对象

Posted

技术标签:

【中文标题】Rails 4 在保存时创建关联对象【英文标题】:Rails 4 Create Associated Object on Save 【发布时间】:2015-11-13 22:07:08 【问题描述】:

如何在保存新的主对象后自动创建多个关联对象?

例如

在 Rails 4 中,我有三个对象:BusinessesBudgetsCategories

#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, how add a associated record after creating a primary record (Books, Auto Add BookCharacter)

我想知道我是否应该在业务模型中使用 after_create 回调并将逻辑存在于预算控制器中(不完全确定如何执行此操作),或者我是否应该将逻辑添加到 business_controller.rb 中'new' 调用类似于:

@business = Business.new
@categories = Category.all
@categories.each do |category|
      category.budget.build(:value => "0", :business_id => @business.id)
end

【问题讨论】:

这里似乎有无关的信息使这个问题难以回答。除了部门预算之外,企业真的有预算吗?此外,如果一个部门拥有许多业务,反之亦然,则需要有一个似乎缺失的连接表。 谢谢。一个企业有很多预算。部门更像是预算类别,所以我想他们不需要与业务相关联。因此,Budget 是连接表。我已经更新了这个问题。如果您对实体名称有更好的建议,请提出建议。 我更新了问题以删除部门和企业之间的关联,并将部门重命名为类别。希望这会更有意义。 【参考方案1】:

根据我的经验,最好避免使用回调,除非它与给定模型的持久性相关。在这种情况下,让预算在未提供时设置它自己的默认值是很好地使用回调。这也消除了您的逻辑的一些复杂性。

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 的魔法,比如接受嵌套属性_for。例如,如果将来您决定并非所有企业都需要每个类别的预算,您可以轻松地将所需的预算作为参数传递给 BudgetGenerator。

您将在控制器中实例化 BusinessGenerator 类:

class BusinessController < ActionController::Base
  ...
  def create
    generator = BusinessGenerator.new(business_params)
    if generator.generate
      flash[:success] = "Yay"
      redirect_to generator.business
    else
      render :new
    end
  end
  ...      
end

使用这种方法可能遇到的一些症结包括:

将验证错误返回到您的业务表单 如果创建预算失败,您将陷入预算不足的困境。您不能等到创建预算后才能保存业务,因为没有要关联的 ID。或许可以考虑在生成器方法中放置一个事务。

【讨论】:

这将是提取服务模式的一个很好的例子。 (见#2 here)【参考方案2】:

不管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_idBudget belongs_to Category。你应该让这个category_id 定义foreign_key:

#app/models/budget.rb
class Budget < ActiveRecord::Base
   belongs_to :category, foreign_key: "department_id"
end

【讨论】:

【参考方案3】:

我最终将逻辑添加到业务控制器中的创建方法中,以循环所有类别并在保存后创建预算。请注意,我很懒惰,没有进行任何错误处理。 :

  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

【讨论】:

以上是关于Rails 4 在保存时创建关联对象的主要内容,如果未能解决你的问题,请参考以下文章

如何根据 Rails 中的其他对象验证多对多关联?

验证在创建和更新时表现不同的关联对象

通过推荐关联成员 - Rails

在 Rails 中创建对象及其所有关联模型的副本

Rails递归查看对象关联的奇怪行为

Rails - has_one 关系:关联和非关联对象的范围