Complex selection sets, the Rails way?
Let's say you have an "Author" object, that has several books, and you are wanting to build some methods in the model. Your basic setup looks something like this:
class Author
def book_count(fiction = nil, genre = nil, published = nil)
end
end
For each argument, you have a couple of ways you want to operate:
fiction = true #retrieve all fiction books
fiction = false #retrieve all nonfiction
fiction = nil #retrieve books, not accounting for type
genre = nil #retrieve books, not accounting for genre
genre = some_num #retrieve books with a specific genre id
published = true #retrieve all published
published = false #retrieve all unpublished
published = nil #retrieve books, not accounting for published
Now, I wrote a basic select statement for some of this, along the lines of:
if published == true
return self.books.select{ |b| b.published == true }.size
elsif 开发者_Go百科published == false
return self.books.select{ |b| b.published == false}.size
else
return self.books.size
end
When I just had one or two arguments, this was unwieldy, but easy enough. However, as the client request more conditions be added to the method, it get more and more tedious to write.
What would the best "rails" way be to handle this?
Thanks!
scopes, (or "named_scopes" if you are using Rails < 3) are probably the best way of doing is.
The following is for rails 3, but it can be done with minor syntax tweaks You can create a bunch of scopes in your model. I.e.
scope :with_genre, lambda {|genre| where(:genre => genre) unless genre.nil?}
scope :published, lambda{|published| where(:published => published) unless published.nil?}
scope :fiction,, lambda{|fiction| where(:fiction => fiction) unless fiction.nil?}
etc
Then whenever you need to access them you can do things like
def book_count(..)
self.books.with_genre(genre).published(published).fiction(fiction).size
end
Also, you can just make the book_count parameter a hash, then you can have any amount of options you'd like without making the function have lots of parameters.
First, you might want book_count
to take a hash options={}
and define key defaults in the method itself. This way, as the client requires more options (or decides to remove some) you don't have to chase down all the calls in your project and alter them accordingly. I prefer doing things this way, but you can also use *arguments
as well.
One benefit of passing as an options hash is that you simply do not pass keys if the values are nil
, then you can simply find a count of books which match your search criteria, as follows:
return self.books.find(:all, :conditions => options).count
This should work fine, and allow for additional specifications to be added later. Just ensure that keys in the options
hash match your model attributes.
If you have eager loaded books
then you can try this:
def book_count(options = {})
books.select{|b| options.all?{|k, v| v.nil? || b.send(key) == v} }.size
end
Now you can make the calls such as
author.books.book_count(:genre => "foo", :fiction => true)
Exclude the attributes from the parameter hash when you want to remove the attribute from the filtering criteria. In the above example, :published
is excluded from the filter criteria as it is missing in the parameter hash. I have added an additional nil
check to cater to the scenarios where the attribute value is genuinely nil.
If the books
list is not eager loaded, then use the named_scope approach suggested by Olives.
The more Rails-y way would be to use ActiveRecord's built in find methods to get these out of the database rather than filtering it in Ruby. It will be faster, and the code will be cleaner. The where
method can take a hash of attributes and values. (see the ActiveRecord guide to querying for more info, it's a good intro)
Are you using Rails 3? In which case ActiveRecord has got even easier to use.
Something like this might work (although I don't have access to rails right now so this probably contains bugs):
class Author
def book_count( filter )
Book.find_by_author( self ).where( filter ).count
end
end
That should find all the books by that author (assuming you have a model association between the Author and Book) where all the conditions you specify are true. You might need to filter out any nils first. filter
would be a hash of conditions such as { :genre => 'Horror', :published => true }
.
Note that I use count
rather than size
. count
uses the SQL count function rather than returning the data and then counting it in ruby.
Hope that helps.
if published.nil?
return books.size
else
return books.count{ |b| b.published == published }
end
or
if published.nil?
return books.size
else
return books.map(&:published).count published
end
or
return books.count{ |b| published.nil? || b.published == published }
or
return published.nil? ? books.size : books.map(&:published).count(published)
or
return published.nil? ? books.size : books.count{ |b| b.published == published }
精彩评论