The Artima Developer Community
Sponsored Link

Ruby Buzz Forum
Fun with Single Table Inheritance

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
Scott Patten

Posts: 43
Nickname: spatten
Registered: Jan, 2008

Scott Patten is a freelance web developer and Ruby on Rails trainer based in Vancouver
Fun with Single Table Inheritance Posted: Jan 24, 2008 1:36 PM
Reply to this message Reply

This post originated from an RSS feed registered with Ruby Buzz by Scott Patten.
Original Post: Fun with Single Table Inheritance
Feed Title: Scott Patten's Blog
Feed URL: http://feeds.feedburner.com/scottpatten.ca
Feed Description: Scott Patten is the cofounder of Ruboss (http://ruboss.com) and Leanpub (http://leanpub.com), both based in Vancouver. He is also the author of The S3 Cookbook (http://leanpub.com/thes3cookbook). He blogs about Startups, Ruby, Rails, Javascript, CSS, Amazon Web Services and whatever else strikes his fancy.
Latest Ruby Buzz Posts
Latest Ruby Buzz Posts by Scott Patten
Latest Posts From Scott Patten's Blog

Advertisement

I’m working on the sample application for the Intermediate Ruby on Rails Workshop that Gerald Bauer and I are putting on in January. I had to remind myself of some of the foibles of Single Table Inheritance (STI). I thought others might find this useful, so here’s what I found.

The Set-up

I have two types of posts in the application I’m building: posts by people with an empty room looking for a roomie (RoomiePosts), and posts by people looking for a room (RoomPosts). They both share a lot of the same attributes, so it’s not very DRY to create two separate models.

This is where Single Table Inheritance comes in. STI is basically a way of subclassing a single model, creating subclasses that all use the same database table.

What I did is create a Post model and two subclasses: RoomiePost and RoomPost. Here’s how you do it.

Creating the Database Table

Here’s the migration for the posts database table. Note that this uses the new migrations syntax in Rails 2.0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.integer :rent
      t.date :start_date, :expiry_date
      t.boolean :includes_utilities
      t.string :text, :type
      
      t.timestamps
    end
  end

  def self.down
    drop_table :posts
  end
end

Most of the columns are pretty straightforward. text will contain the text of the post. start_date and expiry_date will determine when the post will start and stop being shown on the site. rent and includes_utilities will only be used by the RoomiePost subclass, but still need to be created for all subclasses of the Post model.

The interesting column is type. This allows Rails to determine which subclass the row should be loaded in to. If you set type = "RoomiePost", then it’s a RoomiePost. If you set type = "RoomPost", then it’s a RoomPost.

Creating the Models

You need to create a Post, RoomiePost and RoomPost model. They look like this:

In /app/models/post.rb
1
2
3
class Post < ActiveRecord::Base 
  
end
In /app/models/roomie_post.rb
1
2
3
class RoomiePost < Post
  
end
In /app/models/room_post.rb
1
2
3
class RoomPost < Post
  
end

Note that RoomiePost and RoomPost are both subclasses of the Post model. Here’s where you run in to a “gotcha”.

You have to create the models in separate files. If you don’t, Rails ignores the subclassed models.

For more information, and another workaround, see this blog post. The comment by Chris at the bottom gives the ‘create the models in separate files’ solution.

Okay, so now you have the models all set up. You can do things like


>> p = RoomiePost.create(:rent => 800, :includes_utilities => true, :text => "Great room in a shared house in the Commercial Drive area") 

And it will create a row in the posts table with type = "RoomiePost". If you have 5 RoomiePosts and 2 RoomPosts, then Posts.count will return 7.

1
2
3
4
5
6
>> RoomiePost.count           
=> 5
>> RoomPost.count
=> 2
>> Post.count
=> 7

Adding behaviour to STI models

I want to be able to determine if a post is active by looking at the start and expiry dates, and to get a list of all active posts, roomie_posts and room_posts. I also want to have a different icon for each post type.

The Post.active_post? and Post::active_posts methods are straight-forward:

1
2
3
4
5
6
7
8
9
10
11
class Post < ActiveRecord::Base
  
  def is_active?
    (start_date .. expiry_date) === DateTime.now
  end 
  
  def self.active_posts
    self.find(:all).select {|p| p.is_active? }
  end  
  
end

The only tricky part is that I used self.find(:all) instead of Post.find(:all) in the Post::active_posts method. This makes sure that RoomiePost::active_posts only returns RoomiePosts and RoomPost::active_posts only returns RoomPosts.

Getting the icon to work took a little more figuring out. If I wanted to do this in straight-up Ruby, I’d do something like

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
#!/usr/bin/env ruby

class Post
  ICON_PATH = "icons"
  
  def initialize
    @icon_name = 'base.gif'
  end
  
  def icon
    File.join(ICON_PATH, @icon_name)
  end
  
end

class RoomiePost < Post
  
  def initialize
    @icon_name = 'roomie.gif'
  end
  
end

class RoomPost < Post
  
  def initialize
    @icon_name = 'room.gif'
  end
  
end

Then I could do this

1
2
3
4
5
6
7
ruby $>irb -r subclass_test
irb(main):001:0> Post.new.icon
=> "icons/base.gif"
irb(main):002:0> RoomiePost.new.icon
=> "icons/roomie.gif"
irb(main):003:0> RoomPost.new.icon
=> "icons/room.gif"

This doesn’t quite work in the Rails version. Not to worry, you only have to make one simple change:

replace the initialize method with an after_initialize callback.

The final post.rb, roomie_post.rb and room_post.rb files look like this:

post.rb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Post < ActiveRecord::Base

  ICON_PATH = "/images/icons"
  
  def after_initialize
    @icon_name = 'base.gif'
  end
  
  def icon
    File.join(ICON_PATH, @icon_name)
  end  
  
  def is_active?
    (start_date .. expiry_date) === DateTime.now
  end 
  
  def self.active_posts
    self.find(:all).select {|p| p.is_active? }
  end  
  
end
roomie_post.rb:
1
2
3
4
5
6
7
class RoomiePost < Post
  
  def after_initialize
    @icon_name = 'roomie.gif'
  end
  
end
room_post.rb:
1
2
3
4
5
6
7
class RoomPost < Post
  
  def after_initialize
    @icon_name = 'room.gif'
  end
  
end

One final note: because of the way the after_initialize callback works, you have to actually define an after_initialize method. You can’t do something like this:

1
2
3
4
5
6
7
8
9
10
class RoomiePost < Post
  after_initialize :set_icon_name

  private

  def set_icon_name
    @icon_name = 'roomie.gif'
  end
  
end

If you want to learn more about STI, and a whole slew of other info on Rails, sign up for the Intermediate Rails Workshop on January 25th, 2008 in Vancouver.

Early Bird rates are available until January 9th!

Read: Fun with Single Table Inheritance

Topic: MicroTip #1 Previous Topic   Next Topic Topic: Remove files that are not in subversion

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use