Single-table inheritance was a difficult concept for me to grasp when I first started using Ruby on Rails. Part of this might stem from the fact that it’s not a perfectly modeled concept in Rails itself. Relational databases like MySQL, PostgreSQL, and sqlite don’t have any concept of (inherited) sub-tables.
Let’s say you want to track pets. Cats and dogs need to be treated differently, but they basically have the same set of attributes, and share some methods as well. Here’s how we setup our models:
# app/models/pet.rb
class Pet < ActiveRecord::Base
validates_attributes :name
def speak
''
end
end
# app/models/dog.rb
class Dog < Pet
def speak
'Woof!'
end
end
# app/models/cat.rb
class Cat < Pet
def speak
'Meow!'
end
end
Notice we include a default speak method in the parent class. This isn’t required, but it will help with any other pet types we may create in the future. This will be the default, so if we add a Fish category, we don’t need to define its own speak method separately.
All three models will share a common database table. In our migration, we’ll need to add a string column called “type”, that will contain the class name of the individual record:
class CreatePets < ActiveRecord::Migration
def self.up
create_table :pets do |t|
t.string :name
t.string :type
t.timestamps
end
end
def self.down
drop_table :pets
end
end
This is how Rails will know which records are cats, and which are dogs. You’ll usually never have to work with this column directly. Now calls to Dog.all will return only dog records from the pets table. Calling speak on a pet record will return “Woof!” for a dog record, and “Meow!” for a cat record.
Now, how do we test this? We should create a test file for each model class. We’ll test everything in that is included in the parent model in its test file, then only test the things that change in the child model tests:
# test/unit/pet_test.rb
require 'test_helper'
class PetTest < ActiveSupport::TestCase
def test_should_validate_presence_of_name
pet = Pet.new()
assert !pet.valid?
assert pet.errors.on(:name).include?("can't be blank")
end
def test_should_speak_blank
pet = Pet.new
assert_equal '', pet.speak
end
end
class DogTest < ActiveSupport::TestCase
def test_should_bark
dog = Dog.new
assert_equal 'Woof!', dog.speak
end
end
class CatTest < ActiveSupport::TestCase
def test_should_meow
cat = Cat.new
assert_equal 'Meow!', cat.speak
end
end
Single-table inheritance is powerful, and only slightly tricky to grasp. It’s well worth learning.
If you have an existing model in your Ruby on Rails application and you’ve already run the migration to create the table in the database, you may want to add columns later on. This is easy. Let’s say we have an ActiveRecord user model, and we want to add a rating for each user, to allow others to vote a user up or down. First, create the migration:
script/generate migration AddRatingToUsers
Or if you want to be fancy, we can use a little magic and specify our new column along with it:
Rails is smart enough to figure out the table name from the migration name, and you’ll see this in the migration file that is created:
class AddRatingToUsers < ActiveRecord::Migration
def self.up
add_column :users, :rating, :integer
end
def self.down
remove_column :users, :rating
end
end
If you run the migration right away, you’ll run into problems because all the users that exist will have a rating of nil, and this will probably break any calculations you have in your code. Let’s update it with a default value:
Now the database will default all existing and new users to a rating of zero.
More Complex Default Values
But what if you need a more complex default? Let’s say you want to add a unique API key for each user. Maybe we have a user instance method called “generate_api_key”, and it’s called whenever a new user is created:
class User < ActiveRecord::Base
after_create :generate_api_key
def generate_api_key
self.update_attribute(:api_key, self.username + '123')
end
end
But what about the users that were already in the database when we added the column? Should we put that in the migration, too? Absolutely not. It can be done, but migrations are not meant for data loading – only creating and changing the structure of your underlying database.
A better way is a rake task. These belong in the lib/tasks folder of your application, and they’re easy to create:
# lib/tasks/invitation_tokens.rake
namespace :seed do
desc "generate api keys for users that don't have one already"
task :api_keys => :environment do
User.all(:conditions => {:api_key => nil}).each do |user|
user.generate_api_key
end
end
end
Then, you can call this rake task from the command line like this:
rake seed:api_keys
This has a couple benefits. First, if you setup a new instance of your rails app on a different server (or even somewhere else on the same server) you can call this rake task to jumpstart your users. Even better, since you’re relying on a model method, you can unit test that method to make sure it performs as expected.
My initial set of controller tests are a great foundation. They lay out exactly how to take advantage of Shoulda to create tests for three different roles of user: anonymous visitor, member, and admin. Now we’ll use the Mocha ruby gem to speed up our tests, by eliminating database calls.
Here’s my (amateurish) video of the changes, followed by a more detailed explanation:
We’ll start by ensuring that rails will require the gem. Add this to config/environments/test.rb:
config.gem 'mocha'
Then on the commandline:
rake gems:install RAILS_ENV=test
Mocha allows us to stub (or “fake”) methods on an object, and track how often they’re called during a test. For example, if you stub an object’s save method in a create action, you can test what happens when saving succeeds (returns true) or fails (returns false), with no messy database interaction.
While I won’t get into a primer on Mocha itself, I will say that almost every database interaction can be removed from functional tests, with the exception of user authentication. I usually leave that in, and that’s my one and only use for fixtures. Fixtures are fast, but cumbersome if you begin adding multiple records for every model in your application. I use factories instead, but they’re not as fast for loading the database. Factories are another chapter in the Functional Tests saga, however.
That said, let’s get on to reviewing the stubbed versions of my tests. Starting with the admin context, our setup changes from this:
setup do
@valid = Factory.build(:setting).attributes
@setting = Factory :setting
login_as :admin
end
to this:
setup do
@setting = Factory.build :setting
@setting.id = 1001
Setting.stubs(:find).returns(@setting)
Setting.stubs(:find).with(:all, anything).returns([@setting])
login_as :admin
end
Instead of creating a new setting, we’re using our factory to build an unsaved one. We’re giving it an id since saving would have normally done this. Finally, we’re stubbing out the find method, both for an individual and for all settings, so they return the one we’ve built. Loggin in as admin will be our only database hit.
Now for the actions. The index, show, and new action tests stay exactly the same, except the first two are no longer hitting the database – we’ve stubbed out the find method to automatically return our fake setting. On to the create method, in the “with valid data” context. Here’s the original:
context "with valid data" do
setup do
post :create, :setting => @valid
end
should_assign_to :setting, :class => Setting
should_redirect_to("setting page"){setting_path(assigns(:setting))}
should_set_the_flash_to "Setting was successfully created."
should "create the record" do
assert Setting.find_by_name(@valid['name'])
end
end
And here’s the stubbed version:
context "with valid data" do
setup do
Setting.any_instance.expects(:save).returns(true).once
Setting.any_instance.stubs(:id).returns(1001)
post :create, :setting => {}
end
should_assign_to :setting, :class => Setting
should_redirect_to("setting page"){setting_path(1001)}
should_set_the_flash_to "Setting was successfully created."
end
We’ve stubbed any instance of Setting to return true upon save, without actually saving. No database hit, and we still get to test everything. We’ve even dropped our “create the record” test, and stopped passing in valid data, because the expectation we set in the setup handles this. To be more paranoid, we could pass in valid data and check that those params end up in the Setting.new call, but I think that’s overkill since it’s such a basic step.
Next, the “with invalid data” context. First the original:
context "with invalid data" do
setup do
post :create, :setting => {}
end
should_assign_to :setting, :class => Setting
should_respond_with :success
should_render_with_layout :settings
should_render_template :new
should_not_set_the_flash
end
And the new version:
context "with invalid data" do
setup do
Setting.any_instance.expects(:save).returns(false).once
post :create, :setting => {}
end
should_assign_to :setting, :class => Setting
should_respond_with :success
should_render_with_layout :settings
should_render_template :new
should_not_set_the_flash
end
The only changes here are that we’re setting a stubbed expectation of a call to save, forcing a return value of false, and again we don’t need to pass in valid data. Now we can run all the same tests, because we’ve forced the code down the failure branch.
Our edit tests stay the same, with the exception again of no database hit thanks to our stubbed finders. Next up is the update action, and we’ll start with the valid data context. Here’s the original:
context "with valid data" do
setup do
put :update, :id => @setting.id, :setting => {:name => 'Bob'}
end
should_assign_to(:setting){@setting}
should_redirect_to("setting page"){setting_path(assigns(:setting))}
should_set_the_flash_to "Setting was successfully updated."
should "update the record" do
@setting.reload
assert_equal 'Bob', @setting.name
end
end
And here’s the new version:
context "with invalid data" do
setup do
@setting.expects(:update_attributes).returns(false).once
put :update, :id => @setting.id, :setting => {}
end
should_assign_to :setting, :class => Setting
should_respond_with :success
should_render_with_layout :settings
should_render_template :edit
should_not_set_the_flash
end
Much like a successful create, we stub out our update to succeed and run all the same tests except that the record was actually changed. Now the invalid data context. Here’s the original:
context "with invalid data" do
setup do
put :update, :id => @setting.id, :setting => {:name => nil}
end
should_assign_to :setting, :class => Setting
should_respond_with :success
should_render_with_layout :settings
should_render_template :edit
should_not_set_the_flash
end
And the new version:
context "with invalid data" do
setup do
@setting.expects(:update_attributes).returns(false).once
put :update, :id => @setting.id, :setting => {}
end
should_assign_to :setting, :class => Setting
should_respond_with :success
should_render_with_layout :settings
should_render_template :edit
should_not_set_the_flash
end
And much like our unsuccessful create, we stub out the update to return false, and run the same tests! The finaly action that changes is update, and it’s very easy. First the original:
context "destroying" do
setup do
delete :destroy, :id => @setting.id
end
should_assign_to(:setting){@setting}
should_redirect_to("index"){settings_path}
should_not_set_the_flash
should "delete the record" do
assert !Setting.find_by_id(@setting.id)
end
end
And the new version:
context "destroying" do
setup do
@setting.expects(:destroy).once
delete :destroy, :id => @setting.id
end
should_assign_to(:setting){@setting}
should_redirect_to("index"){settings_path}
should_not_set_the_flash
end
This time we’re only stubbing out the call to destroy, expecting it to happen once. All the tests stay the same, except we no longer test that a record has actually been removed. No record was actually in the database, so we can’t. Plus, a failing call to destroy is so rare, it doesn’t even let you know in the return value.
The admin context is the only one that changes with these upgrades, members and visitors never get this far in our examples. This will generally make your functional tests over twice as fast, which is huge when you’re faced with mounting test times. I believe I was able to cut testing time down from 90 seconds to just under 30, implementing stubbing across an application’s test suite.
If you view the source code for this part of the series, you’ll see a lot of repetition in the tests. We’ll DRY (Don’t Repeat Yourself) that code in the next chapter in the series.
I recently read a blog post by Amr Mostafa that benchmarked running MySQL databases in memory. I’ve been trying to figure out how to do this, and he had the answer: use the tmpfs filesystem, which runs in memory, to store your database. I’ll have to figure out just how difficult that is later, since I’m not a super DBA…or even really a DBA at all.
Amr is not a Rails developer, and the purpose of his benchmark was to simulate regular web traffic. His results seemed ambiguous, but I noticed something missing in his trial: writes to the database. His benchmark tests only used select statements, which read from the database. While this is the majority of most database usage, I think the perfomance gain during writes would tip the scales decidedly in favor of running MySQL in memory, if you can afford the RAM.
This has an added benefit to us Ruby on Rails developers: we could potentially use it to dramatically increase the speed of our Test Driven Development, especially for those of use (should be all of us) using autotest! TDD requires running tests every few minutes, or even seconds. And writing to the database in tests is a lot bigger piece of the puzzle, since the database is recreated from scratch before every test.
For a lot of us, test suites are manageable. Autotest only runs tests for changed files during normal development, occasionally running the entire test suite. This, coupled with judicious mocking, stubbing and unit testing techniques can keep most test suites under control. But larger apps use increasingly more tests, and higher-level tools like RSpec can be especially resource-intensive.
I tried to contact Mostafa about running some benchmarks for write speed, but my comment was considered spam! I did get a smaller message through, so hopefully I’ll hear back. If so, I’ll post a link to his thoughts/results. Until then, I may dabble with doing this myself, and seeing what amateurish benchmarks I can run myself.
This is Part 3 in my series Retro-reading RailsSpace. Despite the book being slightly out of date, and geared toward beginners, it’s a classic. I’m reading it (finally) and posting what I learn.
First, I’m Cheating
I didn’t actually learn today’s migration mojo from RailsSpace directly. The book was more of an inspiration. I saw the old-school t.column call, and for the first time I wondered, “how does it work?”
The “What”
When I started using Ruby on Rails, this was just magic. And I didn’t question it…
create_table :posts do |t|
t.column :name, :string # poof, a string column #
t.string :desc # poof, a string column with even shorter syntax #
t.integer :views # poof, an integer column #
end
The “How”
So how does it work? The answer is the TableDefinition class we’ve been using. Don’t see it? It’s the t in t.column,t.string, and t.integer above. When we call create_table an instance of the TableDefinition class is created, and passed to the block (everything between do and end).
column is an instance method of TableDefinition, and adds the column definition to the TableDefinition class. t.string,t.integer, and others just call t.column on the backend.
The “Why”
I think it’s obvious why it was coded this way. Can you imagine a shorter, sweeter syntax? I experimented with lots of SQL shortcuts back in my Perl days, but nothing that worked this well. Blocks in general are a very cool aspect of Ruby, and Rails would lose a lot of its power without them.
I was looking at a new (to me) project today, and stumbled across a rails “gotcha” that needs attention. I tried to run rake test, but it failed. I received this error on each and every test:
rake test
SQLite3::SQLException: no such table
There are something like 60 migrations in this project, so I went (almost) straight to db/schema.rb which gives you the all-in-one look at your database structure. That’s where I stumbled on this:
db/schema.rb
# Could not dump table "generic_table" because of following StandardError
# Unknown type 'bool' for column 'generic_column'
it turns out a field was created with the type bool. I’m using SQLite3 for this project, but I did flip over to MySQL during debugging. Here’s what I found:
Under MySQL, bool and boolean appear to work just fine as column types. schema.rb is fine, tests are fine, and these two descriptors are synonymous.
Under SQLite3, boolean is the correct field descriptor. bool won’t break your migration, but:
rake db:schema:dump (which is called by rake db:migrate) will fail SILENTLY, and your schema.rb file will be corrupted.
rake db:test:prepare uses schema.rb, so it will fail, also SILENTLY.
tests will fail
rake db:reset also uses schema.rb, and so it will recreate a broken database.
Oddly, the bool field in ruby will return ‘f’ for false, and ‘t’ for true. If you set it to true, it does the “magic” on the backend, and there will be a ‘t’ in that field. Therefore, setting something to false doesn’t really work, because that field contains ‘f’, which equates to true.
Rails will do all of this without complaint. Your migrations, db:test:prepare, even code will run without raising red flags. Scary. Tests are where things will start breaking.
A few months ago, we had a problem. We had a (relatively) mature application we wanted to deploy to a new server. In this case, waited to deploy to the production server until after several weeks of development/testing in stage. We deployed via Webistrano, then we tried to deploy:migrate, and that’s where things went south.
Migrations that were written and worked with version X of our app failed 10 versions down the road. You’re especially vulnerable when something that was a model attribute (ergo, a table field) before is now an instance method, or vice versa. For example, maybe password starts out as an attribute, but later becomes a method because you want password=(x) to generate a hash.
If you’re doing a fresh setup on a new server, and your app has been through several migrations already, you should ditch migrations for the initial deploy and run db/schema.rb instead, with the command rake db:schema:load. It will create the latest version of your database schema, including the current version number so future migrations will run without a problem.
There is even a note about this in db/schema.rb itself, which I just found today:
helpful hint provided in schema.rb itself
Note that this schema.rb definition is the authoritative source for your
database schema. If you need to create the application database on another
system, you should be using db:schema:load, not running all the migrations
from scratch. The latter is a flawed and unsustainable approach (the more
migrations you'll amass, the slower it'll run and the greater likelihood
for issues).
Also, db/schema.rb is a great place to see the current state of your tables! Just this morning I returned to a dusty project, and needed to know what table fields already existed, and which I needed to add. In fact, that’s how I stumbled on the above notice.