开发者

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 }
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜