Subscribe to the MPC blog rss feed feed-icon-14x14
 

named_scope

Posted on 03:45AM on 09/21/2008
Tags: activerecord, ruby, rails

Lately everyone has been going mad over a relatively new addition to rails: named_scope. Up until now, I hadn't had the chance to try it out. I always just assumed that it was more syntactic fluff that seems to accumulate. Wow, was I wrong! It's an unbelievably cool and useful idea. Here's a really simple example. Say I have 2 models: Country and Region. They are as you would expect. A Region belongs to a Country and a Country has many Regions, etc. I use them pretty much for dropdown lists and things like that. Here's some code:

class Region < ActiveRecord::Base
  belongs_to :country
end

class Country < ActiveRecord::Base
  has_many :regions
end

Prior to named_scope, if you wanted to get all Regions in alphabetical order (as an example), you'd have to do something like:

class Region < ActiveRecord::Base
  #other code
  def self.ordered
    find(:all, :order => "name ASC")
  end
end

Which was ok, but this is nicer:

class Region < ActiveRecord::Base
  named_scope :ordered, :order => "name ASC"
  #other code
end

This creates a method on the Region class called ordered that can simply be called.

OK, so that's cool and all, but really not that big a deal, right? Well, no - until you consider that you can chain calls to named_scope, thus adding more and more constraints. For example:

class Region < ActiveRecord::Base
  belongs_to :country
  named_scope :ordered, :order => "name ASC"
  named_scope :by_country, lambda { |c| { :conditions => ["country_id = ?", c.id] } }
  named_scope :containing, lambda { |s| { :conditions => ["name like ?", "%#{s}%"] } }
end

class Runner
 c = Country.find_by_name("Canada")
 regions = Region.by_country(c).containing("A").ordered
 puts "Provinces in Canada containing the letter A in ascending order: #{regions.inspect}"
end

This generates SQL that looks like this:

SELECT * FROM `regions` WHERE ((name like '%A%') AND (country_id = 1)) ORDER BY name ASC

Another reason that I like name_scope is that because models such as these are frequently eused in dropdowns, etc, you can use named scope to return a very lightweight version of the class, while excluding attributes that are unnecessary for a simple dropdown. For example:

class Region < ActiveRecord::Base
  belongs_to :country
  named_scope :ordered, :order => "name ASC"
  named_scope :by_country, lambda { |c| { :conditions => ["country_id = ?", c.id] } }
  named_scope :containing, lambda { |s| { :conditions => ["name like ?", "%#{s}%"] } }
  named_scope :simple, :select => "id, name"
end

This allows me to only bring back the attributes that I need. And again, it could be chained with other named scopes

I think that this is a very powerful concept that allows developers to write more DRY and readable code.

0 Comments (Show) (Comments are closed for this post)

Importing Data From Sinatra App To Django App

Posted on 02:39PM on 09/11/2008
Tags: migration, django, sinatra, activerecord

Since I have recently ported this site to Django (see here), I realized that it would be nice to have my existing blog entries and comments ported over as well. I decided to basically just copy them from one database to another. While this would get the existing posts and comments, it would not move new ones over. That's fine. I just want some data in the Django version.

So my approach is relatively simple. Since both data models match (with the exception of table names), why not just use ActiveRecord? There are other ways, obviously, but I thought that this might be a nice exercise.

Here's the basic layout of my DataConversion project

DataConversion
  lib
    Source
      source_base.rb
      post.rb
      comment.rb
    Destination
      destination_base.rb
      post.rb
      comment.rb
    main.rb

Essentially, the concept here is to model each database seperately, but to allow them to interact through active record.

Here are the base classes:

#The destination base class
module Destination
  class DestinationBase < ActiveRecord::Base
    establish_connection(
      :adapter => "mysql",
      :username => "username",
      :password => "password",
      :database => "the_dest_db")
    self.abstract_class = true
  end
end

#The source base class
module Source
  class SourceBase < ActiveRecord::Base
    establish_connection(
      :adapter => "mysql",
      :username => "username",
      :password => "password",
      :database => "the_source_db")
    self.abstract_class = true
  end
end

Really all each does is set up it's own connection and declare itself abstract.

Here are the models:

#The destination models
module Destination
  class Post < DestinationBase
    self.table_name = "blog_post"
    has_many :comments
  end
end

module Destination
  class Comment < DestinationBase
    self.table_name = "blog_comment"
    belongs_to :post
  end
end

#The source models
module Source
  class Post < SourceBase
    has_many :comments
  end
end

module Source
  class Comment < SourceBase
    belongs_to :post
  end
end

Pretty straighforward, yes? The thing to note is that the Destination models specify a table name, since it is non-standard for ActiveRecord.

Now, here's the main.rb file

require 'rubygems'
require 'activerecord'
['source_base', 'post', 'comment'].each {|f| require File.join(File.dirname(__FILE__), "source", f)}
['destination_base', 'post', 'comment'].each {|f| require File.join(File.dirname(__FILE__), "destination", f)}

puts "Reading source posts ... "
puts "\n"

src = Source::Post.all

puts "Creating destination post from source post ... "
puts "\n"

src.each do |p|
  dest = Destination::Post.new
  p.attributes.select {|attr, value| attr != "id"}.each {|attr, value| eval("dest.#{attr}=p.#{attr}") }
  dest.save!
  p.comments.each do |c|
    dest_comment = Destination::Comment.new
    c.attributes.select {|attr, value| attr != "id" || attr != "post_id"}.each {|attr, value| eval("dest_comment.#{attr}=c.#{attr}")}
    dest_comment.post = dest
    dest_comment.save!
  end
end

puts "Done!"

So, in the main app, we load all of the source posts and iterate through them while creating new destination posts by copying their attribute values (and ignoring ids). We do the same with the comments for each, while also ignoring the source post id and making sure to assign it to the newly created destination post.

0 Comments (Show) (Comments are closed for this post)

Please note that I am currently unavailable for any large, long term work.