In my Ruby on Rails 7 application I have a large Account
model with various has_many
associations (most of them are left out for brevity here):
class Account < ApplicationRecord
has_one :address, :dependent => :destroy, :as => :addressable # Polymorphic!
has_many :articles, :dependent => :destroy
has_many :bank_accounts, :dependent => :destroy, :as => :bankable # Polymorphic!
has_many :clients, :dependent => :destroy
has_many :invoices, :dependent => :destroy
has_many :languages, :dependent => :destroy
has_many :payments, :dependent => :destroy
has_many :projects, :dependent => :destroy
has_many :reminders, :dependent => :destroy
has_many :tasks, :dependent => :destroy
has_many :users, :dependent => :destroy
end
Users
may create a guest account and then later switch it over to a registered account.
I have created a Service object to achieve this task:
class Accounts::Move < ApplicationService
def initialize(account)
@account = account
@guest_account = @account.guest_account
end
def call
if @guest_account
update_dependencies
@guest_account.delete
reset_guest_account_id
end
end
private
def update_dependencies
dependencies.each do |dependency|
dependency.constantize.where(:account_id => @guest_account.id).update_all(:account_id => @account.id) # Not working with polymorphic associations!
end
end
def dependencies
Account.reflect_on_all_associations.map(&:class_name)
end
def reset_guest_account_id
@account.update_column(:guest_account_id, nil)
end
end
This works well for all associations that have an account_id
column, however it doesn't work for polymorphic associations such as address
and bank_accounts
.
How can I switch over those too without having to hardcode the column names of the foreign keys (addressable_id
and bankable_id
in this case, for example)?
Instead of hardcoding the foreign keys (on any type of assocation) you could use ActiveRecords ability to reflect on assocations:
# Do not use the scope resolution operator for defining namespaces - it leads to autoloading bugs
# and surprising constant lookups
module Accounts
class Move < ApplicationService
def initialize(account)
@account = account
@guest_account = @account.guest_account
end
def call
# Why is this even needed?
# You should move this filtering up to where you enqueue the service
if @guest_account
update_dependencies
reset_guest_account_id # Null the reference first
@guest_account.delete
end
end
private
def update_dependencies
# Instead of iterating accross an array of strings it's
# ActiveRecord::Reflection::AssociationReflection instances
dependencies.each do |reflection|
scope = {
reflection.foreign_key => @guest_account.id,
reflection.type => reflection.type ? 'Account' : nil
}.compact
reflection.klass.where(scope)
.update_all(reflection.foreign_key => @account.id)
end
end
def dependencies
Account.reflect_on_all_associations(:has_many)
end
def reset_guest_account_id
@account.update_column(:guest_account_id, nil)
end
end
end
However I have to strongly discourage you from actually doing this.
Looping over the assocations of a class like this and automatically updating a bunch of rows feels needlessly reckless and dangerous. Using a whitelist would be far better.
Another big issue here is the complete lack of error handling - if any of the updates here fails it's going to leave your data in a very messy state.