Superhero

What Are Named Scopes?

Of all the great new features of Rails 2.1 (and the forthcoming Rails 2.2), named_scope is my favorite. named_scope provides a mechanism for wrapping up a set of find conditions. Let's check out an example to see how this all works.

Imagine we have a Person model with the following schema:

create_table :people do |t|
  t.string :eye_color
  t.integer :age
  t.boolean :admin
end

Now, in many of our finds we are probably going to be interested in that :admin attribute, especially when we're looking up users that are able to log into the admin section of our site. Normally, we might write a find something like this:

@admins = Person.find(:all, :conditions => {:admin => true})

That would grab all the people in our database who are admins. However, that find syntax is a little verbose, especially for a controller (which is probably where we might find code like this). named_scope to the rescue! We'll add the following to our Person model:

named_scope :admins, :conditions => {:admin => true}

This allows us to change our controller call to something a bit closer semantically to what we're actually doing, leaving the meat of the logic in our model where it belongs:

@admins = Person.admins

We may also be interested in our non-admins as well, so we might write another named scope like the following:

named_scope :non_admins, :conditions => {:admin => false}

But this is starting to seem a little redundant. Our admin and non_admin named scopes are almost the same, just flipping the boolean value in our conditions. There must be a better way... and there is! Our old friend lambda to the rescue:

named_scope :admins, lambda {|boolean| {:conditions => {:admin => boolean}} }

Now we can pass the boolean value to our named_scope like so:

@admins = Person.admins(true)
@non_admins = Person.admins(false)

Nice and DRY... Like a Good Chardonnay

Now, I know what you're thinking. You're thinking you'd like a nice glass of chardonnay right now. But this article's not about chardonnay. This article's about named_scope... so stay with me. No, you're thinking, "Gee, couldn't I just write some class-level finder methods and achieve the same thing?" Of course you could. Before Rails 2.1 and named_scope that's exactly what you might have done. Something like this:

def self.admins(boolean)
  find(:all, :conditions => {:admin => boolean})
end

And that would work the same as our "admins" named scope does, with a few big exceptions. The thing that makes named_scope so powerful is the ability to chain them together to make more complex finds.

Let's say our admin area for our site is going to have an ad for that chardonnay I mentioned earlier. Naturally, we're going to want to make sure our admins are also of legal drinking age. If we weren't using named scopes, we would write another class-level finder method, something like the following:

def self.drinkin_admins
  find(:all, :conditions => ['age >= ? and admin = ?', 21, true])
end

Hmm. Is your code Spidey-sense tingling like mine is? We've got essentially the same admin conditions from our admins class-level finder mixed in there. Not so DRY. Let's see how named scopes would make this situation better:

class Person < ActiveRecord::Base
  named_scope :admins, lambda {|boolean| {:conditions => {:admin => boolean}} }
  named_scope :drinkers, :conditions => ["age >= ?", 21]
end

Our resulting find might look something like this:

@drinkin_admins = Person.admins(true).drinkers

Just like that, we've got all the admins who are also over 21 years old. But more than that, we haven't mixed our conditions together. And because our drinkers named scope is separate, we can use it in other scenarios as well.

Furthermore, what if you need to get a count of all the admins who are over 21? Before named_scope, you'd write a second class-level finder method using the count method instead of find. But because named scopes soak up all those conditions and evaluate them at the time that you need them, you can simply add a .count to the end of your scope-chain.

@drinkin_admins_count = Person.admins(true).drinkers.count

Is That a Cape and Tights I See?

Of all the awesomeness that we've seen, named_scope is just getting started. Let's say one of our scopes is "almost" exactly what we want, but we really only need the first 10 results, or maybe we want to order the results in some special way. No worries, named_scope pulls out it's super hero outfit and saves the day:

@first_ten_drinkers = Person.drinkers.scoped(:limit => 10, :order => 'eye_color')

And just like that we can scope our named_scope even more, on the fly, no problems. The scoped named scope is special in that it can get merged into the scope chain and add additional conditions right there inline.

Now, this is all well and good, but we can make this whole thing even easier. What if we added another scope called limit, like the following:

named_scope :limit, lambda {|num| {:limit => num}}

Now we can limit any find we do off the Person model simply by including the limit scope in the chain, like so:

@first_ten_drinkers = Person.drinkers.limit(10).scoped(:order => 'eye_color')

Nice! Now that's feeling a whole lot better. I bet you can think of a few others that would be helpful too, like an order scope that lets you order by created_at dates. And we probably want these scopes on all of our ActiveRecord models, right? In fact, Ryan Daigle has already done a lot of that work for us with his utility_scopes gem (http://github.com/yfactorial/utility_scopes/).

script/plugin install git://github.com/yfactorial/utility_scopes.git

Now you'll have all kinds of great named scopes to make finding records easy as pie, including:

Model.with(:association)    #=> eager loads the association, so you don't have to.
Model.except(1, 2, 3)       #=> returns all of the models except the ones with ids 1, 2 or 3
Model.limited               #=> returns the first 10 (default, no argument)
Model.limited(5)            #=> returns the first 5
Model.order_by(:eye_color)  #=> results ordered by the eye_color attribute.

And much, much more. Check out the README for utility_scopes on github.

What's that you say? Pagination? With will_paginate? Well, wouldn't you know it, will_paginate uses named_scope to implement it's pagination methods. So, just tack a "paginate" on the end of your scope-chain, like so:

@drinkin_admins = Person.admins(true).drinkers.paginate(:page => params[:page])

Easy peasy.

For more information on named_scope, check out these fine articles across the internet:



Ryan_git

Git is a very powerful source control tool that is quickly gaining traction, especially in the Rails community. Getting started with Git, however, can be quite overwhelming thanks to its 100+ commands. The goal of this article is not to provide a comprehensive guide any means. Instead, this is simply a collection of useful tools and configurations that I have incorporated into my day-to-day workflow, using Git with Rails development using Mac OS X and TextMate.

Installation Essentials

Mac OS X Precompiled git packages: There are easy installers for both Leopard (10.5) and Tiger (10.4), just download, open and install.

Git From Source: If you would rather install Git from source, first download the latest package at git.or.cz. Open the file and cd into its folder, then:

make prefix=/usr/local all
sudo make prefix=/usr/local install
which git

If the last command returns '/usr/local/bin/git' then you're golden — if not, then you may need to add /usr/local/bin to your path.

Note: When you install from source, you also get the added benefit of being able to use Git to update itself.

TextMate Bundle: Tim Harper, with the help of various other contributors, has been nice enough to publish a very helpful TextMate bundle hosted on gitorious.org. If you feel like contributing (or just looking through the repository) go ahead and snoop around. To install the TextMate bundle:

# may need to create the Bundles directory if it does not exist
cd ~/Library/Application\ Support/TextMate/Bundles
git clone git://gitorious.org/git-tmbundle/mainline.git Git.tmbundle

In TextMate, go to Preferences > Advanced Tab > Shell Variables and set the TM_GIT variable to point to your installation of Git (ie /usr/local/bin/git). Once installed, this bundle will enable you to do all the common Git tasks such as pull, push, commit, stash, etc. all from within TextMate. It is worth mentioning that it also supports the ability to resolve conflicts with FileMerge, visualize branch history with gitk, and visualize history with gitnub.

Configuration Tips and Tricks

To set up Git's global configuration:

git config --global user.name "Name"
git config --global user.email "email"
git config --global color.status auto
git config --global color.diff auto 
git config --global color.branch auto
git config --global merge.tool opendiff
git config --global apply.whitespace nowarn

To alias checkout to co (ie git checkout branch_name becomes git co branch_name):

git config --global alias.co checkout

You can keep all those pesky Mac OS X .DS_Store files from being committed once and for all in a global .gitignore file. The .gitignore file is where you specify all files and patterns you would like Git to ignore.

git config --global core.excludesfile ~/.gitignore
echo ".DS_Store" >> ~/.gitignore

At the end of all this, my ~/.gitconfig looks like the following (you could also edit this file directly):

[user]
	name = user_name
	email = user_name@example.com
[alias]
	co = checkout
[apply]
	whitespace = nowarn
[color]
	status = auto
	diff = auto
	branch = auto
[merge]
	tool = opendiff
[core]
	excludesfile = /Users/anemic/.gitignore

To change fonts in gitk for better readability on Mac OS X, simply edit ~/.gitk and change it to:

set mainfont {Monaco 10}
set textfont {Monaco 10}
set uifont {Monaco 10}

You can set up Git to use TextMate instead of the default editor vim, simply add this to ~/.bash_login or ~/.profile :

export GIT_EDITOR="mate -w" 

If you would like to set up some more aliases for less typing on the command line, you can throw these into your ~/.bash_login or ~/.profile as well, for example:

alias gst='git status'
alias gl='git pull'
alias gp='git push'
alias gd='git diff | mate'
alias gc='git commit -v'
alias gca='git commit -v -a'
alias gb='git branch'
alias gba='git branch -a'

Initial Repository Setup

Git is a distributed control system, meaning that repositories can exist in many different locations. In the event of a failure of a single repository, other copies exist and can be cloned to minimize data loss. If you are working with a team you may find it useful to use a centralized server as a "main" repository for all users to push to and pull from.

To demonstrate this, I will first set up a standard Rails application:

rails git_test
cd git_test

I mentioned .gitignore files earler but only created a global .gitignore. We now can add local .gitignore files to this repository. These can be added to any directory, but I prefer to add one to RAILS_ROOT and another to RAILS_ROOT/log:

	mate .gitignore
	# add in TextMate (will ignore all .DS_STORE file in project)
	.DS_Store
	# save and close TextMate
	
	mate log/.gitignore
	# add in TextMate (will ignore all .log files in RAILS_ROOT/log)
	*.log
	# save and close TextMate

We already set the global .gitignore to not track .DS_Store files, but these local .gitignore files will be pushed up to the main repository and will be tracked so that every clone will ignore the same files.

We now need to initialize the Git repository, add all tracked files and commit all files to the local repository.

git init
git add . 
git commit -m 'initial import, not tracking any .DS_Store or log/*.log files'
 

To set up a remote repository, first create a directory on the remote server (ie mkdir preferred_repo_name.git). In this case I will create git_test.git. Copy the .git directory from your local repository to this newly created directory on the remote server:

scp -rp .git user@remoteserver.com:/path/to/repository/git_test.git

Now you need to set up your local repository to track the remote repository so that you are able to pull and push changes:

git remote add origin user@remoteserver.com:/path/to/repository/git_test.git

You are now ready to notify your team members that the repository is set up and ready for them to clone at the specified URL:

git clone user@remoteserver.com:/path/to/repository/git_test.git desired_local_repo_name
# desired_local_repo_name is optional and will default to what ever is before .git if not specified
hack... hack.. hack...
git commit -m "message for change log"
git pull 
git push

Note for Subversion users: clone is like checking out a branch, but instead of just checking out HEAD you get the entire repository history.

Your local repository will pull in any changes from remote that occurred since your last pull, and your local changes are pushed up to the remote server so team members can now pull your changes and push their own.

Merging and Branching

Merging and branching is very easy and inexpensive in Git thanks to how it tracks objects, not files:

git co master
git merge [your_branch]
git push

upstream	A-B-C-D-E            A-B-C-D-E-F-G
	             \        ---->               \
your branch	      C-D-E                        G

Now you are ready to go and make changes in your own branch until your next merge into master.

Tagging

One very useful tool in Git is tagging. Tagging allows you to preserve a snapshot of the repository at that time and use it as a reference to easily revert to if necessary.

git tag v1.0.0 -m 'finally a stable release'

To check out or revert to revision v1.0.0 at a later date:

git branch v1.0.0 origin/v1.0.0

This will check out revision v1.0.0 from the remote repository into your local branch v1.0.0. The first advice I would give to a Git beginner is to put a tag on the head revision before making significant changes (just in case).

Troubleshooting and Flexibility

What should you do if you find that you are no longer able to push or pull from the remote repository (ie your local repository has some how gotten corrupted)? First, check to see if you can still run git log. If so, create a patch of all the changes you have made since your last push:

git log
git diff [sha1-before-change-from-git-log] [sha1-after-change-from-git-log] > my.patch
mv my.patch ~/
cd ..
rm -rf git_test
git clone user@remoteserver.com:/path/to/repository/git_test.git
cd git_test
mv ~/my.patch ./
git apply my.patch

This will make a patch of all the changes, create a newly cloned localrepository and then apply the patch. You should now have a fresh copy with your changes and can continue working without any loss. The only drawback is that all the changes made between sha1-before-change and sha1-after-change have now been lumped into one single commit. To combat this, you could make a patch for each commit and then apply each one at a time, committing after each patch.

This is also useful if you are not particularly comfortable with rebase and wanted to take out a prior commit or reorder some commits.

git diff [sha1-before-change] [sha1-after-change] | patch -p1 -R

This way you are able to leave the history intact and remove all changes from sha1-before-change to sha1-after-change.

If you must cause a "rift in the space-time continuum" there is git rebase. This is where the diagram above showing commits and pulls goes right out the window and you are able to order the way Git applies the changes to you local branch.

Warning: git rebase is a bit like juggling knives — if done correctly it can be spectacular but there is an inherent risk of harm if mistakes are made. If you end up losing a finger or an entire limb there is always git gc to resolve this, but that goes beyond the scope of this article.

Another note worth mentioning is that git rebase should never be run on a remote branch or any branch that is being pulled from, as it will cause conflicts for everyone who already has a working copy. It is meant to be run on a local, private branch where you would like to be able to switch up or modify history in a way to make a better, more cohesive patch. If you do find the need to use git rebase, always put a tag (perhaps called BACKUP) on the head of the branch before doing so. That way, no matter what you do, you can easily restore your history by resetting the branch to that tag:

git reset BACKUP

Git's reset command comes to the rescue if you suddenly find your changes FUBARed the code base. If you just want to revert back to HEAD and toss out those changes, you can:

git reset --hard HEAD~3
# will permanently scrap the last three commits and revert your branch
git reset --hard HEAD~5
# will permanently scrap the last five commits and revert your branch

When you only want to merge a particular change from a branch (instead of merging the whole branch), Git allows you to just pick one commit from a different branch, apply the changes in revision sha1-rev and commit them to the current branch:

# in the branch you would like to take changes from
git log
# find the sha1-rev you would like to apply and take note
git checkout branch_you_want_changes_applied_to
git cherry-pick -x your-sha1-rev

Lets say you are working on a new, not-yet-stable feature. Suddenly you get an urgent request to make a bug fix but you are not ready to commit your other changes. No worries, just stash your local changes away for later reapplication:

# bat phone rings
git stash
git checkout master

# fix bug
git commit -a
git push

# problem solved, back to what you where doing before
git checkout [original_branch_you_where_working_on]
git stash apply
# if you have many stashes you can use 'git stash list' to see all the available stashes to apply 

Links



Track

I was working on a large community-driven website recently, which had always had the requirement to synchronize its user database with another (3rd party) site that we didn't control. The most we could get out of the other site was for them to send us regular XML dumps of the changes (additions, removals, deletions). Predictably, we wrote that as a rake task and added it to cron.

@hourly   cd /apps/product/current && export RAILS_ENV=production && rake product:synchronize_database

It worked great, until the client threw in ONE additional little snag; not only should it check for updates every hour, but an admin should be able to log in to the site and request an immediate synchronization. Unfortunately, this task can take anywhere from 10 minutes to 10 hours, depending on the size of the XML file and the number of employees involved. Clearly not a job for a simple backtick or %x{}.

There are a number of different options for running background processes in Rails, but since I had pretty simple requirements (and needed quick results) the best choice for me was clearly Spawn.

You can install the plugin from rubyforge:

script/plugin install http://spawn.rubyforge.org/svn/spawn/

Then implementing the client's request was as simple as adding a button with a remote_function and a controller action with:

spawn(:nice => 7) do
        exec("cd #{RAILS_ROOT} && export RAILS_ENV=#{RAILS_ENV} && rake product:synchronize_database")
end

The :nice option (vital, in my case!) will make sure that your process doesn't monopolize the CPU (just like its shell counterpart). There are only a couple of other options; you can choose to fork or thread your process (fork is the default), and you can wait for it to finish. Neither was necessary in my case. Problem solved and, in typical Rails fashion, it only took a few lines of code!




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites