Screencasts

Using ActiveRecord with Sinatra

Show Notes

In this screencast we'll create a URL shortener in Ruby with Sinatra. We're going to show you how to integrate ActiveRecord with a Sinatra application.

You'll need this regular expression to follow along:

/^\b((?:https?:\/\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))$/

What you'll learn

  • How to integrate ActiveRecord in to Sinatra with sinatra-activerecord
  • How to build a URL shortener and how to use Base 62 in order to keep your urls short

Links

Script

Welcome to the Using ActiveRecord with Sinatra screencast. In this episode we'll show you how to build a URL shortener in Sinatra. If you're new to Sinatra, we recommend you watch our Introduction to Sinatra screencast before watching this video. We'll also be using Haml in our views, so we also recommend getting up to speed with our Introudction to Haml screencast.

ActiveRecord in Sinatra

To get Sinatra working with ActiveRecord, we could do it the long way, by configuring it all up ourselves and writing the migrations ourselves, but that would take quite a while. Thankfully, Blake Mizerany, the creator of Sinatra, has provided a gem that will get you up and running in a couple of minutes. The gem is called sinatra-activerecord.

sinatra-activerecord extends Sinatra to use ActiveRecord, and provides helpful rake tasks. Currently there are two rake tasks provided. There is db:create_migration for creating migrations, and the familiar db:migrate for running migrations. We'll go into the setup and usage of both later in this video.

Installing sinatra-activerecord

First let's get sinatra-activerecord installed. We need to install the gems: activerecord, sinatra-activerecord, and sqlite3 (for our database).

sudo gem install activerecord
sudo gem install sinatra-activerecord
sudo gem install sqlite3

Now that they're installed, we need to set up our Rakefile. To do this, let's first create our app.rb, then we'll require 'sinatra' and 'sinatra/activerecord'

require 'sinatra'
require 'sinatra/activerecord'

Then, we'll make a Rakefile, and require our 'app' and 'sinatra/activerecord/rake', which includes db:create_migration and db:migrate in our list of available rake tasks.

require 'app'
require 'sinatra/activerecord/rake'

So when we run rake -T in our console, we see both tasks are now available.

rake db:create_migration  # create an ActiveRecord migration in ./db/migrate
rake db:migrate           # migrate your database

Creating Our Model

Now that we've got this set up, let's create our first model ShortenedUrl. So lets type in rake db:create_migration and hit return. As you can see, we need to pass in a NAME option with the name of our migration. In our case, let's pass in NAME=create_shortened_urls.

As you can see, it's created a db/migrate folder with our migration in it. Now, let's write our up and down migration code.

In self.up we want to create the table :shortened_urls with the string :url. Let's also be sure to add an index on :url.

class CreateShortenedUrls < ActiveRecord::Migration
  def self.up
    create_table :shortened_urls do |t|
      t.string :url
    end
    add_index :shortened_urls, :url
  end

  def self.down
  end
end

Now to self.down. While sinatra-activerecord doesn't have a db:rollback task at the moment, it's always good to get in the habit of doing so. So we want to drop the table :shortened_urls on rollback.

class CreateShortenedUrls < ActiveRecord::Migration
  def self.up
    create_table :shortened_urls do |t|
      t.string :url
    end
    add_index :shortened_urls, :url
  end

  def self.down
    drop_table :shortened_urls
  end
end

So when we run rake db:migrate, you'll see the migration run fine, and it creates a development.db database with the table shortend_urls.

You can manually rollback by going into irb, requiring 'app', and then the migration path. Then, call 'down' on the migration class. In our case, CreateshortenedUrls.down. And as you can see, the table is dropped. Let's go up again and move on.

OK, in our app.rb let's create our ShortenedUrl model. This is done in our app.rb file by typing class ShortenedUrl, and then inheriting from ActiveRecord::Base. We can include validates_uniqueness_of and validates_presence_of too, just like you'd expect in Rails.

...
class ShortenedUrl < ActiveRecord::Base
  validates_uniqueness_of :url
  validates_presence_of :url
end

We should also add a regular expression to validate the format of the :url.

...
class ShortenedUrl < ActiveRecord::Base
  validates_uniqueness_of :url
  validates_presence_of :url
  validates_format_of :url, :with => /^\b((?:https?:\/\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))$/
end

We found a regular expression on John Gruber's Daring Fireball blog, but we've had to tweak it a little to fit our example a bit better. We'll include it in the show notes.

So that's all we need for our model. Now, to our routes.

Routes

We'll create a get route for our root path, for when people visit the site.

...
get '/' do
end

We'll also create a post route, to create our shortened URL.

...
post '/' do
end

We'll also create a get route to forward people from the shortened URL to their destination. Let's create a named parameter called :shortened.

...
get '/:shortened' do
end

Let's assume for the time being that the id of our saved ShortenedUrl is the shortened version of our URL. So let's find our ShortenedUrl with our :shortened parameter, which is its id.

...
get '/:shortened' do
    short_url = ShortenedUrl.find(params[:shortened])
end

Now let's redirect to the short_url's url.

...
get '/:shortened' do
    short_url = ShortenedUrl.find(params[:shortened])
    redirect short_url.url
end

Views

Now that we have our redirection code, let's add some view files.

Let's create a views folder and include a layout. We'll create a very simple, crude layout...

!!!
%html
  %body= yield

OK, now let's create an index view. In here, let's create a %form that posts to the root with a %label(for="url") [label for url] and a text%inputfor the URL. Finally, we'll add asubmit` button.

%form(action="/" method="POST")
  %label(for="url")
  %input(type="text" name="url" id="url")
  %input(type="submit" value="Shorten")

Let's not forget to add haml :index inside of our get route block for the root path.

...
get '/' do
  haml :index
end
...

Now, in our post route block, let's write the code for finding or creating our ShortenedUrl. So @short_url = ShortenedUrl.find_or_create_by_url(params[:url]) will either find or create a new @short_url. If @short_url.valid? [is valid] then we show the :success view. If not, let's show the :index again.

...
post '/' do
  @short_url = ShortenedUrl.find_or_create_by_url(params[:url])
  if @short_url.valid?
    haml :success
  else
    haml :index
  end
end
...

OK, so let's create a success.haml file, and in it, let's include our shortened url.

%p http://localhost:4567/#{@short_url.id}

Let's go back to our index and add an error message. If @short_url.present? [is present] and !@short_url.valid? [not valid] then let's inform the user that they've entered an "Invalid URL".

- if @short_url.present? && !@short_url.valid?
  %p Invalid URL: #{@short_url.url}
...

OK, so let's try it out.

As you can see, if we type in something invalid, we see the error message appear. And if we type in a valid URL, like http://screencasts.org, we the "shortened" URL.

Let's copy and paste this address into our address bar, hit return...and as you can see, it forwards us to Screencasts.org.

Shortening

Alright, so our code is working. But this isn't that much of a URL shortener since when we get to the id of 10 our URL will increase by one character. What we can do to prevent the premature lengthening of the URL is to convert our id into a Base 62 string. Base 62 uses letters and numbers, to write numbers with with less characters, since we have all 26 letters, upper and lower case, with the 10 digits, 0-9, all at our disposal. Twenty six uppercase letters, plus twenty six lowercase letters, plus ten numbers, equals 62. That means our URL won't increase by one character until we reach the 62nd URL. After that, our URL won't increase by one until the 3,844th URL is added, as that is 62 squared.

So using Base 62 scales well! Another benifit form using Base 62 is that since the characters are all alphanumerical, we won't have any problems with the shortened URLs.

To encode integers into Base 62 strings, we can use a gem called alphadecimal. This gem allows you to call .alphadecimal on integers to convert them to Base 62 strings. The reverse is also true. If you call .alphadecimal on a string, you get an integer returned.

61.alphadecimal
#=> "z"
 "z".alphadecimal
#=> 61

So let's install alphadecimal, using the command: sudo gem install alphadecimal.

Now let's require it in our app.rb.

require 'sinatra'
require 'sinatra/activerecord'
require 'alphadecimal'
...

Let's add a shorten method to our ShortenedUrl where we convert the id to a Base 62 string.

class ShortenedUrl < ActiveRecord::Base
  ...
  def shorten
      self.id.alphadecimal
  end
end

Then in our :success view, let's change id to shorten

%p http://localhost:4567/#{@short_url.shorten}

Now, we need to create a class method on ShortenedUrl to find it by the Base 62 string. We'll call this method find_by_shortened. We'll pass in a string, shortened and then convert it back to the integer, id by calling .alphadecimal. Then we just need to find that id.

class ShortenedUrl < ActiveRecord::Base
  ... 
  def self.find_by_shortened(shortened)
    find(shortened.alphadecimal)
  end

Finally we just need to update our redirect route block, so instead of a simple find we replace it with find_by_shortened.

...
get '/:shortened' do
    short_url = ShortenedUrl.find_by_shortened(params[:shortened])
    redirect short_url.url
end

And that's it. If we go back to our browser and enter a few more URLs, we see the Base 62 URLs being shown. And if we copy-and-paste them, we find that they redirect correctly too.

Thanks for watching! Subscribe to our RSS feed, follow us on Twitter, and please leave any questions, comments or suggestions for new screencasts in the comments below. If you like our videos, and think your friends, followers or colleagues would benefit from seeing them, please feel free share via any of the links below the video. We really appreciate your support.

See you next time!

← Latest Episodes

blog comments powered by Disqus