Get a list/array of child classes from Single Table Inheritance in Rails?
Asked Answered
H

7

16

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.

Hohenzollern answered 10/5, 2012 at 13:54 Comment(0)
I
-8

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

Object.all.uniq{|x| x.type}.collect(&:type)
Indecisive answered 10/5, 2012 at 14:4 Comment(9)
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.Hohenzollern
Ah. Instead try the aptly named "subclasses" call: Object.subclassesIndecisive
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.Hohenzollern
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.Quartus
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? CringeOutwardly
Object.pluck(:type).uniq would be less expensive in resource costs, but still the best answer is Object.subclassesBaccalaureate
@Outwardly 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.Mitchum
@Mitchum 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, 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] }Outwardly
@Outwardly 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)Mitchum
B
31

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)
Bovill answered 10/5, 2012 at 14:44 Comment(5)
it is protected? i can do Parent.subclases #=> [class,class] without any problemAstrogate
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.Bovill
Also, I believe you'll have to preload your classes in development because lazy loading is turned on.Prebendary
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.Whitecap
is there a way to get subclasses that haven't been instantiated yet?Outmost
S
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
Shipe answered 4/10, 2013 at 5:12 Comment(0)
S
7

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]
Samira answered 6/7, 2018 at 9:1 Comment(0)
M
6
ParentClass.subclasses.map(&:name)
Majesty answered 26/7, 2016 at 7:15 Comment(1)
This answer would benefit from a brief explanation of how it addresses the OP's issue.Glomerulus
H
1

This will do it in one SQL query:

# SELECT DISTINCT type FROM objects
Object.uniq.pluck(:type)
Harmonium answered 26/9, 2013 at 10:7 Comment(1)
This won't work unless there is at least one instance of each child class stored in the database.Interference
H
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.

Hohenzollern answered 21/7, 2012 at 16:2 Comment(2)
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.Bean
This would only return the types that exist in the database vs. all the types that actually exist.Transcendence
I
-8

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

Object.all.uniq{|x| x.type}.collect(&:type)
Indecisive answered 10/5, 2012 at 14:4 Comment(9)
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.Hohenzollern
Ah. Instead try the aptly named "subclasses" call: Object.subclassesIndecisive
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.Hohenzollern
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.Quartus
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? CringeOutwardly
Object.pluck(:type).uniq would be less expensive in resource costs, but still the best answer is Object.subclassesBaccalaureate
@Outwardly 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.Mitchum
@Mitchum 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, 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] }Outwardly
@Outwardly 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)Mitchum

© 2022 - 2024 — McMap. All rights reserved.