Tintin81
Tintin81

Reputation: 10207

How to automatically derive foreign key column names with polymorphic associations?

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)?

Upvotes: 1

Views: 80

Answers (1)

max
max

Reputation: 102026

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.

Upvotes: 1

Related Questions