1. Technology
You can opt-out at any time. Please refer to our privacy policy for contact information.

Reddit Clone in Rails Part 2: Posts

By

Reddit Clone in Rails Part 2: Posts

In the first part of the tutorial we set up the Rails project, explored Git a bit and generated a dummy User class. Now it's time to step things up a bit and implement the very core of a Reddit clone: links. Reddit is all about links, one would say that apart from the comments, links is all Reddit is. So we'll scaffold up a link class and explore ActiveRecord relations.

Branch and Scaffold

Before starting any task, we first create a git branch. Since we're scaffolding and doing the initial work on the Posts feature of the site, we'll call it scaffold_posts. Note that often Rails strings have special and implied meanings, but as these are only seen by Git, you can call your branch names anything at all. But its best to keep them meaningful instead of "foo" or "blah." There's no telling how long it'll be before you get back to the project, will you remember what the branch is for a week from now? A month?


$ git checkout -b scaffold_posts
Switched to a new branch 'scaffold_posts'
$ git branch
  master
* scaffold_posts

Once this is done, we'll go ahead and scaffold the Post model, migration, view and controller. This will generate everything we'll need in one command. It'll generate a migration with the fields we want already filled in, it'll generate a model (albeit a rather blank model), a controller that does all the things we need and the views so we can actually see something in the browser. The code generated by the scaffold command is rarely exactly what you want, but it's often such a good start, it makes sense to use it.

We're going to pass the fields we need in the Post model directly to the scaffold generator. The first two are simple, they're the title and URL as string, but the third might look a bit strange. It's the "references" type, with the name of "user." This is the first step to our ActiveRecord relations.

ActiveRecord relations are a way to relate rows in the database to each other. They come in the form of "one to one," "one to many" or "many to one" and "many to many." A product manual being associated with a single product in the database would be a one to one relationship. This would be referred to a has_one and belongs_to relationship. In our case, a User being associated with many Posts is a one to many relationship. In Rails-speak, we would say that "a User has_many Posts." Conversely, you would say that "a Post belongs_to a User." In order to implement this, each post needs to hold the user ID of the user that owns it, and this is what the "references" field is. We'll take a better look at this once we get up and running. For now, just run the command so we can look at them in the console.


$ rails generate scaffold Post title:string url:string user:references
      invoke  active_record
      create    db/migrate/20121129201411_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
…

Now take a look at the files generated by this command. The migration, for example, is a bit more interested than last time. It now has the fields we specified on the command line already in there for us.


class CreatePosts < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.references :user
      t.string :title
      t.string :url

      t.timestamps
    end
    add_index :posts, :user_id
  end
end

It's actually ready to be migrated right off the bat, so go ahead and do that with rake db:migrate. This will create the posts table.


$ rake db:migrate
==  CreatePosts: migrating ====================================================
-- create_table(:posts)
   -> 0.0027s
-- add_index(:posts, :user_id)
   -> 0.0003s
==  CreatePosts: migrated (0.0033s) ===========================================

Relationships

Before we even take a look at the new Post model in the Rails console, we need to set up the relationships. As discussed above, every post has a user that submitted that post. The user "owns" the post, and the post holds that user's ID in its user_id column. So, when looking a Post object you can get information about the user that owns it by cross-referencing it. ActiveRecord does this automatically, if you have a post p, the user that owns the post can be found with p.user. Conversely, a User can find all of the posts it owns. If you have a user in the variable u, you can find all of the posts owned by that user with u.posts.

Setting this up is very simple. Open up the two models, found in app/models/post.rb and app/models/user.rb. And, since Rails is so smart, you'll see that it's done some of your work for you. Since you added a "references" field to the Post model, it's seen that it belongs to the User model. It's automatically added the belongs_to relationship. So here is the Post model from app/models/post.rb.


class Post < ActiveRecord::Base
  belongs_to :user
  attr_accessible :title, :url
end

The Post belongs_to the User. This is a "many to one" relationship, there are many Posts that all belong to the same User. Just remember that the belongs_to relation belongs on the "many" model. Also take a look at the attr_accessible line. This will come into play shortly, it prevents a form on a page from mass-assigning to fields it shouldn't be able to. In this example, you cannot assign to the user field since it's not explicitly listed in the attr_accessible whitelist. This is an important security feature of Rails, don't simply list all of your attributes in there without thinking. But, as usual, Rails is smart and has done our work for us, guessing that we don't want to be able to assign to the user field from forms, but this will be a problem later, so pay attention to it.

Now we take a look at the User model and see the other side of the relationship. If you open up app/models/user.rb, you'll see a very blank model. There's a commented out attr_accessible line reminding you to do something about it, but as it has no attributes right now, we'll just leave that. You need to add only one line to the top of this class, has_many :posts. And that's it, both side of the relationship are set up.


class User < ActiveRecord::Base
  # attr_accessible :title, :body
  has_many :posts
end

A User has_many Posts. This is a "one to many" relationship, the other side of a "many to one." There is one User that has many Posts. The User doesn't keep track of which posts it owns, it simply queries the database for all Posts with a matching User ID. Since this is our dummy User model, there are no attr_accessible worries here at this time.

The Console

OK, time to fire up the Rails console. We'll start by retrieving the dummy user we made in the previous part using u = User.first. Rails auto-generates a whole host of methods to query the database for ActiveRecord objects. For example, in a future part we could be doing things like User.find_by_name, a auto-generated method that will find users by their username. But since there is only one User in the database, we'll pull it out by using User.first. We'll need this in order to create a Post.

Next, we'll create a Post object. This is done by simply calling p = Post.new and assigning the user and strings to its attributes. We'll finish it all off by saving the Post to the database.


$ rails console
Loading development environment (Rails 3.2.8)
irb(main):001:0> u = User.first
  User Load (0.1ms)  SELECT "users".* FROM "users" LIMIT 1
=> #
irb(main):002:0> p = Post.new
=> #
irb(main):003:0> p.user = u
=> #
irb(main):004:0> p.title = "The first post, Reddit of course!"
=> "The first post, Reddit of course!"
irb(main):005:0> p.url = "http://reddit.com/"
=> "http://reddit.com/"
irb(main):006:0> p.save
   (0.2ms)  begin transaction
  SQL (4.8ms)  INSERT INTO "posts" ("created_at", "title", "updated_at", "url", "user_id") VALUES (?, ?, ?, ?, ?)  [["created_at", Thu, 29 Nov 2012 20:41:16 UTC +00:00], ["title", "The first post, Reddit of course!"], ["updated_at", Thu, 29 Nov 2012 20:41:16 UTC +00:00], ["url", "http://reddit.com/"], ["user_id", 1]]
   (16.0ms)  commit transaction
=> true
irb(main):007:0> 

Why do this step? Why not jump right into the web interface? It's always best to check things out the console first, and now we can take a look at ActiveRecord relationships first hand. We have a User stored in the variable u and we want to find all of the Post owned by that User. So run u.posts and examine the output (note, this will only work once the Posts are actually saved to the database, don't forget to save!).


irb(main):002:0> u.posts
  Post Load (0.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1
=> [#]

If you know anything about SQL (not required, but very useful), you'll see that it's selecting all posts where the user_id column is equal to the user ID of the user. And it's returning an array of Post objects, ready to be made into links for the user. How handy is that? And on the other side, we'll look at p.user, accessing the User that owns the Post.


irb(main):004:0> p.user
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
=> #

Again, looking at the SQL, you can see it's selecting from the Posts table the user whose ID is the ID from the Post's user_id column. If you're used to developing web applications without an ORM (ActiveRecord is an "ORM," or Object-Relational Mapping, a fancy way of explaining everything we've been exploring this article), this is just plain magic. I, like many others, had come from PHP and SQL and this just blew me away, how much time and typing it saved! Also, since it decouples the database logic from the application logic, larger changes are entirely possible in Rails, where in PHP they would have taken a very long time.

Time to Merge?

How often you branch and merge is really up to you, but we're not really done with this task. It'll be finished in the next part of this tutorial, and that's what I'm planning to merge, but your branches and merges don't have to be the same as mine, so it's really up to you.

  1. About.com
  2. Technology
  3. Ruby
  4. Ruby on Rails
  5. Creating a Reddit Clone with Rails
  6. Reddit Clone in Rails Part 2: Posts

©2014 About.com. All rights reserved.