将所有记录关系移至另一记录

Fut*_*cky 4 ruby mysql ruby-on-rails ruby-on-rails-4

我正在尝试将一个记录及其所有关系子项“合并”到另一记录中。

例如:

我有vendor1和vendor2其中都有许多包含其他 has_many 的关系。例如,供应商有许多purchase_orders,采购订单有许多ordered_items,ordered_item有许多received_items。

如果我将vendor2的名称更改为与 的名称相同,vendor1那么我想销毁它vendor2,但将其所有 has_many 移动到vendor1。

这就是我一直在努力做的事情:

  def vendor_merge(main_vendor, merge_vendor)
    relationships = [
      merge_vendor.returns, merge_vendor.receiving_and_bills,
      merge_vendor.bills, merge_vendor.purchase_orders, merge_vendor.taxes,
      Check.where(payee_id: merge_vendor.id, payee_type: "Vendor"),
      JournalEntryAccount.where(payee_id: merge_vendor.id)
    ]

    relationships.each do |relationship|
      class_name = relationship.class.name
      relationship.each do |r|
        if class_name === "Check"
          r.update(payee_id: main_vendor.id)
        else
          r.update(vendor_id: main_vendor.id)
        end
        r.save
      end

      relationship.delete_all
    end

    merge_vendor.destroy
  end
Run Code Online (Sandbox Code Playgroud)

这样做会给我带来约束错误,因为 has_many 的 has_many 以及 has_many through: :ect...

有什么直接的解决方案吗?

MrY*_*iji 5

您将需要在应用程序中定义合并逻辑。这可能是一个 PORO(普通的旧 ruby​​ 对象),例如VendorMerger,它保存了将一条记录合并到Vendor另一条记录中的所有逻辑(这也可能位于Vendor模型内部,但它会污染您的模型)。

这是 PORO 的一个例子:

# lib/vendor_merger.rb
class VendorMerger
  def initialize(vendor_from, vendor_to)
    @vendor_from = vendor_from
    @vendor_to = vendor_to
  end

  def perform!
    validate_before_merge!
    ActiveRecord::Base.connection.transaction do # will rollback if an error is raised in this block
      migrate_related_records!
      destroy_after_merge!
    end
  end

  private

  def validate_before_merge!
    raise ArgumentError, 'Trying to merge the same record' if @vendor_from == @vendor_to
    raise ArgumentError, 'A vendor is not persisted' if @vendor_from.new_record? || @vendor_to.new_record?
    # ...
  end

  def migrate_related_records!
    # see my thought (1) below
    @vendor_from.purchases.each do |purchase|
      purchase.vendor = @vendor_to
      # ...
      purchase.save!
    end
  end

  def destroy_after_merge!
    @vendor_from.reload.destroy!
  end
Run Code Online (Sandbox Code Playgroud)

用法:

VendorMerger.new(Vendor.first, Vendor.last).perform!
Run Code Online (Sandbox Code Playgroud)

此 PORO 允许您将与合并相关的所有逻辑包含到一个文件中。它尊重 SRP(单一责任原则),并使测试和维护变得非常容易(例如:包括记录器、自定义错误对象等)。

想法 (1):您可以手动检索要合并的数据(如我的示例中所示),但这意味着如果有一天您向模型添加另一个关系Vendor,比如说Vendor has_many :customers但忘记将其添加到 VendorMerger,那么由于 VendorMerger 不知道新关系,它将“默默地失败” :customers。为了解决这个问题,您可以动态地获取所有引用的模型Vendor(其中列是vendor_idORclass_name选项等于'Vendor'OR 关系是多态的并且XX_type列包含一个'Vendor'值)并将所有这些外部 ID 从旧 ID 转换为新 ID。