As a little follow up to the gotchas, I updated Squirrel. Its pagination is compatible with the latest will_paginate now. However, more important is the fact that you can use Squirrel blocks to build scopes:
User.scoped{ name.not =~ "Jon%" }.ordered.find(:all)
It works just like scoped does normally, but it lets you give Squirrel-style blocks. This lets you keep all your nice named_scope methods and it lets you do all the crazy crap that Squirrel makes look so much nicer at the same time.
For those of you who may not have seen Squirrel before, it’s a plugin for ActiveRecord that allows for a more Ruby-ish syntax when specifying queries. It’s especially handy for advanced searches and the like, where lots of joins and conditionals are required. It makes those a lot more easy to read, and makes absolutely sure that it uses the right names to refer to the right columns, which isn’t always as straightforward as you might think on a complex join.
Sadly, we can’t actually use named_scope with a Squirrel block; it’s already using blocks for other things. However, we can do the next best thing, which is creating class methods on our models. Since named_scopes proxy all their methods back to where they were created, everything will work out fine.
Let’s say we had an app that lets users put up postings for a length of time. We can also tag those postings. We want to allow people to search for the postings, so we whip up a Squirrel query like so:
class Post
named_scope :ordered, :order => "created_on DESC"
named_scope :limit, lambda{|x| {:limit => x} }
named_scope :after, lambda{|d| {:conditions => ["posts.created_on > ?", d]} }
def self.search(params)
scoped do
any do
title.contains? params[:keyword]
body.contains? params[:keyword]
end
expired == false unless params[:all] == "1"
created_on > params[:timeframe].to_i.days.ago
params[:tag].split(",").each do |tag|
tags.name == tag.strip
end
end
end
end
This lets us chain search alongside the limit and ordered scopes (using some params that look like they came in a Rails request):
Post.search({:all => "1",
:tag => "one, two",
:keyword => "Something",
:timeframe => ""}).ordered.limit(5)
This performs the following query:
SELECT DISTINCT "posts".id FROM "posts"
LEFT OUTER JOIN "tags" ON tags.post_id = posts.id
WHERE ((posts.created_on > '2008-06-25 15:13:36'
AND (posts.title LIKE '%Something%' OR posts.body LIKE '%Something%')
AND (tags.name = 'one') AND (tags.name = 'two')))
ORDER BY created_on DESC
LIMIT 5
Now let’s say those Posts have Comments (and both of those belong_to :user). We can find all Posts that have Comments by their author like so:
def Post.with_author_comments
scoped { user.id == comments.user.id }
end
So to find all the Posts in the last week that have a comment by their Author, we can say:
Post.with_author_comments.after(7.days.ago)
Which gets us this lovely bit of SQL:
SELECT
... 32 aliases snipped ...
FROM "posts"
LEFT OUTER JOIN "comments" ON comments.post_id = posts.id
LEFT OUTER JOIN "users" ON "users".id = "comments".user_id
LEFT OUTER JOIN "users" users_posts ON "users_posts".id = "posts".user_id
WHERE ((posts.created_at > '2008-06-18 15:35:27')
AND (((users_posts.id = users.id))))
And we can even find all the Users who have never made a Comment this way:
def User.without_comments
scoped { comments.id.nil? }
end
Each of these works exactly like any other named_scope and can be used in any situation where a named_scope could also be used.
Update: After I hit publish on this, I noticed that the Squirrel-style named scopes need to be specified first in the chain or they clobber what came before, but otherwise work fine. Using blocks with scoped not using the “named” methods also works completely fine. I’ll be looking into why this is and I’ll have an update soon. So, you can use them almost exactly the same as regular named scopes, anyway.
You can get the latest on Squirrel’s github page. Like our other plugins, we’re really hoping everyone will use the git repo, but in case you can’t or won’t, we also have the SVN repo available.
We’ve upgraded several Rails 2.0 application to Rails 2.1 now, and we’ve compiled a list of little things to keep in mind as you upgrade. Hopefully this list will help you avoid banging your head against a wall.
The updated_at and updated_on columns are NOT automatically updated on a #save on an AR object in Rails 2.1, unless another column has also changed. In each of the cases where we were relying on this behavior, we were using it to detect in a general way that something had changed with the model (without introducing a dependency on acts_as_modified). Because Rails 2.1 has dirty attribute checking, these methods were able to be refactored using this new functionality.
Older versions of will_paginate are broken on 2.1 (stack level too deep errors), to resolve, install the latest version of will_paginate like this:
sudo gem install mislav-will_paginate -s http://gems.github.com
Then put require ‘will_paginate’ in an initializer or in environment.rb (you need to have added vendor/gems to load path already to do that).
This newer will_paginate has changed the #page_count method to be #total_pages instead, so you’ll have to keep that in mind. Which means, some of the will_paginate view helpers changed as well – if you’ve monkey patched them (you haven’t unless you’re me!) look for changes there too!
Finally, Squirrel’s WillPagination module provides a #page_count method. To get a #total_pages from squirrel results, we temporarily monkey patched Squirrel by adding a lib/extensions/squirrel.rb which was just…
module Squirrel
module WillPagination
alias_method :total_pages, :page_count
end
end
We’ll need to update Squirrel soon to have this built in so that its compatible with the latest version of will_paginate.
Make sure you’ve upgraded Shoulda if you get errors about not being able to find fixture methods and/or assert, especially if these errors appear in setup blocks.
Unfortunately, haml isn’t Rails 2.1 compatible. To fix this, upgrade to haml-2.0.
If you have the following hack in your mailer, just remove it. Rails 2.1 does it for you:
def reply_to(str)
@headers["Reply-To"] = str
end
ActiveRecord::Base#attributes does not allow filtering anymore (it does not accept :only, for example). You must do the filtering manually, with something like this:
def json_attributes_for(model, *attrs)
attrs = [attrs].flatten.map(&:to_s)
model.attributes.delete_if{|k,v| !attrs.include?(k) }.to_json
end
Called like so: json_attributes_for(page, :id, :keyword)
Now in Rails 2.1, if you both foo.rhtml and foo.rxml exist and you aren’t explicitly specifying one or the other, Rails will render with foo.rxml. Renaming foo.rhtml to foo.html.erb fixes this, but in Rails 2.0, this was ok.
In order to deal with the 1+n query problem, Active Record has changed how it does eager loading. Now, it will optimize out :includes on finders when they are not being used. “When they are not being used” is the key here. Active Record is supposed to noticed when there are additional conditions on the find that rely on the included table, and not leave it out.
However, on an association like this:
has_many :active_sites, :through => :clients, :source => :sites, :include => :domains, :conditions => 'domains.id IS NOT NULL'
Active Record leave out the domains table, even though it shouldn’t. We fixed this by changing active_sites to just a normal method, as the bug doesn’t seem to happen in find, like this:
def active_sites
sites.find :all, :include => [:domains], :conditions => 'domains.id IS NOT NULL'
end
has_finder has been integrated into Rails 2.1 as named_scope, so you don’t need it anymore. However, you also shouldn’t keep it around. For example, has_finder-1.0.5 was giving us stack trace too deep exceptions when traversing a has_many :though association. Removing it in favor of named_scope fixed that issue.
Those are the things that we’ve found that we feel might be helpful to other people out there. For the most part, upgrading to Rails 2.1 is a a straightforward process, and we’re quickly upgrading most of our apps.
Have you upgraded to 2.1 and run into anything that other people might hit? If so, feel free to add your gotchas to the comments.