Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move all record relationships to another record

I am trying to "merge" one record into another record with all it's relationship children.

For example:

I have vendor1 and vendor2 which both have many relations that contain other has_many. For example a vendor has many purchase_orders and a purchase order has many ordered_items and an ordered_item has many received_items.

If I change the vendor2's name to be the same as vendor1's name then I want to destroy vendor2 but move all of it's has_many to vendor1.

This is what I've been trying to do:

  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

Doing it this way gives me constraint errors because of the has_many of the has_manys and because of the has_many through: :ect...

Any straight forward solution to this?

like image 943
FutoRicky Avatar asked Sep 21 '25 10:09

FutoRicky


1 Answers

You will need a merge logic defined in your app. This could be a PORO (plain old ruby object), like VendorMerger, which holds all the logic in order to merge a Vendor record into another (this could also be inside the Vendor model but it would pollute your model).

Here is an example of that 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

Usage:

VendorMerger.new(Vendor.first, Vendor.last).perform!

This PORO allows you to contain all the logic related to the merge into one file. It respects the SRP (Single responsibility principle) and makes the testing very easy, as well as maintenance (ex: include a Logger, custom Error objects, etc).

Thought (1): You can either go by manually retrieving the data to be merged (as in my example), but this means if some day you add another relation to the Vendor model, let's say Vendor has_many :customers but forgot to add it to the VendorMerger, then it will "fail silently" since VendorMerger is not aware of the new relation :customers. To solve this, you can dynamically grab all models having a reference to Vendor (where column is vendor_id OR the class_name option is equal to 'Vendor' OR the relation is polymorphic and the XX_type column holds a 'Vendor' value) and convert all those foreign from the old to the new ID.

like image 200
MrYoshiji Avatar answered Sep 23 '25 02:09

MrYoshiji