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
I decided for MakeVoteable to have just one model named
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.
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
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
:force => true ensures that the table will be recreated for each test run (even if it is already there). The
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 (
There is also another index that just guarantees that a combination of
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.