Skip to content

Chapter 6

Jonadabe Souza edited this page Oct 21, 2018 · 8 revisions

Section 6.1.1

1) Rails uses a file called schema.rb in the db/ directory to keep track of the structure of the database (called the schema, hence the filename). Examine your local copy of db/schema.rb and compare its contents to the migration code in Listing 6.2.

It's quite similar, but the definition of timestamps are splitted, and the column names are strings instead of symbols like in the migration file.

2) Most migrations (including all the ones in this tutorial) are reversible, which means we can “migrate down” and undo them with a single command, called db:rollback:

$ rails db:rollback

After running this command, examine db/schema.rb to confirm that the rollback was successful. (See Box 3.1 for another technique useful for reversing migrations.) Under the hood, this command executes the drop_table command to remove the users table from the database. The reason this works is that the change method knows that drop_table is the inverse of create_table, which means that the rollback migration can be easily inferred. In the case of an irreversible migration, such as one to remove a database column, it is necessary to define separate up and down methods in place of the single change method. Read about migrations in the Rails Guides for more information.

rails db:rollback

== 20180929114159 CreateUsers: reverting ======================================
-- drop_table(:users)
   -> 0.0008s
== 20180929114159 CreateUsers: reverted (0.0040s) =============================

It's empty, there is no more command to create the users table.

3) Re-run the migration by executing rails db:migrate again. Confirm that the contents of db/schema.rb have been restored.

rails db:migrate

== 20180929114159 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0010s
== 20180929114159 CreateUsers: migrated (0.0011s) =============================

db/schema.rb

  ...

  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
  
  ...

Section 6.1.2

1) In a Rails console, use the technique from Section 4.4.4 to confirm that User.new is of class User and inherits from ApplicationRecord.

rails console

2.5.1 :001 > user = User.new
 => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> 
2.5.1 :002 > user.class.superclass
 => ApplicationRecord(abstract)
2) Confirm that ApplicationRecord inherits from ActiveRecord::Base.
2.5.1 :003 > user.class.superclass.superclass
 => ActiveRecord::Base 

Section 6.1.3

1) Confirm that user.name and user.email are of class String.
2.5.1 :001 > foo = User.create(name: "Foo", email: "[email protected]")
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "Foo"], ["email", "[email protected]"], ["created_at", "2018-10-09 22:59:22.715432"], ["updated_at", "2018-10-09 22:59:22.715432"]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
 => #<User id: 1, name: "Foo", email: "[email protected]", created_at: "2018-10-09 22:59:22", updated_at: "2018-10-09 22:59:22"> 
2.5.1 :002 > foo.name.class
 => String 
2.5.1 :003 > foo.email.class
 => String 
2) Of what class are the created_at and updated_at attributes?
2.5.1 :004 > foo.created_at.class
 => ActiveSupport::TimeWithZone 
2.5.1 :005 > foo.updated_at.class
 => ActiveSupport::TimeWithZone 

Section 6.1.4

1) Find the user by name. Confirm that find_by_name works as well. (You will often encounter this older style of find_by in legacy Rails applications.)
2.5.1 :001 > user = User.find_by(name: 'Bar')                                                            │ jhonndabi@dell  ~/Code/ruby/railstutorial-book/dw5-sample-app.wiki   master  
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Bar"], [│
"LIMIT", 1]]                                                                                             │
 => #<User id: 2, name: "Bar", email: "[email protected]", created_at: "2018-10-09 23:09:15", updated_at: "2018│
-10-09 23:09:15">                                                                                        │
2.5.1 :002 > user = User.find_by_name 'Bar'                                                              │
  User Load (0.5ms)  SELECT  "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Bar"], [│
"LIMIT", 1]]                                                                                             │
 => #<User id: 2, name: "Bar", email: "[email protected]", created_at: "2018-10-09 23:09:15", updated_at: "2018│
-10-09 23:09:15"> 
2) For most practical purposes, User.all acts like an array, but confirm that in fact it’s of class User::ActiveRecord_Relation.
2.5.1 :003 > users = User.all                                                                            │
  User Load (0.7ms)  SELECT  "users".* FROM "users" LIMIT ?  [["LIMIT", 11]]                             │
 => #<ActiveRecord::Relation [#<User id: 1, name: "Foo", email: "[email protected]", created_at: "2018-10-09 23│
:08:53", updated_at: "2018-10-09 23:08:53">, #<User id: 2, name: "Bar", email: "[email protected]", created_at:│
 "2018-10-09 23:09:15", updated_at: "2018-10-09 23:09:15">]>                                             │
2.5.1 :004 > users.class                                                                                 │
 => User::ActiveRecord_Relation
3) Confirm that you can find the length of User.all by passing it the length method (Section 4.2.3). Ruby’s ability to manipulate objects based on how they act rather than on their formal class type is called duck typing, based on the aphorism that “If it looks like a duck, and it quacks like a duck, it’s probably a duck.”
2.5.1 :005 > users.length                                                                                │
  User Load (0.6ms)  SELECT "users".* FROM "users"                                                       │
 => 2 

Section 6.1.5

1) Update the user’s name using assignment and a call to save.
2.5.1 :001 > user = User.find(1)
  User Load (0.1ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
 => #<User id: 1, name: "El Duderino", email: "[email protected]", created_at: "2018-10-09 23:08:53", updated_at: "2018-10-10 23:28:47">
2.5.1 :002 > user.name = "New Name"
 => "New Name"
2.5.1 :003 > user.save
   (0.3ms)  begin transaction
  SQL (1.1ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "New Name"], ["updated_at", "2018-10-10 23:30:37.359449"], ["id", 1]]
   (13.0ms)  commit transaction
 => true
2) Update the user’s email address using a call to update_attributes.
2.5.1 :004 > user.update_attributes(name: "The Dude", email: "[email protected]")
   (0.3ms)  begin transaction
  SQL (0.9ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "The Dude"], ["updated_at", "2018-10-10 23:31:39.465053"], ["id", 1]]
   (12.8ms)  commit transaction
 => true 
3) Confirm that you can change the magic columns directly by updating the created_at column using assignment and a save. Use the value 1.year.ago, which is a Rails way to create a timestamp one year before the present time.
2.5.1 :005 > user.created_at = 1.year.ago
 => Tue, 10 Oct 2017 23:32:47 UTC +00:00 
2.5.1 :006 > user.save
   (0.2ms)  begin transaction
  SQL (0.9ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2017-10-10 23:32:47.262960"], ["updated_at", "2018-10-10 23:32:52.117031"], ["id", 1]]
   (12.8ms)  commit transaction
 => true 

Section 6.2.1

1) In the console, confirm that a new user is currently valid.
2.5.1 :001 > user = User.new
 => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> 
2.5.1 :002 > user.valid?
 => true 
2) Confirm that the user created in Section 6.1.3 is also valid.
2.5.1 :003 > user = User.new(name: "Michael Hartl", email: "[email protected]")
 => #<User id: nil, name: "Michael Hartl", email: "[email protected]", created_at: nil, updated_at: nil> 
2.5.1 :004 > user.valid?
 => true

Section 6.2.2

1) Make a new user called u and confirm that it’s initially invalid. What are the full error messages?
2.5.1 :001 > u = User.new
 => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> 
2.5.1 :002 > u.valid?
 => false 
2.5.1 :003 > u.errors.messages
 => {:name=>["can't be blank"], :email=>["can't be blank"]} 

2) Confirm that u.errors.messages is a hash of errors. How would you access just the email errors?
2.5.1 :004 > u.errors.messages.class
 => Hash 
2.5.1 :005 > u.errors.messages[:email]
 => ["can't be blank"]

Section 6.2.3

1) Make a new user with too-long name and email and confirm that it’s not valid.
2.5.1 :001 > user = User.new(name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", email: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2.5.1 :002"> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2.5.1 :003"> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
2.5.1 :004"> [email protected]")
 => #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", created_at: nil, updated_at: nil> 
2.5.1 :005 > user.valid?
 => false 
2) What are the error messages generated by the length validation?
2.5.1 :006 > user.errors.messages
 => {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]} 

Section 6.2.4

1) By pasting in the valid addresses from Listing 6.18 and invalid addresses from Listing 6.19 into the test string area at Rubular, confirm that the regex from Listing 6.21 matches all of the valid addresses and none of the invalid ones.

Rubular Email Validation

2) As noted above, the email regex in Listing 6.21 allows invalid email addresses with consecutive dots in the domain name, i.e., addresses of the form [email protected]. Add this address to the list of invalid addresses in Listing 6.19 to get a failing test, and then use the more complicated regex shown in Listing 6.23 to get the test to pass.

test/models/user_test.rb

    ...
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
                           foo@bar_baz.com foo@bar+baz.com [email protected]]
    ...

rails test

 FAIL["test_email_validation_should_reject_invalid_addresses", UserTest, 0.2902890870027477]
 test_email_validation_should_reject_invalid_addresses#UserTest (0.29s)
        "[email protected]" should be invalid
        test/models/user_test.rb:46:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:44:in `each'
        test/models/user_test.rb:44:in `block in <class:UserTest>'

  17/17: [====================================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.29819s
17 tests, 35 assertions, 1 failures, 0 errors, 0 skips

app/models/user.rb

  ...
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  ...

rails test

  17/17: [====================================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.28683s
17 tests, 35 assertions, 0 failures, 0 errors, 0 skips
3) Add [email protected] to the list of addresses at Rubular, and confirm that the regex shown in Listing 6.23 matches all the valid addresses and none of the invalid ones.

Rubular Email Validation

Section 6.2.5

1) Add a test for the email downcasing from Listing 6.32, as shown in Listing 6.33. This test uses the reload method for reloading a value from the database and the assert_equal method for testing equality. To verify that Listing 6.33 tests the right thing, comment out the before_save line to get to red, then uncomment it to get to green.

app/models/user.rb

  ...
  # before_save { self.email = email.downcase }
  ...

rails tests

 FAIL["test_email_addresses_should_be_saved_as_lower-case", UserTest, 0.30243509099818766]
 test_email_addresses_should_be_saved_as_lower-case#UserTest (0.30s)
        Expected: "[email protected]"
          Actual: "[email protected]"
        test/models/user_test.rb:61:in `block in <class:UserTest>'

  19/19: [====================================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.30419s
19 tests, 37 assertions, 1 failures, 0 errors, 0 skips

app/models/user.rb

  ...
  before_save { self.email = email.downcase }
  ...

rails tests

  19/19: [====================================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.31910s
19 tests, 37 assertions, 0 failures, 0 errors, 0 skips
2) By running the test suite, verify that the before_save callback can be written using the “bang” method email.downcase! to modify the email attribute directly, as shown in Listing 6.34.

app/models/user.rb

  ...
  before_save { email.downcase! }
  ...

rails tests

  19/19: [====================================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.30759s
19 tests, 37 assertions, 0 failures, 0 errors, 0 skips

Section 6.3.2

1) Confirm that a user with valid name and email still isn’t valid overall.
2.5.1 :001 > user = User.new(name: "Example User", email: "[email protected]")
 => #<User id: nil, name: "Example User", email: "[email protected]", created_at: nil, updated_at: nil, password_digest: nil> 
2.5.1 :002 > user.valid?
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
 => false 
2) What are the error messages for a user with no password?
2.5.1 :003 > user.errors.messages
 => {:password=>["can't be blank"]} 

Section 6.3.3

1) Confirm that a user with valid name and email but a too-short password isn’t valid.
2.5.1 :001 > user = User.new(name: "Example User", email: "[email protected]", password: "cinco")
 => #<User id: nil, name: "Example User", email: "[email protected]", created_at: nil, updated_at: nil, password_digest: "$2a$10$4fjppYIhlRqhiTxjRE80V.t7K/AYmoWhNOKtQcIyqOP..."> 
2.5.1 :002 > user.valid?
  User Exists (0.3ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
 => false 
2) What are the associated error messages?
2.5.1 :003 > user.errors.messages
 => {:password=>["is too short (minimum is 6 characters)"]}

Section 6.3.4

1) Quit and restart the console, and then find the user created in this section.
2.5.1 :001 > user = User.find_by(email: "[email protected]")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
 => #<User id: 4, name: "Michael Hartl", email: "[email protected]", created_at: "2018-10-21 10:43:56", updated_at: "2018-10-21 10:43:56", password_digest: "$2a$10$V0aZ.nHOZQdJdyBXhggUsOZkAbGQnBfeqCFH.E5w6vr..."> 
2) Try changing the name by assigning a new name and calling save. Why didn’t it work?
2.5.1 :002 > user.name = "new name"
 => "new name" 
2.5.1 :003 > user.save
   (0.3ms)  begin transaction
  User Exists (0.7ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "[email protected]"], ["id", 4], ["LIMIT", 1]]
   (0.1ms)  rollback transaction
 => false 

Because the attribute password is required and we didn't provide it.

3) Update user’s name to use your name. Hint: The necessary technique is covered in Section 6.1.5.
2.5.1 :004 > user.update_attribute(:name, "New Name")
   (0.2ms)  begin transaction
  SQL (0.3ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "New Name"], ["updated_at", "2018-10-21 10:54:20.723899"], ["id", 4]]
   (11.8ms)  commit transaction
 => true 
2.5.1 :005 > user.name
 => "New Name"
Clone this wiki locally