-
Notifications
You must be signed in to change notification settings - Fork 0
Chapter 6
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
...
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.5.1 :003 > user.class.superclass.superclass
=> ActiveRecord::Base
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.5.1 :004 > foo.created_at.class
=> ActiveSupport::TimeWithZone
2.5.1 :005 > foo.updated_at.class
=> ActiveSupport::TimeWithZone
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
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.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
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.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
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.5.1 :004 > u.errors.messages.class
=> Hash
2.5.1 :005 > u.errors.messages[:email]
=> ["can't be blank"]
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.5.1 :006 > user.errors.messages
=> {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}
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.
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.
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
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.5.1 :003 > user.errors.messages
=> {:password=>["can't be blank"]}
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.5.1 :003 > user.errors.messages
=> {:password=>["is too short (minimum is 6 characters)"]}
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.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.
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"