views:

30

answers:

2

I think it's safe to say everyone loves doing something like this in Rails:

Product.find(:all, :conditions => {:featured => true})

This will return all products where the attribute "featured" (which is a database column) is true. But let's say I have a method on Product like this:

def display_ready?
     (self.photos.length > 0) && (File.exist?(self.file.path))
end

...and I want to find all products where that method returns true. I can think of several messy ways of doing it, but I think it's also safe to say we love Rails because most things are not messy.

I'd say it's a pretty common problem for me... I'd have to imagine that a good answer will help many people. Any non-messy ideas?

+1  A: 

You need to do a two level select:

1) Select all possible rows from the database. This happens in the db.

2) Within Ruby, select the valid rows from all of the rows. Eg

possible_products = Product.find(:all, :conditions => {:featured => true})
products = possible_products.select{|p| p.display_ready?}

Added:

Or:

products = Product.find(:all, :conditions => {:featured => true}).select {|p|
               p.display_ready?}

The second select is the select method of the Array object. Select is a very handy method, along with detect. (Detect comes from Enumerable and is mixed in with Array.)

Larry K
I know you were first to suggest the select method which is what I needed, but tadman just plain gave great advice.
tybro0103
No worries. Thanks for the comment. Regards.
Larry K
+4  A: 

The only reliable way to filter these is the somewhat ugly method of retrieving all records and running them through a select:

display_ready_products = Product.all.select(&:display_ready?)

This is inefficient to the extreme especially if you have a large number of products which are probably not going to qualify.

The better way to do this is to have a counter cache for your photos, plus a flag set when your file is uploaded:

class Product < ActiveRecord::Base
  has_many :photos
end

class Photo < ActiveRecord::Base
  belongs_to :product, :counter_cache => true
end

You'll need to add a column to the Product table:

add_column :products, :photos_count, :default => 0

This will give you a column with the number of photos. There's a way to pre-populate these counters with the correct numbers at the start instead of zero, but there's no need to get into that here.

Add a column to record your file flag:

add_column :products, :file_exists, :boolean, :null => false, :default => false

Now trigger this when saving:

class Product < ActiveRecord::Base
  before_save :assign_file_exists_flag

protected
  def assign_file_exists_flag
    self.file_exists = File.exist?(self.file.path)
  end
end

Since these two attributes are rendered into database columns, you can now query on them directly:

Product.find(:all, :conditions => 'file_exists=1 AND photos_count>0')

You can clean that up by writing two named scopes that will encapsulate that behavior.

tadman
My display_ready method was hypothetical, but that's great advice! I never knew about the counter_cache. Plus I completely forgot that select method as well. Thanks!
tybro0103
Counter caching is really handy for situations like this where you want to deal with relationships without getting into the details. Also rendering some of your flags into database columns makes doing selections very easy and it's not too hard to write a rake task to fix up the flags if they fall out of sync.
tadman