giant robots smashing into other giant robots

We are thoughtbot. We make web & mobile apps.

Tagged:

Comments (View)

Squirrel Updates for Rails 2.1, now with Named Scopes!

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.

A Refresher!

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.

Tagged:

Comments (View)

Gotchas When Upgrading to Rails 2.1

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.

Partial Updates

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.

Will Paginate

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.

Shoulda

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.

HAML

Unfortunately, haml isn’t Rails 2.1 compatible. To fix this, upgrade to haml-2.0.

Reply-to in mailers

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

Changes in Active Record Attribute Filtering

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)

Changes in Template Rendering

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.

Relationship Optimized Eager Loading

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_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.

In Conclusion

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.