Courtesy of Twitter

Database & Models

As of Rails 2.0.2, it supports SQLite as the default database, and if you’re using it you won’t need to edit the database.yml file. If you’re one of the brave souls using an alternative, you might want to edit your config/database.yml to look like this:
config/database.yml

development:
username: root
password: password
database: forum
adapter: mysql
socket: /var/run/mysqld/mysqld.sock

with obviously ‘root’ and ‘password’ replaced with your actual username and password for MySQL.

Using SQLite means that it’ll store the database along with the Rails application in the db folder as a file. Nobody else should have access to this file unless you specifically give them access to it, so it’s secure also.

Let’s get started by generating how our users are going to log in by using a plugin called restful_authentication. restful_authentication can be installed by typing script/plugin install http://svn.techno-weenie.net/projects/plugins/restful_authentication/. After doing this, type script/generate authenticated user session to generate our first model (User) and controller (SessionController). This command will also generate our first migration named 001_create_users.rb and stored in db/migrate, and which looks a little like this:
db/migrate/001_create_users.rb

class CreateUsers < ActiveRecord::Migration
def self.up
create_table "users", :force => true do |t|
t.column :login,                     :string
t.column :email,                     :string
t.column :crypted_password,          :string, :limit => 40
t.column :salt,                      :string, :limit => 40
t.column :created_at,                :datetime
t.column :updated_at,                :datetime
t.column :remember_token,            :string
t.column :remember_token_expires_at, :datetime
 
end
end
 
def self.down
drop_table "users"
end
end

When we eventually run rake db:migrate this file will be ran and it will create our users table. The self.up definition of the migration is the code that will be ran when we migrate up to this migration, and the self.down defintion is the code that will be ran when we migrate down.

The first line in the self.up definition is create_table “users”, :force => true do |t| which calls the create_table migration method, creating a table called users with a primary key field of id, and also a thing called a TableDefinition. The :force => true after this will drop our table if it already exists and then the do |t| opens up the block where we can specify what fields are in our table. The fields specified are added to our TableDefinition, and ultimately to our table once the migration is finished running.

This migration is very Rails 1.2.6 and the migration isn’t very “sexy”. Sexy Migrations were introduced into Rails 2.0, but they were originally created by the guys at errtheblog. These are a DRYer (Don’t Repeat Yourself) way of writing migrations and generally end up being much shorter and neater than their predecessors. For example, a sexy way of writing the up method for this migration would be:
db/migrate/001_create_users.rb, the self.up method

  def self.up
create_table "users", :force => true do |t|
t.string :login, :email, :remember_token
t.string :crypted_password, :salt, :limit => 40
t.timestamps
t.datetime :remember_token_expires_at
end

Now isn’t that much sexier? Instead of calling #column on the TableDefinition, we now call the methods corresponding with the field types we want. In this example we’ve called #string, #timestamps and #datetime. Now #string and #datetime are simple methods, these will define fields with VARCHAR (or whatever your database of choice uses) and DATETIME. You’ll notice when we call #string, it works just like :column, except we can specify multiple column names and then the options after them, :limit => 40 in this instance. #timestamps is not so simple, it’ll create two datetime fields called created_at and updated_at. These two fields will store the precise date and time that your record was created and updated, respectively.

Great, now that we’ve covered the nitty-gritty of ActiveRecord migrations, lets move right on to applying what we learnt into creating a new migration. Type script/generate model forum in the console, or in the Projects view in netbeans right click on the Models folder and click generate, specifying forum as the name to begin. Note the singular version of the name, as it is defining a class of a Forum object. This’ll generate four new files for us, the model (app/models/forum.rb), a unit test for it (test/unit/forum_test.rb), fixtures (test/fixtures/forums.yml) for the test and a migration (db/migrate/002_create_forums.rb). Firstly we’ll take a peek in the migration:
db/migrate/002_create_forums.rb

class CreateForums < ActiveRecord::Migration
def self.up
create_table :forums do |t|
 
t.timestamps
end
end
 
def self.down
drop_table :forums
end
end

As you can see it’s already got some information filled out for us. It’ll create a table with three fields: created_at, updated_at and id when the migration is ran upwards, and it’ll drop it when it is ran downwards. Let’s add some fields to our migration so it now looks like this:
db/migrate/002_create_forums.rb

class CreateForums < ActiveRecord::Migration
def self.up
create_table :forums do |t|
t.string :title, :description
end
end
 
def self.down
drop_table :forums
end
end

Currently we only have need for the title and description fields in our forums table, so that’s all we’ll add in for now.
Running rake db:migrate should make this migration run successfully and now we have a forums table in our database.

Next up is the topic model, and to get this we’ll just run script/generate model topic or click generate on the Models folder again. Now the relationship between a topic and a forum is that 1 forum has many topics, and therefore a topic must belong to a forum. We’ll now define this relationship in our models:
app/models/forum.rb

class Forum < ActiveRecord::Base
has_many :topics
end

app/models/topic.rb

class Topic < ActiveRecord::Base
belongs_to :forum
belongs_to :user
end

app/models/user.rb

class User < ActiveRecord::Base
has_many :topics
# insert all the stuff generated by restful authentication here
end

What we’ve done here is called #has_many and #belongs_to in our models, which gives us a whole swag of methods and helps us in regards to dealing with the relationship between objects of these classes. For instance, when I initialize a forum object as, say, @forum, to get all the topics for the forum I don’t need to do SELECT * FROM `topics` WHERE `forum_id` = ‘#{@forum.id}’ as ActiveRecord will do this for us, we can just do @forum.topics. Similarly, with a topic object we can just do @topic.forum to find out which forum it belongs to. There’s many more options that these methods give you, and some of them will be covered later on in the tutorial once we get to the controllers.

Our topic migration now needs to be created. We’re going to need a forum_id (int), user_id (int), subject (string), timestamps (datetime), and locked (boolean), which will make our migration look like this:
db/migrate/003_create_topics.rb

class CreateTopics < ActiveRecord::Migration
def self.up
create_table :topics do |t|
t.integer :forum_id, :user_id
t.string :subject
t.timestamps
t.boolean :locked
end
end
 
def self.down
drop_table :topics
end
end

Fairly standard migration by now, no new tricks here! Running it will give us our topics table.

Our final model and table will be our posts table, script/generate model post or right click on the Models directory, click generate and specify post as the name. This will give us the model, migration, test and fixture file. Our model should look like this:
app/models/post.rb

class Post < ActiveRecord::Base
belongs_to :user
belongs_to :topic
end

We’ll need to had a has_many :posts to our Topic and User models, and a has_many :posts, :through => :topics in our forum model. Now we’ve already covered #has_many with just one option, the association name. In our forum model we’ve specified it with an additional option, and that is :through => :topics. This will gather up all the posts related to all the topics that belong to our forum for us. The catch here is that you won’t be able to get all the users that have posted on a forum by doing has_many :users, :through => :posts, as this will generate a query that will look on the posts table for the related forum_id, and then find all the users on it and throw this error:

Evil SQL Error

ActiveRecord::StatementInvalid: Mysql::Error: #42S22Unknown column 'posts.forum_id' in 'where clause':
SELECT users.* FROM users  INNER JOIN posts ON users.id = posts.user_id    WHERE ((posts.forum_id = 1))

So four level has_many relationships are a big no-no, ok? Great. There is no logical reason why a model four “jumps” away from another model should depend on that other model.

So, just a recap, by now our forum model should has_many :topics and has_many :posts, with the posts association going :through => :topics. The topic model should has_many :posts, belongs_to :user and belongs_to :forum. The post model should belongs_to :user and belongs_to :topic. Finally, the user model should has_many :topics and has_many :posts. It should make the whole model situation look like this:
app/models/forum.rb

class Forum < ActiveRecord::Base
has_many :topics
has_many :posts, :through => :topics
end

app/models/topic.rb

class Topic < ActiveRecord::Base
belongs_to :forum
belongs_to :user
has_many :posts
end

app/models/post.rb

class Post < ActiveRecord::Base
belongs_to :topic
belongs_to :user
end

app/models/user.rb

class User < ActiveRecord::Base
has_many :topics
has_many :posts
# insert all the stuff generated by restful authentication here
end

Let’s not forget our final migration, the one that’s going to create our posts table.
db/migrate/004_create_posts.rb

class CreatePosts < ActiveRecord::Migration
def self.up
create_table :posts do |t|
t.text :text
t.integer :topic_id, :user_id
t.timestamps
end
end
 
def self.down
drop_table :posts
end
end

Run rake db:migrate and we’ll have our last migration.

Onwards!