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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With