Search code examples
ruby-on-railsruby-on-rails-4activerecordassociationsrails-activerecord

Rails, use different join table for `has_many :through` association based on condition


I'm working on a application in which there is table named companies and a company can have multiple users and users can have multiple books and magazines belonging to company. Currently the associations is as follows:

Company.rb

has_many :users
has_many :books
has_many :magazines

User.rb

belongs_to :company
has_many :user_books, dependent: :destroy
has_many :books, through: :user_books
has_many :user_magazines, dependent: :destroy
has_many :magazines, through: :user_magazines

Book.rb

belongs_to :company
has_many :user_books, dependent: :destroy
has_many :users, through: :user_books

Same association exists for magazines as well. Not using has_and_belongs_to_many association as we are capturing additional data in association table. Now we need to introduce a new entity called audio_books similar to books and magazines but instead of creating one more association table called user_audio_books we are planning to create a polymorphic association table called user_items which would record the association between user and the company items(books, magazines & audio_books).

New association: User.rb

has_many :user_items, dependent: destroy
has_many :books, through: :user_items, as: :source, source: source, source_type: 'Book'
has_many :magazines, through: :user_items, as: :source, source: source, source_type: 'Magazine'

However this new association will be enabled only for few companies for time being, until then we need to support all the 16 methods added by has_many association on both the models on conditional basis.

I'm looking for something like in user.rb:

has_many :user_books, dependent: :destroy
has_many :user_items, dependent: :destroy
<if company.audio_books_enabled? >
has_many :books, through: :user_items, as: :source, source: source, source_type: 'Book'
<else>
has_many :books, through: :user_books
<end>

audio_books_enabled? is not an sql query but a redis based check.

Is it possible to achieve this?


Solution

  • You can't have two associations with the same name. But a method that would return different associations could work:

    class User < ApplicationRecord
      belongs_to :company
      has_many :user_books, dependent: :destroy
      has_many :user_items, dependent: :destroy
    
      # NOTE: rename the old `books` association
      has_many :legacy_books, through: :user_books, source: :book, class_name: "Book"
    
      # NOTE: add another `books` association
      has_many :new_books, through: :user_items, as: :source, source: :source, source_type: "Book"
    
      def books
        if company.audio_books_enabled?
          new_books
        else
          legacy_books
        end
      end
    
      # maybe something like this for associating books with user
      def books= params
        if company&.audio_books_enabled?
          self.new_books = params
        else
          self.legacy_books = params
        end
      end
    end
    

    You could have a books association and books method if you have to:

    class User < ApplicationRecord
      belongs_to :company
      has_many :user_books, dependent: :destroy
      has_many :user_items, dependent: :destroy
    
      # NOTE: rename the old `books` association
      has_many :legacy_books, through: :user_books, source: :book, class_name: "Book"
    
      # NOTE: add another `books` association
      has_many :books, through: :user_items, as: :source, source: :source, source_type: "Book"
    
      def books
        if company.audio_books_enabled?
          super
        else
          legacy_books
        end
      end
    end