Micro-Reddit walkthrough
02 May 2014Estimated Time: 1 hr
Course: Ruby on Rails » Databases and Active Record » Project: Building With Active Record
##Objective:
Let’s build Reddit. Well, maybe a very junior version of it called micro-reddit. In this project, you’ll build the data structures necessary to support link submissions and commenting. We won’t build a front end for it because we don’t need to… you can use the Rails console to play around with models without the overhead of making HTTP requests and involving controllers or views.
##Basic Steps:
- Create Rails App
- Create User Model
- Create Post Model
- Build Associations between User and Post models
- Create Comment Model
- Build additional assocations
- Add Validations to Comment Model
- Check Associations in Console
Step 1: Create Rails App
Create a basic Rails app. Rails does all the heavy lifting. All you have to do is run
the rails new <project name>
command from your terminal, which creates a basic Rails directory structure with everything you need to run a simple app.
From the command line, run:
jamies-air:~ jxberc$ rails new micro-reddit
And you should see:
jamies-air:~ jxberc$ rails new micro-reddit
create
create README.rdoc
create Rakefile
create config.ru
create .gitignore
create Gemfile
create app
create app/assets/javascripts/application.js
create app/assets/stylesheets/application.css
create app/controllers/application_controller.rb
create app/helpers/application_helper.rb
create app/views/layouts/application.html.erb
...
After you create new direcotry, change into that directory from the command line:
jamies-air:~ jxberc$ cd micro-reddit
jamies-air:micro-reddit jxberc$
Step 2: Create a User Model
From the command line:
jamies-air:micro-reddit jxberc$ rails generate model User username:string email:string password:string
The rails generate
script creates templates for models, controllers, and views. In this case, we will generate a new model
, and name it User
.
The additional arguments username:string
, email:string
, and password:string
create 3 columns and sets their data type to string.
After you create the User Model, you should see this in your terminal:
jamies-air:micro-reddit jxberc$ rails generate model User username:string email:string password:string
invoke active_record
create db/migrate/20140502132449_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
Check the micro-reddit/db/migrate/
folder to confirm migration file was created. It should look like this:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :username
t.string :email
t.string :password
t.timestamps
end
end
end
From the command line, run:
jamies-air:micro-reddit jxberc$ rake db:migrate
== 20140502150931 CreateComments: migrating ===================================
-- create_table(:comments)
-> 0.0074s
== 20140502150931 CreateComments: migrated (0.0074s) ==========================
The rake db:migrate
command should create a sql database in your micro-reddit/db
folder.
Working with the Model in the Console
From command line:
jamies-air:micro-reddit jxberc$ rails console
Loading development environment (Rails 4.0.4)
2.0.0-p451 :001 >
Check User Table; it should be empty:
2.0.0-p451 :003 > User.all
User Load (0.3ms) SELECT "users".* FROM "users"
=> #<ActiveRecord::Relation []>
Create a new User record:
2.0.0-p451 :004 > u = User.new
=> #<User id: nil, username: nil, email: nil, password: nil, created_at: nil, updated_at: nil>
Check if record is valid:
2.0.0-p451 :005 > u.valid?
=> true
It’s currently valid because we have no validations. We don’t want to accidently create blank usernames, so we can create validations in the app/models/user.rb
file:
class User < ActiveRecord::Base
validates :username, presence: true, uniqueness: true,
length: { maximum: 20 }
end
Reload the console, and confirm validations are working:
2.0.0-p451 :010 > reload!
Reloading...
=> true
2.0.0-p451 :011 > u2 = User.new
=> #<User id: nil, username: nil, email: nil, password: nil, created_at: nil, updated_at: nil>
2.0.0-p451 :012 > u2.valid?
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."username" IS NULL LIMIT 1
=> false
2.0.0-p451 :013 >
If you can’t save record to database, it is good to check error messages with #errors.full_messages
method:
2.0.0-p451 :021 > u = User.new(username: 'abcdefghijklmnopqrstuvwxyz')
=> #<User id: nil, username: "abcdefghijklmnopqrstuvwxyz", email: nil, password: nil, created_at: nil, updated_at: nil>
2.0.0-p451 :024 > u.errors.full_messages
=> ["Username is too long (maximum is 20 characters)"]
Finally, create a user and save to User table:
2.0.0-p451 :019 > u = User.new(username: 'Thor', email: 'Thor@email.com', password: 'foobar')
=> #<User id: nil, username: "Thor", email: "Thor@email.com", password: "foobar", created_at: nil, updated_at: nil>
2.0.0-p451 :020 > u.save
(0.1ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."username" = 'Thor' LIMIT 1
SQL (4.8ms) INSERT INTO "users" ("created_at", "email", "password", "updated_at", "username") VALUES (?, ?, ?, ?, ?) [["created_at", Sat, 03 May 2014 15:43:22 UTC +00:00], ["email", "Thor@email.com"], ["password", "foobar"], ["updated_at", Sat, 03 May 2014 15:43:22 UTC +00:00], ["username", "Thor"]]
(0.8ms) commit transaction
=> true
Step 3: Create a Post Model
Generate new model Post
:
jamies-air:micro-reddit jxberc$ rails generate model Post title:string body:text
invoke active_record
create db/migrate/20140502133817_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
Code above generates Post
model with title:
and body:
columns.
Migrate table to database:
jamies-air:micro-reddit jxberc$ rake db:migrate
== 20140502133817 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0062s
== 20140502133817 CreatePosts: migrated (0.0063s) =============================
Add some validations to app/models/post.rb
:
class Post < ActiveRecord::Base
validates :title, presence: true
validates :body, presence:true
end
Confirm that you can create and save a post in the console:
Run rails console
if you haven’t already.
2.0.0-p451 :025 > p = Post.new
=> #<Post id: nil, title: nil, body: nil, created_at: nil, updated_at: nil, user_id: nil>
2.0.0-p451 :026 > p.title = 'First Post'
=> "First Post"
2.0.0-p451 :027 > p.body = 'Hello World'
=> "Hello World"
2.0.0-p451 :028 > p.save
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO "posts" ("body", "created_at", "title", "updated_at") VALUES (?, ?, ?, ?) [["body", "Hello World"], ["created_at", Sat, 03 May 2014 17:21:54 UTC +00:00], ["title", "First Post"], ["updated_at", Sat, 03 May 2014 17:21:54 UTC +00:00]]
(0.8ms) commit transaction
=> true
Step 4: Build Associations between User and Post Models
Generate Migration to Add Foreign Key to Post
Model.
jamies-air:micro-reddit jxberc$ rails generate migration AddForeignKeyToPost user:references
invoke active_record
create db/migrate/20140502140547_add_foreign_key_to_post.rb
The user:references
argument will add a column with foreign key that references User model.
Check db/migrate
folder to see new migration file:
class AddForeignKeyToPost < ActiveRecord::Migration
def change
add_reference :posts, :user, index: true
end
end
Run migration:
jamies-air:micro-reddit jxberc$ rake db:migrate
== 20140502140547 AddForeignKeyToPost: migrating ==============================
-- add_reference(:posts, :user, {:index=>true})
-> 0.0033s
== 20140502140547 AddForeignKeyToPost: migrated (0.0034s) =====================
Create relationships in app/models/post.rb
and app/models/user.rb
:
class Post < ActiveRecord::Base
...
belongs_to :user
end
class User < ActiveRecord::Base
...
has_many :posts
end
The above code creates the relationship between Post
and User
models. It basically says a post belongs to a user, and a user can have many posts. This is known as a one-to-many relationship.
Goign forward, you will likely see the belongs_to
and has_many
helper methods a lot when building models in Rails.
###Confirm associations in console:
First, create a new user,
2.0.0-p451 :016 > u = User.create(username: "Odin", email: "odin@email.com", password: "foobar")
...
=> #<User id: 5, username: "Odin", email: "odin@email.com", password: "foobar", created_at: "2014-05-03 18:02:23", updated_at: "2014-05-03 18:02:23">
Now create new post referencing new user. Add the user_id
from previously created user. In the above example, the user id is 5.
2.0.0-p451 :017 > p = Post.create(title: "A New Post", body: "A post by Odin himself.", user_id: 5)
...
=> #<Post id: 4, title: "A New Post", body: "A post by Odin himself.", created_at: "2014-05-03 18:03:16", updated_at: "2014-05-03 18:03:16", user_id: 5>
Confirm you can find posts for given user:
2.0.0-p451 :018 > u.posts
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ? [["user_id", 5]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 4, title: "A New Post", body: "A post by Odin himself.", created_at: "2014-05-03 18:03:16", updated_at: "2014-05-03 18:03:16", user_id: 5>]>
Finally, confirm you can find User for given post (the other side of the relationship):
2.0.0-p451 :021 > p.user
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 5]]
=> #<User id: 5, username: "Odin", email: "odin@email.com", password: "foobar", created_at: "2014-05-03 18:02:23", updated_at: "2014-05-03 18:02:23">
Step 5: Build Comment Model
Generate a Comment
Model:
jamies-air:micro-reddit jxberc$ rails generate model Comment body:text user:references post:references
invoke active_record
create db/migrate/20140502150931_create_comments.rb
create app/models/comment.rb
invoke test_unit
create test/models/comment_test.rb
create test/fixtures/comments.yml
Running the above command will create a migration file located in db/migration
folder. See below:
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.text :body
t.references :user, index: true
t.references :post, index: true
t.timestamps
end
end
end
Run migration:
jamies-air:micro-reddit jxberc$ rake db:migrate
== 20140502150931 CreateComments: migrating ===================================
-- create_table(:comments)
-> 0.0074s
== 20140502150931 CreateComments: migrated (0.0074s) ==========================
##Step 6: Building Additional Associations
In the app/models
folder, update associations for each of the models:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
...
end
class User < ActiveRecord::Base
...
has_many :posts
has_many :comments
end
class Post < ActiveRecord::Base
...
belongs_to :user
has_many :comments
end
##Step 7: Add Validations to Comment Model
We’re adding validations to the Comment model, so we don’t accidentally create comments with no associated User or Post.
In the file app/models/comment.rb
, include validations:
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :post
validates :user, presence:true
validates :post, presence:true
validates :body, presence:true
end
By validating :user
and :post
, we validate based on associated objects. Another way is to validate
using the foreign keys: :user_id
and post_id
, but I’ve read this is not as robust as the above validations.
Validating :user
and :post
will check that associated object exists, while validating on foreign key will only check that a key was entered, but not whether that :user_id
or :post_id
actually exist in the other models.
Let’s test in the console:
2.0.0-p451 :049 > c = Comment.new(body: "I enjoyed your post!")
=> #<Comment id: nil, body: "I enjoyed your post!", user_id: nil, post_id: nil, created_at: nil, updated_at: nil>
2.0.0-p451 :050 > c.save
(0.1ms) begin transaction
(0.1ms) rollback transaction
=> false
2.0.0-p451 :051 > c.errors.full_messages
=> ["User can't be blank", "Post can't be blank"]
First, I create a new comment with :body
argument and try to save. I cannot save, lets check the errors.full_messages
.
Next, add auser_id
and post_id
to your comment. They need to already exist in your User and Post models:
2.0.0-p451 :052 > c.user_id = 5
=> 5
2.0.0-p451 :053 > c.post_id = 4
=> 4
2.0.0-p451 :054 > c.save
(0.1ms) begin transaction
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 5]]
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1 [["id", 4]]
SQL (1.0ms) INSERT INTO "comments" ("body", "created_at", "post_id", "updated_at", "user_id") VALUES (?, ?, ?, ?, ?) [["body", "I enjoyed your post!"], ["created_at", Sun, 04 May 2014 21:08:34 UTC +00:00], ["post_id", 4], ["updated_at", Sun, 04 May 2014 21:08:34 UTC +00:00], ["user_id", 5]]
(1.7ms) commit transaction
=> true
It looks like it is now a valid comment. Also check to make sure you cannot create a comment with invalid user_id
and post_id
.
Lastly, check to see if you can find comments from User and Post objects:
2.0.0-p451 :056 > u = User.find(5).comments
...
=> #<ActiveRecord::Associations::CollectionProxy [#<Comment id: 3, body: "another comment", user_id: 5, post_id: 4, created_at: "2014-05-04 20:43:47", updated_at: "2014-05-04 20:43:47">, #<Comment id: 4, body: "I enjoyed your post!", user_id: 5, post_id: 4, created_at: "2014-05-04 21:08:34", updated_at: "2014-05-04 21:08:34">]>
2.0.0-p451 :057 > p = Post.find(4).comments
...
=> #<ActiveRecord::Associations::CollectionProxy [#<Comment id: 3, body: "another comment", user_id: 5, post_id: 4, created_at: "2014-05-04 20:43:47", updated_at: "2014-05-04 20:43:47">, #<Comment id: 4, body: "I enjoyed your post!", user_id: 5, post_id: 4, created_at: "2014-05-04 21:08:34", updated_at: "2014-05-04 21:08:34">]>
##Step 8: Check valid associations in Console
This is a final run-through to make sure the associations between the User
, Post
, and Comment
models are working as we expect.
My tables should be different than yours, so play around with your sample data and make sure you can return similar results:
2.0.0-p451 :067 > u = User.find(5)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 5]]
=> #<User id: 5, username: "Odin", email: "odin@email.com", password: "foobar", created_at: "2014-05-03 18:02:23", updated_at: "2014-05-03 18:02:23">
2.0.0-p451 :068 > c = u.comments.last
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = ? ORDER BY "comments"."id" DESC LIMIT 1 [["user_id", 5]]
=> #<Comment id: 4, body: "I enjoyed your post!", user_id: 5, post_id: 4, created_at: "2014-05-04 21:08:34", updated_at: "2014-05-04 21:08:34">
2.0.0-p451 :069 > c.user
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 5]]
=> #<User id: 5, username: "Odin", email: "odin@email.com", password: "foobar", created_at: "2014-05-03 18:02:23", updated_at: "2014-05-03 18:02:23">
2.0.0-p451 :070 > p = Post.first
Post Load (0.3ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1
=> #<Post id: 4, title: "A New Post", body: "A post by Odin himself.", created_at: "2014-05-03 18:03:16", updated_at: "2014-05-03 18:03:16", user_id: 5>
2.0.0-p451 :071 > p.comments.first
Comment Load (0.3ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."id" ASC LIMIT 1 [["post_id", 4]]
=> #<Comment id: 3, body: "another comment", user_id: 5, post_id: 4, created_at: "2014-05-04 20:43:47", updated_at: "2014-05-04 20:43:47">
2.0.0-p451 :072 > c.post
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT 1 [["id", 4]]
=> #<Post id: 4, title: "A New Post", body: "A post by Odin himself.", created_at: "2014-05-03 18:03:16", updated_at: "2014-05-03 18:03:16", user_id: 5>