15

I have a whole bunch of child classes that inherit from a parent class via single-table-inheritance in my Rails app. I'd like a way to get an array of all the child classes that inherit from the main class.

I tried the following single-link command that I found in another SO answer, but it only returns the parent class.

ObjectSpace.each_object(class<<MyParentClass;self;end)

Is there any clean way to do this?

EDIT: Apparently Rails only lazy-loads child classes when called in Dev mode, and possibly production depending on the Rails version. However, the first answer should work on Rails 3.1 and higher in Prod mode.

thoughtpunch
  • 1,857
  • 4
  • 24
  • 41

7 Answers7

30

Rails extends Ruby Class with the subclasses() method.

In Rails 3 you can call it directly:

YourClass.subclasses

In Rails 2.3, ".subclasses" is protected, so we use have to call it using send():

YourClass.send(:subclasses)
lulalala
  • 15,996
  • 12
  • 101
  • 165
Pavling
  • 3,853
  • 1
  • 20
  • 25
  • 1
    it is protected? i can do `Parent.subclases #=> [class,class]` without any problem – Orlando Jul 19 '12 at 15:43
  • 1
    It was protected in Rails 2.3 (which is what I was probably using when I tried it while writing this post), but is not protected in Rails 3.2 (and I've just tested it with both versions again). So in Rails 3.2, you can happily call Parent.subclasses. – Pavling Apr 10 '13 at 10:59
  • 3
    Also, I believe you'll have to preload your classes in development because lazy loading is turned on. – ifightcrime Jun 17 '13 at 21:29
  • Surprisingly, if you have ` config.autoload_paths << Rails.root.join('app', 'models', 'superclass')` in config/application.rb, then .subclasses returns an empty array. Remove that config, and you get the subclasses. No idea why. – David Hempy Jul 28 '17 at 20:37
  • is there a way to get subclasses that haven't been instantiated yet? – stackjlei Sep 27 '19 at 22:17
15

You need to eager load the classes, as stated in: https://github.com/rails/rails/issues/3364

ActionDispatch::Reloader.to_prepare do
  Rails.application.eager_load!
end

Then you will be able to use:

YourClass.subclasses

or

YourClass.descendants
sequielo
  • 1,404
  • 18
  • 27
5
ParentClass.subclasses.map(&:name)
Adam
  • 51
  • 1
  • 1
5

In your config/environments/development.rb

Rails.application.configure do
  config.eager_load = false
end

U can change false to true, and then in your console to do

Class.subclasses

or

Class. descendants

here is the difference between subclasses and descendants

subclasses:

class Foo; end
class Bar < Foo; end
class Baz < Bar; end

Foo.subclasses # => [Bar]

descendants:

class C; end
C.descendants # => []

class B < C; end
C.descendants # => [B]

class A < B; end
C.descendants # => [B, A]

class D < C; end
C.descendants # => [B, A, D]
TorvaldsDB
  • 415
  • 6
  • 3
1

This will do it in one SQL query:

# SELECT DISTINCT type FROM objects
Object.uniq.pluck(:type)
0

Note, their is a more efficient way to implement Dave G's method above..

Object.select(:type).map(&:type).uniq

This first sends marshaled objects that only have the "type" attribute from the DB, which takes WAY less memory, then plucks only the types into an array that you can then uniq on. I'm sure there is an infinitely more efficient pure SQL way to do this though.

thoughtpunch
  • 1,857
  • 4
  • 24
  • 41
  • That much more efficient SQL way is available through Rails with `Object.select(:type).distinct.map(&:type)`. You get just the `distinct` types from the DB up front. Quite a bit more efficient as your table grows. – Anson Dec 13 '19 at 15:22
  • This would only return the types that exist in the database vs. all the types that actually exist. – aaronbartell Jun 05 '20 at 13:13
-8

Assuming there is at least one of each of object extant in the table:

Object.all.uniq{|x| x.type}.collect(&:type)
tokland
  • 60,415
  • 12
  • 129
  • 162
Dave G
  • 4,306
  • 4
  • 22
  • 27
  • This works as long as one of each child class has an associated record in the DB. In this circumstance, I want to get a list of all child classes wether or not they have a record yet. – thoughtpunch May 10 '12 at 14:07
  • 1
    Ah. Instead try the aptly named "subclasses" call: Object.subclasses – Dave G May 10 '12 at 14:11
  • Interestingly, this only works if the subclasses have been loaded into memory, either through a instantiation or a db query. I'm guessing that Rails doesn't know about subclasses at run time unless explicitly called. – thoughtpunch May 10 '12 at 14:21
  • 5
    2 years late, but it's worth noting here that the issue with subclasses not being loaded is only the case when config.cache_classes is set to false as it is only in development mode. – Will Tomlins Oct 29 '12 at 15:57
  • 7
    This is a terrible, terrible idea for production! Instantiate objects for every entry in the objects table (which could have thousands or even millions of rows), then iterate through them to discover their types? *Cringe* – Ajedi32 Sep 09 '14 at 14:48
  • 2
    `Object.pluck(:type).uniq` would be less expensive in resource costs, but still the best answer is `Object.subclasses` – MrYoshiji Aug 05 '15 at 15:30
  • @Ajedi32 Yeah, it's not good but not as bad as that. `uniq` is not the array method it appears to be but an active record alias of `distinct` which alters the `all` and causes the SQL to `SELECT DISTINCT`. If there's an index on `type` that will be very efficient query but it's still an unnecessary and hacky database query. – Adamantish Jun 04 '16 at 21:32
  • @Adamantish You sure about that? I mean, it's certainly possible, but I see nothing in the docs about that. I did find [`ActiveRecord::Associations::CollectionProxy#uniq`](http://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-distinct), but according to that, uniq shouldn't take a block at all. Also, if your assertion is true then that does seem like a rather leaky abstraction, since the block then wouldn't be able to contain more complicated operations like, for example `{|x| /([a-z]+).*/.match(x.type)[1] }` – Ajedi32 Jun 04 '16 at 21:48
  • 1
    @Ajedi32 a very good point. My first guess was that the block is simply ignored so I tried it out in pry and indeed that's the case. This accepted answer would work exactly the same if it were just `Object.all.uniq.collect(&:type)` – Adamantish Jun 05 '16 at 10:13