10

Is there a way to find all Polymorphic models of a specific polymorphic type in Rails? So if I have Group, Event, and Project all with a declaration like:

has_many :assignments, :as => :assignable

Can I do something like:

Assignable.all

...or

BuiltInRailsPolymorphicHelper.all("assignable")

That would be nice.

Edit:

... such that Assignable.all returns [Event, Group, Product] (array of classes)

Lance
  • 75,200
  • 93
  • 289
  • 503

5 Answers5

16

There is no direct method for this. I wrote this monkey patch for ActiveRecord::Base. This will work for any class.

class ActiveRecord::Base

  def self.all_polymorphic_types(name)
    @poly_hash ||= {}.tap do |hash|
      Dir.glob(File.join(Rails.root, "app", "models", "**", "*.rb")).each do |file|
        klass = File.basename(file, ".rb").camelize.constantize rescue nil
        next unless klass.ancestors.include?(ActiveRecord::Base)

        klass.
          reflect_on_all_associations(:has_many).
          select{ |r| r.options[:as] }.
          each do |reflection|
            (hash[reflection.options[:as]] ||= []) << klass
          end
      end
    end
    @poly_hash[name.to_sym]
  end

end

Now you can do the following:

Assignable.all_polymorphic_types(:assignable).map(&:to_s)
# returns ['Project', 'Event', 'Group']
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
  • How do I implement this? do I go to class ActiveRecord::Base to modify? cant seem to find this class in my Project.. – james Apr 23 '16 at 01:32
  • 1
    @james refer to this answer: http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase/2329394#2329394 – Harish Shetty Apr 23 '16 at 02:36
  • This is beautiful! Ain't there a way to avoid having to open the `model` files? Doesn't Rails knows all the classes already? – Augustin Riedinger Feb 07 '18 at 11:19
  • 1
    @AugustinRiedinger In the prod mode you can use `ActiveRecord::Base.subclasses`, in dev mode the classes are loaded only after they are referred explicitly. So it is not too reliable. My approach reads the file names(not the content) and it is done once for the life time of the process. So it is tolerable.. – Harish Shetty Feb 08 '18 at 03:38
  • Yeah I googled it too. The fact that it is read only once makes it acceptable indeed. But it would be even nicer that it loads on startup instead of at first call because the first one is actually much slower... – Augustin Riedinger Feb 08 '18 at 07:57
  • @AugustinRiedinger, you can add an initializer file( `config/initializers/active_record.rb`) to your project and call `ActiveRecord::Base. all_polymorphic_types('foo')` in it. – Harish Shetty Feb 09 '18 at 05:16
  • Yeah, but I meant for all classe. Which means opening the `models` folder again, unless it is loaded at first. – Augustin Riedinger Feb 09 '18 at 07:53
  • 1
    @AugustinRiedinger calling `ActiveRecord::Base. all_polymorphic_types` loads all classes. – Harish Shetty Feb 10 '18 at 03:01
  • 1
    Awesome. This really should be part of AR itself. Tremendously helpful for example for building forms. – cseelus Dec 31 '18 at 12:03
1

You can also try this way.cause above solution doesn't work for me cause i had some mongo's model.

def get_has_many_associations_for_model(associations_name, polymorphic=nil)

  associations_name = associations_name.to_s.parameterize.underscore.pluralize.to_sym
  active_models = ActiveRecord::Base.descendants
  get_model = []
  active_models.each do |model|

    has_many_associations =model.reflect_on_all_associations(:has_many).select{|a|a.name==associations_name }
    has_many_associations = has_many_associations.select{ |a| a.options[:as] == polymorphic.to_s.to_sym} if polymorphic.present?
    get_model << model if has_many_associations.present?

  end
  get_model.map{|a| a.to_s}
end

Anb call it like

get_has_many_associations_for_model("assignments", "assignable")

Here Second parameters is optional for if you want polymorphic records than pass it otherwise leave it as blank.

It will Return Array of Model name as String.

Ketan Mangukiya
  • 360
  • 1
  • 7
1

Harish Shetty's solution will not work for namespaced model files which are not stored directly in Rails.root/app/models but in a subdirectory. Although it correctly globs files in subdirectories, it then fails to include the subdir when turning the file name into a constant. The reason for this is, that the namespacing subdir is removed by this line:

klass = File.basename(file, ".rb").camelize.constantize rescue nil

Here is what I did to retain the namespacing subdir:

file.sub!(File.join(Rails.root, "app", "models"), '')
file.sub!('.rb', '')
klass = file.classify.constantize rescue nil

Here's the full modified solution:

  def self.all_polymorphic_types(name)
    @poly_hash ||= {}.tap do |hash|
      Dir.glob(File.join(Rails.root, "app", "models", "**", "*.rb")).each do |file|
        file.sub!(File.join(Rails.root, "app", "models"), '')
        file.sub!('.rb', '')
        klass = file.classify.constantize rescue nil
        next unless klass.ancestors.include?(ActiveRecord::Base)

        klass.
          reflect_on_all_associations(:has_many).
          select{ |r| r.options[:as] }.
          each do |reflection|
            (hash[reflection.options[:as]] ||= []) << klass
          end
      end
    end
    @poly_hash[name.to_sym]
  end

Now, the method will turn /models/test/tensile.rb correctly into Test::Tensile before reflecting on its associations.

Just a minor improvement, all credit still goes to Harish!

Ardent Coder
  • 3,777
  • 9
  • 27
  • 53
Andreas Gebhard
  • 361
  • 2
  • 8
0

I created a polymorphic model class with a method 'all' to test this.

class Profile
  # Return all profile instances
  # For class return use 'ret << i' instead of 'ret << i.all'
  def self.all
    ret = []
    subclasses_of(ActiveRecord::Base).each do |i|
      unless i.reflect_on_all_associations.select{|j| j.options[:as] == :profile}.empty?
        ret << i
      end
    end
    ret.flatten
  end

  def self.all_associated
    User.all.map{|u| u.profile }.flatten
  end
end

Here is my app setup:

User < ActiveRecord::Base
  belongs_to :profile, :polymorphic => true
end

Student < ActiveRecord::Base
  has_one :user, :as => :profile
end
nanda
  • 1,313
  • 9
  • 16
-1

You should be able to just use the associated collection:

model.assignments.all
Toby Hede
  • 36,755
  • 28
  • 133
  • 162