Rodhos Soft

備忘録を兼ねた技術的なメモです。Rofhos SoftではiOSアプリ開発を中心としてAndroid, Webサービス等の開発を承っております。まずはご相談下さい。

チュートリアルをやってみる。ユーザモデル編

これはチュートリアルをやってみたログである。
第6章 ユーザーのモデルを作成する | Rails チュートリアル

RailsではモデルをActive Recordとして扱い、SQLを意識せずモデリングできる。
例えば開発環境でSQLite、本番環境のHerokuでPostgreSQLといった使い方ができる。

モデルの作成

nameとemailの属性があるユーザモデルを作る。

rails generate model User name:string email:string

モデル名には単数形(コントローラには複数形)をつける。

マイグレーションファイルが生成された。
db/migrate/20150914082003_create_users.rb

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end

つまり、usersテーブル(複数形になっている)が生成され、name,emailというカラム、定義されている。
t.timestampsでcreated_atとupdate_atというdatetime属性のカラムができる。

マイグレーションの適用

マイグレーションを実行する。

bundle exec rake db:migrate

db/development.sqlite3が生成されているのでsqlitebrowser等で確認する。

マイグレーションは可逆なのでロールバックもできる。

bundle exec rake db:rollback

モデルファイルを見てみる。

app/models/user.rb

class User < ActiveRecord::Base
end

ActiveRecordの継承となっていることを確認した。

ユーザオブジェクトをコンソールで触ってみる。

コンソールをサンドボックスモードで起動

rails console --sandbox

ユーザオブジェクトを作成してみる

User.new
 => #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil> 
user = User.new(name: "hoge fuga", email: "hoge@hoge.com")
 => #<User id: nil, name: "hoge fuga", email: "hoge@hoge.com", created_at: nil, updated_at: nil> 

セーブする。

user.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (69.3ms)  INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Mon, 14 Sep 2015 08:38:39 UTC +00:00], ["email", "hoge@hoge.com"], ["name", "hoge fuga"], ["updated_at", Mon, 14 Sep 2015 08:38:39 UTC +00:00]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
 => true 

SQLでINSERTされた。

 user
 => #<User id: 1, name: "hoge fuga", email: "hoge@hoge.com", created_at: "2015-09-14 08:38:39", updated_at: "2015-09-14 08:38:39"> 

idが1になった。日時なども記録されている。

user.name
 => "hoge fuga" 

ドット記法でアクセスできる。

生成と保存をcreateで一度にやる。
User.create(name:"poi", email:"poi@poi.com")
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.8ms)  INSERT INTO "users" ("created_at", "email", "name", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", Mon, 14 Sep 2015 08:41:43 UTC +00:00], ["email", "poi@poi.com"], ["name", "poi"], ["updated_at", Mon, 14 Sep 2015 08:41:43 UTC +00:00]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
 => #<User id: 2, name: "poi", email: "poi@poi.com", created_at: "2015-09-14 08:41:43", updated_at: "2015-09-14 08:41:43"> 
createの反対 destroy
 user = User.create(name:"poge", email:"poge@poge.com")

とcreateしておいて、

 user.destroy
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.1ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 3]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
 => #<User id: 3, name: "poge", email: "poge@poge.com", created_at: "2015-09-14 08:43:20", updated_at: "2015-09-14 08:43:20"> 
ユーザオブジェクトを検索してみる。
 User.find(1)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 1]]
 => #<User id: 1, name: "hoge fuga", email: "hoge@hoge.com", created_at: "2015-09-14 08:38:39", updated_at: "2015-09-14 08:38:39"> 

先ほど消したid=3は

User.find(3)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 3]]
ActiveRecord::RecordNotFound: Couldn't find User with id=3

例外が投げられる。

findで検索

User.find_by_email("hoge@hoge.com")
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."email" = 'hoge@hoge.com' LIMIT 1
 => #<User id: 1, name: "hoge fuga", email: "hoge@hoge.com", created_at: "2015-09-14 08:38:39", updated_at: "2015-09-14 08:38:39"> 

シンボルで

User.find_by(email: "hoge@hoge.com")
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."email" = 'hoge@hoge.com' LIMIT 1
 => #<User id: 1, name: "hoge fuga", email: "hoge@hoge.com", created_at: "2015-09-14 08:38:39", updated_at: "2015-09-14 08:38:39"> 

先頭

User.first
  User Load (0.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
 => #<User id: 1, name: "hoge fuga", email: "hoge@hoge.com", created_at: "2015-09-14 08:38:39", updated_at: "2015-09-14 08:38:39"> 

全部

User.all
ユーザオブジェクトを更新してみる。
user = User.first
user.name
 => "hoge fuga" 
 user.email = "chopin@hoge.com"

としてみると

user
 => #<User id: 1, name: "hoge fuga", email: "chopin@hoge.com", created_at: "2015-09-14 08:38:39", updated_at: "2015-09-14 08:38:39"> 

セーブする

 user.save
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = 1  [["email", "chopin@hoge.com"], ["updated_at", Mon, 14 Sep 2015 08:52:15 UTC +00:00]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
 => true 
user
 => #<User id: 1, name: "hoge fuga", email: "chopin@hoge.com", created_at: "2015-09-14 08:38:39", updated_at: "2015-09-14 08:52:15"> 

update_atも更新されていることがわかる。

update_attributesで更新を連続させることもできる(一つ失敗すると失敗になる)

user.update_attributes(name:"The hoge", email:"theHoge@hoge.dom")
   (0.1ms)  SAVEPOINT active_record_1
  SQL (0.2ms)  UPDATE "users" SET "name" = ?, "email" = ?, "updated_at" = ? WHERE "users"."id" = 1  [["name", "The hoge"], ["email", "theHoge@hoge.dom"], ["updated_at", Mon, 14 Sep 2015 08:54:18 UTC +00:00]]
   (0.0ms)  RELEASE SAVEPOINT active_record_1
 => true 

ユーザ検証(validation)

  1. メールアドレスはidのように使いたいので重複させない。一意性(uniquness)
  2. nameは空にさせたくない。存在性(presence)
存在の検証
class User < ActiveRecord::Base
    validates :name, presence: true
end

これは以下と同じ

class User < ActiveRecord::Base
    validates(:name, presence: true)
end

コンソールを立ち上げ

rails console --sandbox

名前を空のユーザを作ってみる。

 user = User.new(name:"", email:"pot@pot.com")

セーブが失敗することを確認する。

 user.save
   (0.1ms)  SAVEPOINT active_record_1
   (0.1ms)  ROLLBACK TO SAVEPOINT active_record_1
 => false 

検証してみる

 user.valid?
 => false 

確かに検証に失敗している。

エラー内容を確認する。

user.errors.full_messages
 => ["Name can't be blank"] 
emailのフォーマットを制限する。
class User < ActiveRecord::Base
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end

正規表現でフォーマットを制限している。

\A 文字列の先頭
[\w+\-.]+ 英数字、アンダースコア、プラス、ハイフン、ドットのいずれかを1文字以上繰り返す
@ アットマーク
[a-z\d\-.]+ 英小文字を1文字以上繰り返す
\z 文字列の末尾
i 大文字、小文字を無視するオプション
emailの一意性(unique)検証
class User < ActiveRecord::Base
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
    uniqueness: true
end

メールアドレスの大文字、小文字を無視した一意性にするなら

class User < ActiveRecord::Base
    validates :name,  presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
    validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
end
一意性の注意点

ユーザ登録時にsubmitを素早く2回クリックすると二つのリクエストが検証にパスしメモリーに作成され、保存できてしまう。
データベースレベルでも一意性を強制するようにすること。
emailカラムにindexを作成する。

 rails generate migration add_index_to_users_email

できたマイグレーションファイル
db/migrate/20150914092358_add_index_to_users_email.rb

class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
  end
end

を変更し

class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end

これで、usersテーブルのemailカラムにindexが追加され、uniqueをtrueとすることで
一意性がデータベースレベルで強制される。
(indexを追加したことで、email属性が全表スキャンにならなくて済むようにもなった。)

マイグレート

bundle exec rake db:migrate

さらに、emailをデータベースにセーブする際に文字を小文字に変換する処理をいれておく。
app/models/user.rb

class User < ActiveRecord::Base
    before_save { self.email = email.downcase }
....
end

このようにbefore_saveコールバックでの処理を書いておくと、セーブ前に小文字に変換してくれる。

セキュアなパスワードの追加

ユーザ属性にパスワードを追加したい。パスワードは暗号化されてデータベースに保存される。
認証は暗号化されたパスワード同士を比較することで行う。

railsではhas_secure_passwordメソッドで作れる。
(カラム名はpassword_digestにすることが重要)

まず、ハッシュ関数には最新のbcryptを使用するため、gemfileにbcrypt-rubyを追加

source 'https://rubygems.org'
ruby '2.0.0'
#ruby-gemset=railstutorial_rails_4_0

gem 'rails', '4.0.5'

gem 'bootstrap-sass', '2.3.2.0'
gem 'sprockets', '2.11.0'
gem 'bcrypt-ruby', '3.1.2'
....

インストール

bundle install

カラム追加マイグレーションを生成

rails generate migration add_password_digest_to_users password_digest:string

db/migrate/20150914094154_add_password_digest_to_users.rb

class AddPasswordDigestToUsers < ActiveRecord::Migration
  def change
    add_column :users, :password_digest, :string
  end
end

マイグレートする。

 bundle exec rake db:migrate


ユーザクラスにhas_secure_passwordを追加する。

app/models/user.rb

class User < ActiveRecord::Base
...
    has_secure_password
end

ユーザのパスワードがあっているかは

user.authenticate(password)

で確認できる。

パスワードの長さを最小でも6文字にする。

class User < ActiveRecord::Base
...
    has_secure_password
    validates :password, length: { minimum: 6 }
end

コンソールで確認してみる。
ここまで。