A voting extension from scratch for Rails 3 (Part 4 – models and migrations)

Better late than never … finally Part 4 of the blog series about developing MakeVoteable, a voting gem for Rails 3.

In the last episode we took a deeper look how to get the logic inside our gem. In this episode we focus on the model this logic acts on. As we use ActiveRecord to store our model in a SQL database, it has to inherit from ActiveRecord::Base.

I decided for MakeVoteable to have just one model named Voting. This Voting model stores the connection between a Voteable and a Voter after a vote took place. That way it is possible to fetch the information afterwards what voters voted for what voteables. MakeVoteable tries to be agnostic about what a voter or voteable exactly is. A voter could even be a voteable (like a user that votes other users). To archive that MakeVoteable makes use of polymophic associations.

# lib/make_voteable/voting.rb
module MakeVoteable
  class Voting < ActiveRecord::Base
    attr_accessible :voteable, :voter, :up_vote
 
    belongs_to :voteable, :polymorphic => true
    belongs_to :voter, :polymorphic => true
  end
end

Those polymorphic associations allows us to easily call voting.voter = user1 or voting.voteable = question1 on a Voting instance. user1 or question1 can thereby be any ActiveRecord::Base derived model instance.

We also need a migration for our model that represents its database structure. This migration must later be included in the target application that uses the MakeVoteable extension. For easily setting this up, we will create an automatic generator in the next episode. For now we just need those migration details (tables, columns, indexes) for setting up the database of our testing environment. We already added an empty schema.rb file to our spec folder in a previous episode. This file gets the same migration details as the migration file the target application later gets.

Let’s have a look at those details for the above Voting model:

ActiveRecord::Schema.define :version => 0 do
  ...
  create_table :votings, :force => true do |t|
     t.string :voteable_type
     t.integer :voteable_id
     t.string :voter_type
     t.integer :voter_id
     t.boolean :up_vote, :null => false
     t.timestamps
  end
 
  add_index :votings, [:voteable_type, :voteable_id]
  add_index :votings, [:voter_type, :voter_id]
  add_index :votings, [:voteable_type, :voteable_id, :voter_type, :voter_id], :name => "unique_voters", :unique => true
end

The :force => true ensures that the table will be recreated for each test run (even if it is already there). The voteable_type, voteable_id, voter_type and voter_id columns are necessary for the mentioned polymorphic associations to work (they are actually the foreign keys). Additionally we store if this voting was an up (up_vote) or a down vote (down_vote) and a timestamp that gets automatically set when the model is saved.

As voting instances are very often accessed through its associations it is important to add indexes for those foreign key columns. In our case, as we use polymorphic associations, we have two columns for each association that together identifies the voter respectively the voteable. That’s why we also need to have one index include two columns (voteable_type and voteable_id, respectively voter_type and voter_id).

There is also another index that just guarantees that a combination of voteable_type, voteable_id, voter_type and voter_id is unique and thereby a voter can just have a voting for one voteable. I also check this programmatically (like here), but I feel much more secure when this is checked a the database level, too. That way you immediately know that something went completely wrong with your logic when such a database error was thrown.

Another important thing about database indexes you should know (even when irrelevant in this case) is that when you define an index like add_index :foo, [:col1, :col2, :col3], you automatically have defined indexes add_index :foo, [:col1] and add_index :foo, [:col1, :col2]. But you have NOT implicitly defined indexes like add_index :foo, [:col2] or add_index :foo, [:col3, :col1] as the column order matters. For more information see this blog post.

The vote count itself is stored together with the voter and voteable. This has the advantage that the vote count can be directly accessed by the respective model instance (user, question, whatever) without doing any further database joins. The (minor) disadvantage is that users must add those columns to their voter and voteable migrations manually inside the app that uses MakeVoteable (maybe I will add a parameterized generator for convenience).

For now we just setup some dummy models and add the migration details in our test schema file:

ActiveRecord::Schema.define :version => 0 do
  create_table :voteable_models, :force => true do |t|
    t.string :name
    t.integer :up_votes, :null => false, :default => 0
    t.integer :down_votes, :null => false, :default => 0
  end
 
  create_table :voter_models, :force => true do |t|
    t.string :name
    t.integer :up_votes, :null => false, :default => 0
    t.integer :down_votes, :null => false, :default => 0
  end
  ...
end

The voter simply stores how often he voted up or down and the voteable how often it was up or down voted (both beginning from 0).

So, that was it for today … upcoming is an insight into generators and how to test them.