Как автоматически получить имена столбцов внешнего ключа с полиморфными ассоциациями?

В моем приложении Ruby on Rails 7 у меня есть большая Account модель с различными has_many ассоциациями (большинство из них здесь опущено для краткости):

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 может создать гостевую учетную запись, а затем переключить ее на зарегистрированную учетную запись.

Я создал объект Service для достижения этой задачи:

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

Это хорошо работает для всех ассоциаций, имеющих столбец account_id, однако не работает для полиморфных ассоциаций, таких как address и bank_accounts.

Как я могу переключать их без необходимости жестко запрограммировать имена столбцов внешних ключей (например, addressable_id и bankable_id в данном случае)?

🤔 А знаете ли вы, что...
Ruby on Rails активно соблюдает принцип REST (Representational State Transfer) для построения веб-сервисов.


1
69
1

Ответ:

Решено

Вместо жесткого кодирования внешних ключей (для любого типа ассоциации) вы можете использовать способность ActiveRecords отражать ассоциации:

# 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

Однако я должен настоятельно отговорить вас от этого.

Перебирать ассоциации такого класса и автоматически обновлять кучу строк кажется излишне безрассудным и опасным. Использование белого списка было бы намного лучше.

Еще одна большая проблема — полное отсутствие обработки ошибок: если какое-либо из обновлений завершится неудачей, ваши данные останутся в очень запутанном состоянии.