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.sockwith 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:
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!
