ruby on rails チュートリアルの6章まとめです。
記憶定着のために書くので、超個人向けです。
6章
railsのデータベース構造
Model
railsで扱うデータベース構造はModelと呼ばれる。(MVCのM)
デフォルトのORMとしてActiveRecordが利用され、sqlなどのRDBを操作する言語を気にせず、rubyやrailsのオブジェクト操作と同じ感覚でデータベース操作ができる。
モデルの作成は、ターミナルで以下のように実行できる。
rails g(generate) model User name:string
モデルは単数系で、controllerは複数形である。これは、モデルはUser単体の構造(およびオブジェクト)を表しており、controllerでは複数のuserを取り出すなどの操作をするためである。
属性(attribute)と、その型を同時に宣言できる。
また、以下のようなファイルが生成される。ApplicationRecordを継承しているため、親クラスに定義されているメソッドをそのまま利用できる。
class User < ApplicationRecord
end
同時に、マイグレーションファイルも生成される。
ここで見ておきたいのは、CreateUsersというクラスが生成されているのと、ActiveRecord::Migrationというクラスを継承していること。(親クラスの詳細は要確認)
また、changeメソッドを呼び出していること。そして、createの場合はその中でcreate_tableが呼ばれていること。
※この、changeメソッドの詳細はよくわかっておらず、ブロック内での記載も詳細はわかっていない。しかし、メソッドの呼び出しをしているというところは押さえておきたいと思う。
また、自動で追加されるtimestampsというのはcreated_atとupdated_atを作成・管理してくれるマジックカラムと呼ばれるもの。
他のormでも基本的に用意されていたりする。
class CreateUsers < ActiveRecord::Migration[7.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
マイグレーション
生成したモデル(およびマイグレーションファイル)を実際のDBに反映するには、マイグレーションをする必要がある。
コマンドは以下。
$ rails db:migrate
すると、db/schema.rb
に変更が入る。
ActiveRecord::Schema[7.1].define(version: 2025_03_31_112243) do
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
end
こちらも、create_tableメソッドが呼ばれているが、ActiveRecord::Schema
というクラスになっている。マジックカラムはちゃんとそれぞれの名前で反映されている。
モデルの作成など
作成、更新、削除
作成 ~ dbに保存までは、以下でできる。
createの場合は、true / falseではなくUserオブジェクトがそのまま返ってくる。createメソッド呼び出し or save時、マジックカラムの二つも自動でデータが入り、idも採番される。
ちなみに、入る時刻はUTCであり、このUTCというのは TUC or CUT どちら?というところで一致せず、折衷案でこう表記しているらしい。
user = User.new(<attributes>)
user.save
# or
user = User.create(<attributes>)
更新する時は、updateメソッドを呼ぶ or オブジェクトの属性に代入し、saveする。
updateの場合、直接呼び出して更新しても戻り値はtrue / falseとなる。
updated_atも更新される。
user = User.first
user.update(name: '~~')
# or
user = User.first
user.name = '~~'
user.save
削除する時は、destroyを呼べば良い。
※メモリ上には残っているので、削除後もコンソールに入力すると値が表示される。しかし、dbアクセスすると取得できないことが確認できる。
user = User.first
user.destroy
検索
ActiveRecordのおかげで、sqlを書かなくてもdbからデータを取得できる。
Userクラスの場合、Userというクラス自身から呼び出すクラスメソッドである。
user = User.first # idの昇順で最初のもの
user = User.find_by(name: '~~') # where 'name' = '~~' みたいな
user = User.find_by_name('') # 昔の書き方など
all = User.all # select * from users;
all.class # User::ActiveRecord_Relationであり、arrayではない。
# しかしall.lengthなど、まるでArrayかのように扱えたりする。
validation
モデルのattributeには、さまざまな検証(Validation)を設けることができる。存在するか?正規表現にマッチするか?など
1. 存在性確認
例えば以下。なんだか魔法のような記述であるが、validatesメソッドを呼び出し、その引数に:nameとvalidationの種類(存在する?)を渡している。
presence: trueはハッシュだが、最後の引数がハッシュの場合は{}がなくても良いというルールがあるので、 {presence: true}としなくて良い。
結果、魔法のようなコードに見える。
class User < ApplicationRecord
validates :name, presence: true
end
user = User.new(name: "")
user.valid? # false, validメソッドで満たしているか判定できる
user.save # false
user.errors.messages # {:name=>["can't be blank"]}
# ハッシュでエラーメッセージを持つ。複数の場合もハッシュの要素としてついかされるので、
# user.errors.messages[:name]とかすれば、特定の属性に関わるメッセージを取り出せる
2. 長さ制限
最大の長さの指定もできる。
(最小も定義できるが、今回はおそらくpresenceのチェック + その後のフォーマット確認でname, emailどちらも十分ということでないのだと思う)
class User < ApplicationRecord
validates: :name, length: { maximum: 50 }
validates: :email, length: { maximum: 255 }
end
ちなみに、ここで指定しているmaximumなどはバイトサイズではなく、文字単位(rubyのsizeなどで取得できる値)を指している。
irb(main):001> 'aaa'.bytesize
=> 3
'あああ'.bytesize
=> 9
irb(main):004> 'aaa'.size
=> 3
irb(main):005> 'あああ'.size
=> 3
また、emailの長さを255としているのは、慣習的にと言われていた。
これは、昔のRDBにおいて1バイトで表現できる長さの上限だったため。(0 ~ 255で2^8)
少し個人的に混乱するのは、1文字のサイズもaなどで1バイトあるのに、lengthのサイズも1バイトとはどういうこと?と思うところである。(1バイト * 文字数 としたら実際のデータサイズはゆうに1バイトを超えるのではないか?ということ)
ただ、これは少し的外れであり、dbなどに登録するlengthの情報把握まで長さの情報であり、実際のデータサイズを表しているわけではない。注意。
フォーマット
ここでは、演習で行なった変更について取り上げる。
# 初期設定
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
# 変更後
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
初期設定では、解説にある通りaaa@test..com
のような、ドットの連続も許してしまっていた。
変更後は何をしたかを見ると、以下の箇所が変更されているのがわかる。
@[a-z\d\-.]+\
@[a-z\d\-]+(\.[a-z\d\-]+)*\
詳しく見ると、[a-z\d\-.]
から[a-z\d\-]
と.がなくなっており、その代わり+(\.[a-z\d\-]+)*
という記述が追加された形である。
そもそもこの設定の意味について考えると、@aaa.co.jp
のように、ドメインがドットで連結されているものがあるため、それを許可しようとしている。
そして最初の設定でも、「英小文字、数字、ハイフン、ドットのいずれかを少なくとも1文字以上繰り返す」という意味になるため許可できているが、逆にドットの連続等も許可してしまっており不十分だった。そこで、
- まず、[a-z\d-]とドットを排除することで、
@.
というような形を排除 - 次に、
.<文字列>
という塊を複数許可
という形に変更している。一番最後は.[a-z]+\z/i
がついているため、最終的に
@<文字列><.文字列の塊0~複数><.comなど締めのドメイン>
のような形を許可することになり、より厳密なフォーマット規制になった。
※文字列、というのは正確でないです。記載の利便性のためそうしています。
unique制限
emailは一意であって欲しいので、その制約を追加する。
class User < ApplicationRecord
validates :email, uniqueness: true
end
さて、ここから「ちゃんと読んでね!」という注記とともに、より詳細な有効性検証にしていく。
この状態で問題となる点は以下。
- 1.メールアドレスは、大文字小文字の区別をしないが、現状は区別している状態
-
aaa@sample.com
とAAA@sample.com
は別、として扱ってしまっている - 大文字利用は許可したくない
- ※本当は、ドメインに関わらない部分における大文字小文字の区別はあるが、扱うのに厄介すぎて基本的には小文字統一しようね、という感じになっている
-
- 2.DBレベルでのValidationにはなっていない
- トラフィック(アクセス)が多いと、railsでの一意性検証を掻い潜ってdbに登録されてしまう可能性がある
- 3.高速なアクセスを実現したい
(確か)チュートリアルではこの2,3から解決していく。
方法は簡単で、新しくdbにindexとuniqueを設定するmigrationを作成して反映すれば良い。
rails g migration add_index_to_users_email
↓(changeメソッド以外からのマイグレーションファイルが生成されるので、中身を記載する)
class AddIndexToUsersEmail < ActiveRecord::Migration[7.1]
def change
add_index :users, :email, unique: true # ここを追記
end
end
ここでふと、indexの追加とuniqueの追加をどうして同時に行えるの?と思った。
理由は単純で、uniqueの検証をする → その値を持つレコードを検索する → 高速に検索したいので、indexと同時に設定できる
というだけである。
だから、index作成とuniqueの設定を分けることもできるようだが、冗長であるためadd_indexというメソッドでもuniqueの設定も受け取れるようになっている。
また、よく見るとマイグレーションファイルでは、毎回コマンド入力した名前のクラスが作成されているのだな、とも思う。
そして、1について解決していく。
本チュートリアルでは以下の流れで進めていく。
- 大文字、小文字を区別しない、というvalidationを付与
- 区別しない、ではなく小文字のみに統一する
1について、railsには便利なオプションがあるとして以下に変更した。
class User < ApplicationRecord
validates :email, uniqueness: { case_sensitive: false } # trueを変更
end
この、 case_sensitive
というのが大文字小文字を区別するというもので、それをfalseにすることで区別しないで重複チェックをする、という意味になる。これで、
AAA@sample.com
と aaa@sample.com
という二つはバリデーションで弾かれることになった。
とはいえ、現状はあくまで両者を区別しないというだけであり、AAA@sample.com
が先にくればそれで登録されてしまう。先述したように、すべて小文字で統一したい。
よって、最終的には以下のように修正した。
class User < ApplicationRecord
before_save { self.email = email.downcase } # コールバック関数を追加
validates :email, uniqueness: true # trueに戻す
end
まず、before_saveというコールバック関数で、saveの直前に呼び出すメソッドを追加する。
そこでは、self.email = email.downcase
と、emailのattributeを小文字にしている。
そして、uniquenessはtrueに戻している。これは、befoer_saveによりすべて小文字に統一されてからvalidationチェックが入るため、大文字でくることを考慮しなくて良いのでそうするのだと考えられる。
また余談だが、self.email = email.downcase
という記載は以下にもrubyっぽいが、少しrubyから離れていると混乱する。selfは右辺の場合省略できるので、このように記載できる
演習の記載方法について
演習では、以下のように記載を変更してみる、ということをした。
before_save { email.downcase! }
こちらでは、selfを利用していないにも関わらずテストは通過した。
どうしてなのか?と思ったが、この場合、rubyはemailという物を探し、ローカル変数にないためオブジェクトのattributeだな、と判断する。よってselfがなくても変更されるということのようだ。
逆に、self.email = email.downcase
がemail = email.downcase
では動かない、というのは、後者の場合emailというローカル変数を宣言し、そこに小文字のemailを代入しているだけであり、オブジェクトのattributeの変更はしないといことである。
なるほど。
セキュアなパスワード
ということで6章の最後、ユーザーにセキュアなパスワードを追加する。
セキュアな、というのは生のパスワードをそのままdbに保存せず、ハッシュかされた物を保存するということ。
railsでは、has_secure_password
というメソッドを利用することで、ほとんどそれが完了できてしまう。以下手順で完了できる。
- password_digestカラムをUserのスキーマに追加
- bcryptという暗号化ようのgemをインストール
-
has_secure_password
をUserモデルに追記
詳細な手順は省くが、これをすることで何ができるのかというと、
- password, password_confirmationという属性やカラムがないにも関わらず、仮想的な属性として利用できるようになる
- passwordに対するバリデーションが追加される & 追加できる
- passwordをハッシュ化してdbに登録するため、セキュリティ向上
- PCなどを利用しても復号は困難
- digestを抜き取ってもそれで認証はできない
以上で、6章の振り返りとする。
余談
モデルのclassのclassとsuperclassについて
※ここは、理解できているかだいぶ怪しいです。参考程度に。
なんとなく不思議だな、と思うことが。それは、
user = User.new
user.class # User
user.class.superclass # ApplicationRecord
user.class.class # Class
このように、Userクラスの継承関係と、Userクラスインスタンス自体のクラスとが違う、というもの。
ここで重要なのは、「何のインスタンスか」という点と、「何を継承しているか」は別ものであるということである。
だから、UserクラスはClassクラスのインスタンス(平たく書くと、User = Class.new(ApplicaionRecord)
という形)であり、また親クラスはApplicaionRecordクラス、ということである。
ということは、以下ということになる。
- UserクラスはClassクラスのインスタンスなので、Classクラスのインスタンスメソッドを利用できる(User.newなど)
- UserクラスはApplicationRecordを継承しているので、そこ(および親クラス)で定義されているクラスメソッドを呼び出せる(User.firstなど)
- Userクラスのインスタンスuserは、Userクラスに定義されたインスタンスメソッドを呼び出せる
- userはApplicationRecordを継承しているので、そちら(および親クラス)に定義されたインスタンスメソッドを呼び出せる
うーん、わかったようなわからないような。
もう少し整理する必要がある気がします。