abeger
abeger

Reputation: 6866

Rails: How can I tell if a record has a child record in any one of its child tables?

I'm working in Rails 3 and have a table with multiple child tables, e.g.

class Foo < ActiveRecord::Base
  has_many :things
  has_many :items
  has_many :widgets
end

class Thing < ActiveRecord::Base
  belongs_to :foo
end

class Item < ActiveRecord::Base
  belongs_to :foo
end

class Widget < ActiveRecord::Base
  belongs_to :foo
end

Is there a simple way for me to check to if a given Foo has a child record in one or more of the tables? Basically, is there a better way to do this:

if !foo.things.empty? or !foo.items.empty? or !foo.widgets.empty?
  puts "This foo is in use!"
emd

Upvotes: 9

Views: 4740

Answers (6)

Masa Sakano
Masa Sakano

Reputation: 2267

The answer by @Marcelo De Polli is the most generalized one posted so far. This answer is an updated version of it for Rails 5.

The parent class for a model is ApplicationRecord in Rails 5 and later, which used to be ActiveRecord::Base up to Rails 4 (n.b., the original question is tagged as Rails 3).

For simplicity of the code, use:

class Foo < ApplicationRecord
  def children?
    self.class.reflect_on_all_associations.map{ |a| self.send(a.name).any? }.any?
  end
end

To pursue more run-time efficiency when a model may have many classes of children, use:

class Foo < ApplicationRecord
  def children?
    self.class.reflect_on_all_associations.each{ |a| return true if self.send(a.name).any? }
    false
  end
end

Upvotes: 0

varatis
varatis

Reputation: 14740

This is what any? is for.

class Foo < ActiveRecord::Base
  def children?
    things.any? || items.any? || widgets.any?
  end
end

Since this has become a topic of contention, I present to you:

> foo = Foo.last
Foo Load (0.6ms)  SELECT "foos"......
> foo.items
Item Load (0.9ms)  SELECT "items".*.......
> foo.items.any?
=> true #OH, WHAT's that? NO SQL CALLS? GEE WILLICKERS
> foo.items.exists?
Item Exists (0.5ms) #Hmmmmmm....
=> true

The point here is that under any circumstances, exists makes a DB call, where as any? will not, if spaces is always loaded into memory. Now as I said, many times, the importance is not the efficiency of the DB call (AND YES, the SQL call exists? makes is more efficient), but the fact that any? won't necessarily make a call to the DB, which is a HUGE advantage. Look for yourself:

[20] pry(main)> Benchmark.measure { foo.item.exists? }
  Item Exists (0.5ms)  SELECT 1 AS one FROM "items" ...
=> #<Benchmark::Tms:0x007fc1f28a8638
 @cstime=0.0,
 @cutime=0.0,
 @label="",
 @real=0.002927,
 @stime=0.0,
 @total=0.00999999999999801,
 @utime=0.00999999999999801>
[21] pry(main)> Benchmark.measure { foo.items.any? }
=> #<Benchmark::Tms:0x007fc1f29d1aa0
 @cstime=0.0,
 @cutime=0.0,
 @label="",
 @real=7.6e-05,
 @stime=0.0,
 @total=0.0,
 @utime=0.0>

For a more concise timing, look at this:

> Benchmark.measure { 1000.times {foo.items.exists?} }.total
=> 2.5299999999999994
> Benchmark.measure { 1000.times {foo.items.any?} }.total
=> 0.0

Now as I said, many times, it depends on circumstance -- you could have many circumstances where these items aren't loaded into memory, but many times, they are. Choose which one works best for you depending on how you're calling it.

Upvotes: 6

Marcelo De Polli
Marcelo De Polli

Reputation: 29291

This should work for any given model.

class Foo < ActiveRecord::Base
  def children?
    has_associated_records = self.class.reflect_on_all_associations.map { |a| self.send(a.name).any? }
    has_associated_records.include?(true)
  end
end

Upvotes: 5

Damien
Damien

Reputation: 27463

Suppose all the associations are loaded into memory:

class Foo < ActiveRecord::Base
  has_many :things
  has_many :items
  has_many :widgets

  def in_use?
    [things, items, widgets].flatten.any?
  end
end

Edit

I just realized that this is wrong: each association (even if still loaded into memory) will be loaded which isn't good.

things.any? || items.any? || widgets.any?

is more correct and has been proposed before me.

Upvotes: 0

Abram
Abram

Reputation: 41874

Well, I think you're on the right track, but maybe just put that as a method in your Foo model

class Foo < ActiveRecord::Base
  def children?
    things.any? || items.any? || widgets.any?
  end
end

Then just say, Foo.first.children? and get true if the Foo instance has any children.

Upvotes: 9

Mark Swardstrom
Mark Swardstrom

Reputation: 18070

You could subclass Thing Item and Widget. Or add a polymorphic join table to keep track of it. Not ideal, I know.

You could at least do this, so it would read a little better.

if foo.things.exists? || foo.items.exists? || foo.widgets.exists?
  puts "This foo is in use!"
end

'empty?' uses 'exists?' behind the scenes, I believe.

Upvotes: 0

Related Questions