This article deepens my understanding by writing a Rails tutorial commentary article to further solidify my knowledge It is part of my study. In rare cases, ridiculous content or incorrect content may be written. Please note. I would appreciate it if you could tell me implicitly ...
Source Rails Tutorial 6th Edition
Create a user model. We will continue to create user models in the following chapters.
User data that almost certainly exists in a web application ID, name, email address, password, address, etc ... Create from the data structure of the database that is saved when these data are created. This data structure is called Model The library that interacts with the database is called ActiveRecord in Rails. With ActiveRecord, you don't usually need to be aware of the SQL required to operate the database. Rails also uses a feature called migration to separate SQL from database creation. Thanks to that, Rails alone can even operate the database.
Let's create a User model at once. What I want to remember here Controller name: Plural → Users, Microposts, etc. Model name: singular → User, Micropost, etc. Name it like this.
rails g model User name:string email:string
This command creates a User model with character columns name and email.
By the way, if you create a model with the generate command
It is very convenient because it also generates migration files, test files, and fixtures at the same time.
This time, I defined two attributes, name and email, but Rails automatically adds three columns.
Here is it
|users||
|:--|:--|
|id|integer|
|name|string|
|email|string|
|created_at|datetime|
|updated_at|datetime|
From Rails Tutorial 6th Edition
https://railstutorial.jp/chapters/modeling_users?version=6.0#fig-user_model_initial
id is a unique value that uniquely identifies the record as it is
created_at is the time the record was created
updated_at represents the time when the data of the record was updated.
Since the migration file was generated at the same time as the model with the generate command earlier
I will migrate immediately.
#### **`migrate`**
```rails db
##### Exercise
1. Compare schema and migration
create_table "users", force: :cascade do |t| t.string "name" t.string "email" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
Neither has a line that defines the id, but timestamps are finely defined in the schema.
The feeling is that the execution result of the database designed by migration is a schema.
2. With `` `rails db: rollback```
== 20200608105854 CreateUsers: reverting ====================================== -- drop_table(:users) -> 0.0047s == 20200608105854 CreateUsers: reverted (0.0125s) =============================
The table has been dropped with the drop_table command.
3. `` `rails db: migrate``` will create the table as before. (Results omitted)
#### model file
Let's check the class inheritance structure of the User model as a review.
$ rails c Running via Spring preloader in process 7640 Loading development environment (Rails 6.0.3)
User.superclass => ApplicationRecord(abstract)
User.superclass.superclass => ActiveRecord::Base
User < ApplicationRecord < ActiveRecord::Base
With this structure, the User model inherits all the features of ActiveRecord :: Base.
Therefore, the User database can also be manipulated using ActiveRecord methods.
##### Exercise
1. It is almost the same as what I did in ↑.
user = User.new (0.4ms) SELECT sqlite_version(*) => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
user.class.superclass => ApplicationRecord(abstract)
2. This is exactly the same as what you are doing in ↑, so omit it.
#### Create a user object
Try to create a user object.
If you don't want to change the database (when trying like this), use sandbox mode.
```rails console --sandbox```Or```rails c -s```
This mode rolls back (undoes) all changes when you exit the console, which is convenient for trials.
The main points here are summarized.
-User object can be created with User.new.
-User object created by User.new can be saved by User.save. (If you do not save, it will not be stored in the database)
-You can tell whether the created User object user is valid by the logical value returned by user.valid ?.
-Created_at and updated_at are nil when the User object is created with the new method, but the time when saved
Is substituted.
-If you use User.create, you can create and save objects at the same time.
##### Exercise
1. Note that if you do not put anything in name and email, it will be nil and it will be NilClass.
user.email.class => String
user.name.class => String
2.ActiveSupport::TimeWithZone
user.created_at.class => ActiveSupport::TimeWithZone
user.updated_at.class => ActiveSupport::TimeWithZone
#### Search for user objects
Summarize the points.
-Return the corresponding user in User.find (user id).
-Return the corresponding user in User.find_by (attribute: search value).
· User.first returns the first user in the database
-All records registered in the database in User.all are formatted like an array
Return as (ActiveRecord_Relation).
##### Exercise
1. find_by_name works the same, but uses the usage of passing a hash to the argument of basic find_by.
user = User.find_by(name: "take") User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "take"], ["LIMIT", 1]] => #<User id: 2, name: "take", email: "[email protected]", created_at: "2020-06-08 12:38:02", updated_at: "2020-06-08 12:38:02">
user = User.find_by_name("take") User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "take"], ["LIMIT", 1]] => #<User id: 2, name: "take", email: "[email protected]", created_at: "2020-06-08 12:38:02", updated_at: "2020-06-08 12:38:02">
user = User.find_by_email("[email protected]")
2.
user = User.all User Load (0.2ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]] => #<ActiveRecord::Relation [#<User id: 1, name: nil, email: nil, created_at: "2020-06-08 12:32:30", updated_at: "2020-06-08 12:32:30">, #<User id: 2, name: "take", email: "[email protected]", created_at: "2020-06-08 12:38:02", updated_at: "2020-06-08 12:38:02">]>
user.class => User::ActiveRecord_Relation
3. I want to know the number of elements (number) of User → It seems to be counted by length. → You can count if you connect them with a method chain.
It is called duck typing that you can understand how to handle such a class that you do not know in detail.
User.all.length User Load (0.2ms) SELECT "users".* FROM "users" => 2
#### Update user object
There are two ways to update the contents of a user object
・ Directly substitute and save
user.name = "takemo" => "takemo"
user.save
・ Immediate update with update method
user.update(name: "take",email: "[email protected]")
When updating only one attribute, pass the attribute name and value instead of hash like ```update_attribute (: name," take ")` `.
To update more than one attribute, pass a hash like ```update (name:" take ", email:" [email protected] ")` ``.
By the way, both update one attribute like ```update_attribute (: name, "take") `` `and ```update (name:" take ")` ``
Writing without using a hash as an argument (the former) also has the effect of avoiding "verification" of the data that will be learned from now on **. ** **
##### Exercise
1.
user.name = "Saori Yoshida" => "Saori Yoshida"
user.save (0.1ms) SAVEPOINT active_record_1 User Update (0.1ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "Saori Yoshida"], ["updated_at", "2020-06-08 13:50:14.777986"], ["id", 2]] (0.1ms) RELEASE SAVEPOINT active_record_1 => true
2.
user.update_attribute(:email,"[email protected]") (0.1ms) SAVEPOINT active_record_1 User Update (0.1ms) UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ? [["email", "[email protected]"], ["updated_at", "2020-06-08 13:56:20.532390"], ["id", 2]] (0.1ms) RELEASE SAVEPOINT active_record_1 => true
3.
user.update_attribute(:created_at,1.year.ago) (0.1ms) SAVEPOINT active_record_1 User Update (0.2ms) UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ? [["created_at", "2019-06-08 13:57:32.050811"], ["updated_at", "2020-06-08 13:57:32.051252"], ["id", 2]] (0.1ms) RELEASE SAVEPOINT active_record_1 => true
### Validate the user
The word "verification" has appeared several times in the previous exercises.
It's a data limit. An unnamed user, a malformed email address, or a password that is too short
It must not be registered in the database.
Validation is what prevents this.
#### Verify effectiveness
This part will proceed with test-driven development
The first user model test looks like this
require 'test_helper'
class UserTest < ActiveSupport::TestCase def setup @user = User.new(name: "Example User", email: "[email protected]") end
test "should be valid" do assert @user.valid? end end
Here is the setup method that is executed before each test (only one at the moment)
Define a test user in the @user variable and
The should be valid test tests that @user is valid data.
At this stage, no validation has been added and any data is valid, so the test will definitely pass.
#### **`models`**
```rails test
Run only the tests related to the model in
But anyway, it is automatically tested by guard, and even when running manually, all the tests are running.
I have never used it ...
It may be used when the test itself increases and becomes heavy.
##### Exercise
1.
new_user = User.new => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
new_user.valid? => true
2.
user.valid? => true
It is obvious, but it would be kind of data because there is no validation is enabled.
#### Verify existence
Try adding the simplest and most basic validation, presense
By adding this, it can be enabled only when the element exists.
Let's write the test first.
test "name should be present" do @user.name = " " assert_not @user.valid? end
In this test added this time, put a blank (does not exist) in the name attribute
@ user.valid? With assert_not (passes when the argument is false)
Passes the test when user is not valid.
In other words
Put a space and test that user is not valid if name does not exist.
Because the test fails at this stage
I decided to add validation and pass the test.
class User < ApplicationRecord validates :name, presence: true end
I think that there are quite a lot of people who get into writing here,
If you write this line without omitting it
#### **`validates(:name,{presence: true})`**
Passing: name as the first argument and hash as the second argument (optional) to the validates method. Rails provides various abbreviations for efficiency. Here the () of the method can be omitted, the last hash of the method argument can omit {}, It uses two abbreviations. Since such abbreviations will continue to appear as a matter of course in the future I will explain as much as possible, but it is good to get used to it now.
I added validation Invalid if name does not exist.
Check if the validation is working on the console.
>> user = User.new(name: "",email:"[email protected]")
(0.1ms) begin transaction
=> #<User id: nil, name: "", email: "[email protected]", created_at: nil, updated_at: nil>
>> user.valid?
=> false
By the way, it cannot be saved in the database in the disabled state.
>> user.save
=> false
Failure in this way creates an error object and saves the error message.
>> user.errors.full_messages
=> ["Name can't be blank"]
When I exit the console and run the test, the test passes. Write a test for the email attribute and then add validation. (Test Driven Development)
Testing the existence of email
test "email should be present" do
@user.email = " "
assert_not @user.valid?
end
The test fails here. Add email validation
validates :email, presence: true
The test passes.
##### Exercise
1. I'm stuck with validation of the existence of name and email
u = User.new (0.1ms) begin transaction => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
u.valid? => false
u.errors.full_messages => ["Name can't be blank", "Email can't be blank"]
2. Since messages are in hash format, you can retrieve them by specifying [: email].
u.errors.messages => {:name=>["can't be blank"], :email=>["can't be blank"]}
u.errors.messages[:email] => ["can't be blank"]
#### Verify length
For the time being, the name is limited to 50 characters, which is a reasonable number of characters.
E-mail addresses will also be limited to 255 characters, which is the standard character limit.
Write a test before adding validation.
test "name should not be long" do @user.name = "a" * 51 assert_not @user.valid? end
test "email should not be too long " do @user.email = "a" * 244 + "@example.com" assert_not @user.valid? end
I put a lot of a to earn the number of characters.
The test does not pass at this point.
validates :name, presence: true, length:{maximum: 50} validates :email, presence: true, length:{maximum: 255}
This will validate the number of characters and pass the test.
##### Exercise
1.
user = User.new(name:"a"*51,email:"a"*256) (0.1ms) begin transaction => #<User id: nil, name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", email: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...", created_at: nil, updated_at: nil>
user.valid? => false
2. I get angry when is too long.
user.errors.messages => {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}
##### Validate the format
If you enter appropriate characters in the email address when creating a new account, which is often seen on various sites
I often get angry when I say "Please enter the correct email address".
In this way, we will verify whether or not the specified format as an email address is satisfied in addition to the number of characters.
You can easily create an array of strings by using% w [], so use this to create an invalid email address list.
A test to make sure that a valid email address is not caught in validation
test "email validation should accept valid address" do valid_addresses = %w[[email protected] [email protected] [email protected]] valid_addresses.each do |valid_address| @user.email = valid_address assert @user.valid? , "#{valid_address.inspect} should be valid" end end
An error message is added with the second argument so that you can see which email address the test failed.
Next is a test to make sure that invalid email addresses are firmly caught in validation
test "email validation should reject invalid address" do invalid_addresses = %w[user@example,com user_at_foo.org [email protected]@bar_baz.com foo@bar+baz.com] invalid_addresses.each do |invalid_address| @user.email = invalid_address assert_not @user.valid?, "#{invalid_address.inspect} should be invalid" end end
The test does not pass at this point.
To pass the test
Add validation of email address.
You must write a regular expression to verify the format of your email address.
There was a site that was very easy to understand, so I will introduce it.
https://murashun.jp/blog/20190215-01.html
Of course, the Rubular introduced in the tutorial is also useful.
Define the regular expression of Email. In Ruby, constants start with an uppercase letter.
#### **`user.rb`**
```rb
class User < ApplicationRecord
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :name, presence: true, length:{maximum: 50}
validates :email, presence: true, length:{maximum: 255},
format: {with: VALID_EMAIL_REGEX}
end
Supplement because there was no explanation on the tutorial. The [\ w + -.] Part represents alphanumeric characters, underscores, pluses, hyphens, and dots. \ w corresponds to alphanumeric characters and underscore +-. Corresponds to each, but I feel that there is one extra . This is the effect of escaping the character immediately after, and the hyphen immediately after has a special meaning in []. It has the effect of specifying a range [a-z](a to z). So to escape and display as the original- Since it is escaped with , it becomes such an expression.
At this point the test passes.
This regular expression doesn't pop a series of dots like [email protected], so I'll fix it in the next exercise.
1.
Match the correct one like this.
/\A[\w+\-.]+@[a-z\d\-.]+(\.[a-z\d\-.]+)*\.[a-z]+\z/i
This is ``` (\. [A-z \ d \-.] +)` `` and adds another pattern with .〇〇〇.
Approximately the same as 3.1
abridgement
#### Verify uniqueness
E-mail addresses are used as user names (such as when logging in) and must not be duplicated between users.
Here, unique validation is added.
With User.new, duplication does not occur even if you query the database just by creating an object in memory.
In other words, put data A in the database and test the exact same data B.
test "email address should be unique" do duplicate_user = @user.dup @user.save assert_not duplicate_user.valid? end
Fix here
Email addresses are usually case insensitive
[email protected] and [email protected] are treated the same.
To test this at the same time
test "email address should be unique" do duplicate_user = @user.dup duplicate_user.email.upcase! @user.save assert_not duplicate_user.valid? end
Upcase the copied addresses and compare.
For validation, just specify case_sensitive: false in the uniqueness option.
The validation is that the email address must be unique but not case sensitive (both are the same).
But there are still flaws.
Because the validation we are adding is verified at the stage of creating the data before registering it in the database.
In the unlikely event that you pass the verification when there is a lot of traffic and heavy traffic when sending registration requests in succession
It will be registered in the database as it is.
To prevent this, you can force it to be unique not only when creating data but also when registering data.
This is achieved by adding an index to the email column of the database and forcing the index to be unique.
* An index is like a database index, and adding it will improve the data search speed.
I won't go into detail here, but be aware that it doesn't mean that you should add an index to all the data.
Create a migration file to add an index
```$ rails g migration add_index_to_users_email```
Whereas migrating and adding an index forced uniqueness in the database email index
Since the email attribute of fixture, which is the sample data of the test DB, is covered, an error is thrown.
This fixture is not used at this time either, so delete the contents
Until now, the test passed even though the fixture value was incorrect before registering in the database.
Because it was a test for data verification (fixture is data registered in the test DB in advance)
But this is still not enough.
Depending on the database, the index may or may not be case sensitive.
In the validation of the user model, uniqueness: {case_sensitive: false} could be used.
In such a case, a method is used to convert all data to lowercase before registering in the database.
Use ActiveRecord's before_save method to specify that all data should be converted to lowercase before saving.
#### **` before_save { self.email.downcase! }`**
This will call a callback on save and all emails will be converted to lowercase. Therefore, ```uniqueness: {case_sensitive: false} `` `also
It's OK to set it back to true
This is not a problem because it is always in lowercase when saved.
#### **`If you set it back to true, you will get an error in the test.`**
```uniqueness
Because validation has returned to treat case as different.
Therefore, the test also restores the capitalized part.
test "email address should be unique" do duplicate_user = @user.dup @user.save assert_not duplicate_user.valid? end
##### Exercise
1. Put only the result when commented out for the time being.
FAIL["test_email_address_should_be_saved_as_lower-case", #<Minitest::Reporters::Suite:0x00007f75a0e7b310 @name="UserTest">, 0.06644777600013185] test_email_address_should_be_saved_as_lower-case#UserTest (0.07s) Expected: "[email protected]" Actual: "[email protected]" test/models/user_test.rb:58:in `block in class:UserTest'
9/9: [=====================================================] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.07422s 9 tests, 15 assertions, 1 failures, 0 errors, 0 skips
It is easy to understand because there are errors in the form of expected value and actual value.
Since the callback will be called by uncommenting and all lowercase letters will be converted before saving.
The test also passes.
2. Omitted because it was originally written in ```email.downcase! `` `.
To explain it, adding! Makes it a "destructive method". In this case, the email is downcased and the result is "overwritten".
### Add a secure password
Implement the user password.
Instead of just registering the password in the database, register the hashed one with the hash function.
Required because the risk is considerably lower than registering as it is.
#### Hashed password
There are convenient methods for implementing passwords
```has_secure_password```80% is finished just by adding.
By calling this method in the model
-The hashed password can be saved in the password_digest attribute. "
・ As soon as the virtual attributes password and password_confirmation become available
Validation is also added to see if the existence and the values of these two attributes match.
-The ```authenticate``` method can be used.
Of course there are other preparations, password_digest is obvious but must be included in the model.
Therefore, create a migration file for adding password_digest.
#### **`string`**
```rails g migration add_password_digest_to_users password_digest
Rails will interpret it as a change to the users table by ending the migration file with to_users.
If you also write password_digest: string after the file name, it will add the string type password_digest to the users table.
It will automatically create a migration. Very convenient.
As mentioned above, the migration file does not need to be changed, so let's migrate it as it is.
In addition, bcrypt, which is a state-of-the-art hash function, is required for password hashing.
Add bcrypt to your Gemfile to use bcrypt.
#### **`gem 'bcrypt', '3.1.13'`**
After adding, hit bundle.
Now that we are ready to use has_secure_password, add `` `has_secure_password``` to the user model. Validation of whether the existence and value of the virtual attributes password and password_confirmation are the same is stuck. Pass by passing the values of these two attributes to the @user attribute of the test.
>> user1 = User.new(name:"takemo",email:"[email protected]")
=> #<User id: nil, name: "takemo", email: "[email protected]", created_at: nil, updated_at: nil, password_digest: nil>
>> user1.valid?
User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]]
=> false
>> user1.errors.full_messages
=> ["Password can't be blank"]
Validate the minimum number of characters (6 characters) for the password. First, write the test first. Write a test to throw an error when the password is blank or less than 6 characters.
test "password should be present (nonblank)" do
@user.password = @user.password_confirmation = " " * 6
assert_not @user.valid?
end
test "password should have a minimum length" do
@user.password = @user.password_confirmation = "a" * 5
assert_not @user.valid?
end
`` `@ User.password =" @ user.password_confirmation = "" * 6``` Uses multiple assignments to assign to two elements at the same time.
Now that we've written the test, we'll implement validation. Since it is the minimum number of characters, use the minimum option, which is the opposite of maximum.
validates :password, presence: true, length:{minimum: 6}
The test passes when the validation is added.
##### Exercise
1.
user1 = User.new(name:"takemo",email:"[email protected]",password:"foo",password_confirmation:"foo") (0.1ms) begin transaction => #<User id: nil, name: "takemo", email: "[email protected]", created_at: nil, updated_at: nil, password_digest: [FILTERED]>
user1.valid? User Exists? (0.6ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]] => false
2.
user1.errors.messages => {:password=>["is too short (minimum is 6 characters)"]}
#### User creation and authentication
Create a new user in the development environment in preparation for future index page creation.
Since the user creation function from the web has not been implemented yet, add it using the rails console.
User.create(name:"take",email:"[email protected]",password:"foobar",password_confirmation:"foobar") (0.4ms) SELECT sqlite_version(*) (0.1ms) begin transaction User Exists? (0.8ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]] User Create (3.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "take"], ["email", "[email protected]"], ["created_at", "2020-06-10 12:51:23.959140"], ["updated_at", "2020-06-10 12:51:23.959140"], ["password_digest", "$2a
12 xkZDNGfs2Dnjzbm5XxfCfu.sB3I4ug4PFtTHukKdp0EF9YLLsE5Sm"]] (6.7ms) commit transaction => #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-10 12:51:23", updated_at: "2020-06-10 12:51:23", password_digest: [FILTERED]>
If you look in password_digest, the hashed password is stored.
user.password_digest => "$2a
12 xkZDNGfs2Dnjzbm5XxfCfu.sB3I4ug4PFtTHukKdp0EF9YLLsE5Sm"
Also, as explained earlier, the `` `authenticate``` method is enabled by the `` `has_secure_password``` method.
I will try it.
user.authenticate("foo!baka") => false
It returns false for incorrect passwords.
user.authenticate("foobar") => #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-10 12:51:23", updated_at: "2020-06-10 12:51:23", password_digest: [FILTERED]>
Returns a user object given the correct password. (true)
You can also use !! to convert it to a logical value and return it → `` `!! user.authenticate ("foobar ")` `
##### Exercise
1.
u = User.find_by(email:"[email protected]") (0.4ms) SELECT sqlite_version() User Load (0.2ms) SELECT "users". FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "[email protected]"], ["LIMIT", 1]] => #<User id: 1, name: "take", email: "[email protected]", created_at: "2020-06-10 12:51:23", updated_at: "2020-06-10 12:51:23", password_digest: [FILTERED]>
2. The virtual attribute password is empty, so it gets stuck in validation.
The previously entered password is hashed and saved in the DB as password_digest, so you need to enter the password every time.
u.name = "taketake" => "taketake"
u.save (0.1ms) begin transaction User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ? [["email", "[email protected]"], ["id", 1], ["LIMIT", 1]] (0.1ms) rollback transaction => false
3. You can use the update_attribute method to bypass validation and update the value directly.
u.update_attribute(:name,"taketake") (0.1ms) begin transaction User Update (2.1ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "taketake"], ["updated_at", "2020-06-10 13:09:09.579353"], ["id", 1]] (6.4ms) commit transaction => true
### Finally
Commit the changes so far to Git
Push and deploy.
Don't forget to migrate your production database.
[To the previous chapter](https://qiita.com/take_webengineer/items/6949dc3131d4f34e27c0)
[To the next chapter]()
Recommended Posts