Life in the fast lane
Posted by Matt Jankowski
Oct 13
You ever write a named_scope and think “hey framework, I already wrote a schema – why are you bothering me with all this work?!”

What about those times when you’re sitting behind the wheel of your ride and the warning flag is up because Johnny gave Jimmy a little too hard of a bump around turn number three and you are left wondering just how fast you should go? I mean sure you’re worried about Jimmy being ok and all, that sure was quite the spinout back there, but geez, this is the Quest For The Cup and your points total is not going to grow itself! Well the latter situation has had a pacecar for a while – and now the first one does too.
Pacecar
Pacecar is a plugin for rails application which gives your ActiveRecord models a bunch of class methods based (mostly) on database introspection. It looks at column names and types to generate named_scope methods at load time which can be used in your app and combined with other named_scopes as well.
| Pacecar (Automobile Racing) | Pacecar (ActiveRecord models) | |
| Communicates driving speed limit to automobile racers | Yes | No |
| Provides named_scopes to ActiveRecord classes | No | Yes |
| Does the boring work for you so you can have fun | Yes | Yes |
Scenario
Assume we have the following schema in a rails application…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class CreateSchema < ActiveRecord::Migration def self.up create_table :users, :force => true do |t| t.boolean :admin, :default => false, :null => false t.datetime :approved_at t.datetime :rejected_at t.string :first_name t.string :last_name t.text :description t.timestamps end create_table :posts, :force => true do |t| t.string :owner_type t.integer :owner_id t.string :publication_state t.string :post_type t.timestamps end create_table :comments, :force => true do |t| t.integer :user_id t.text :description t.timestamps end end end |
This is a fairly basic scenario – most applications are not this simple. But, this is enough to get the point across, and the benefits really only grow as you add more tables and more models. Here’s how we might model this data (the unfamiliar methods are coming from pacecar and will be explained shortly).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class User < ActiveRecord::Base has_many :posts, :as => :owner has_many :comments scopes_ranking :comments end class Post < ActiveRecord::Base PUBLICATION_STATES = %w(Draft Submitted Rejected Accepted) TYPES = %w(Free Open Private Anonymous PostModern) belongs_to :owner, :polymorphic => true scopes_state :publication_state scopes_state :post_type, :with => TYPES scopes_polymorph :owner end class Comment < ActiveRecord::Base belongs_to :user end |
Usage
Ok, so you have a schema and you have some models. Now, what named_scope do you need? Here’s what you get “for free”, when the pacecar plugin is installed…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
#Records where approved_at is not null, or where it is null… User.approved_at_present User.approved_at_missing #Records where first_name is not null, or where it is null… User.first_name_present User.first_name_missing #Records ordered by first_name (default to ‘asc’, can specify to override)… User.by_first_name User.by_first_name(:asc) User.by_first_name(:desc) #Records where an attribute matches a search term (column LIKE "%term%")… User.first_name_matches('John') #Records where an attribute starts or ends with a search term… User.first_name_starts_with('A') User.first_name_ends_with('a') #Records where any non-state text or string column matches term… User.search_for('test') #Records where any of a list of columns match the term… User.search_for 'test', :on => [:first_name, :last_name] #Records where all of a list of columns match the term… User.search_for 'test', :on => [:first_name, :last_name], :require => :all #Records that are all admins or non-admins… User.admin User.not_admin #Records approved before or after certain times… User.approved_at_before(5.days.ago) User.approved_at_after(4.weeks.ago) #Records with approved_at in the past or future… User.approved_at_in_past User.approved_at_in_future #Records with approved_at inside or outside of two times… User.approved_at_inside(10.days.ago, 1.day.ago) User.approved_at_outside(2.days.ago, 1.day.ago) #Records with certain year, month or day… User.approved_at_in_year(2000) User.approved_at_in_month(01) User.approved_at_in_day(01) #Records with a duration (time delta between two columns) of, #over or under a certain number of days… User.with_duration_of(14, :approved_at, :rejected_at) User.with_duration_over(14, :approved_at, :rejected_at) User.with_duration_under(14, :approved_at, :rejected_at) #First x records… User.limited(10) |
And by using those #scopes_* class methods in the models, we also get…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#Records which have an owner_type of User… Post.for_owner_type(User) #Records with the most and least associated records… User.maximum_comments User.minimum_comments #Records which are in a particular state, or not in a state… Post.publication_state_draft Post.post_type_not_open |
Installation
To get the latest pacecar straight from git as a rails plugin, you can do..
ruby script/plugin install git://github.com/thoughtbot/pacecar.git
Alternately you can use the gem version from github…
gem sources -a http://gems.github.com
sudo gem install thoughtbot-pacecar
..and then add a line like this to your config/environment.rb…
1 2 3 4 |
config.gem 'thoughtbot-pacecar', :lib => 'pacecar', :version => '>= 1.0.0', :source => 'http://gems.github.com' |
After installing pacecar with either of these methods you will be up and running with all the “for free” methods, and you’ll then be able to add in the class method calls for enabling the Polymorph, State and Ranking use cases.
Sample usage
“Hey dev team, can you tell us how many users are active but dont have bios?”
User.bio_missing.active.count |
“How many events have we had that last longer than a week?”
Event.with_duration_over(7, :starts_at, :ends_at).count |
“Can I see the titles and authors of all the blog posts that we are scheduled to post in the future?”
Post.published_at_in_future.collect { |p| "#{p.title} #{p.user.name}" } |
Word of caution
Pacecar does a decent amount of method generation at load time – so if you’re opposed to “having lots of methods” you should be aware of that. You should also do an audit of the class methods you already have in place before installing pacecar, and make sure you’re not overwriting any of your own methods (named scopes or otherwise). Also, if you’re philosophically opposed to method generation … this does that.
Comments on this post
Oct 14
Seban said,
Interesting. But maybe it’s too much? I am sceptic for this idea, but maybe it’s only firt impression.
Oct 14
David Backeus said,
Sweet!
Oct 14
Ed Spencer said,
This looks awesome – I’ll definitely give it a try.
Oct 14
Lucas Efe said,
I think its a great plugin. It looks pretty slick, and covers pretty much everything. I love the approach of code generation.
Thanks
Oct 14
Mr. Bojangles said,
How much does the method generation affect load times? Have you guys run any benchmarks compared to explicitly named scopes?
Oct 15
Tim said,
Very nice. I would prefer to define those named scopes in the model though. I understand the coolness of already having those available, and I can see how it fits with existing active record patterns, but I would prefer to actually only have a very minimal set of core “built-in” features, and everything on top of that is explicitly shown in code. This reduces the number of things a developer has to learn in a framework and simplifies development. Having said that, your named scopes are intuitively named, and everyone has their own way of doing things. Kudo’s to you for contributing :)
Oct 15
Matt Jankowski said,
Bojangles – I have no idea about load time. I suspect it’s minimal, but haven’t benchmarked.
Tim – I thought about that a bit, and I think on an application that only has the need for 2-3 named scopes across all models, it would be silly to introduce this, because there’s certainly a cost in terms of knowing where methods are coming from (ie, if you pass a project to a new developer). On a larger (or more complicated) project with 15+ named_scopes in each model, it starts to make sense (and that’s what I extracted it from, basically).
You can probably eliminate some of that risk by making sure you add tests for methods you are using – and certainly add tests for any class methods you add which combine methods from this plugin.
Oct 15
Jeff said,
Those *_missing and *_present methods should end with a question mark, i.e. approved_at_missing?
Otherwise looks great! Thanks.
Oct 15
Matt Jankowski said,
Jeff, I wouldnt name them to end with question marks, because they dont return true/false, they return a collection of records.
For instance methods returning true/false, I do use the query method naming.
Oct 16
iain said,
I seem to have a problem when doing migrates and db:create with this plugin. When I have this plugin installed it already tries to load the models, even before I have the tables or even before I made the database. This is causing db:create and db:migrate to fail. Temporarily moving the plugin out of vendor/plugins is a quick solution.
Oct 16
Matt Jankowski said,
Iain – I’m not able to replicate that, but I believe you. Can you email me more info about your setup, what database you’re using, etc? (mjankowski at thoughtbot dot com)
Oct 20
Dennis said,
I’m having a similar problem to Iain with migrations. I’m able to perform db:create and db:migrate only if I don’t perform any migrations that load data into the models during the migration. DB creation works correctly.
Sorry, comments are closed for this article.
© 2000 - 2009 by thoughtbot, inc.
written by a bushel of tiny robots
Widgetfinger: Simple content management for simple websites.
Come “ride the toad” on Hoptoad, the app error app.
Tee-Bot, funny shirts your friends won't understand!
Umbrella Today: “It’s like totally the simplest weather report ever, Julie.”
Thoughtbot
thoughtbot is a technology consulting firm that provides web application development and design services. We focus on building modern systems, embracing good ideas and delivering elegant solutions.
Interested in learning Rails?
Sign up for our training.
Archives