【scope】EverydayRailsで初学者が理解できなそうなところ
なんだこの記述は、、!?
こんにちは!大ちゃんの駆け出し技術ブログです。
RSpec学習には以下の有名教材があります。 Everyday Rails - RSpec による Rails テスト入門 テスト駆動開発の習得に向けた実践的アプローチ
Aaron Sumnerさんという方が書いた本を日本語訳されています。 翻訳した方々の中にはあの「プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで」を書かれた伊藤淳一もいらっしゃいます。
自分もプログラミングスクールの課題のために読み進めていますが、かなり序盤に出てくる以下の記述に驚きました。
class Note < ApplicationRecord scope :search, ->(term) { where("LOWER(message) LIKE ?", "%#{term.downcase}%") } end
うわ、なんだこの見たこともない記載方法は、、!?。scope? whereの中身がなんか変!
この文章を初学者の人は理解できるのかなと思いました。 自分は初見では全くわからずぐぐりましたね。
ということで今回は上記の文をまるっと全部説明したいと思います!
本記事を読む読者のレベル
本記事は「Everyday Rails」を読んでいる読者を想定しています。 したがって、本記事の対象者のレベルは「Everyday Rails」に書かれている読者レベルと同じです。 つまり、
- Rails で使われているサーバーサイドの Model-View-Controller
- gem の依存関係を管理する Bundler
- Rails コマンドの実行方法
- リポジトリのブランチを切り替えられるぐらいの Git 知識
なお、SQLについての知識、特に、where、ワイルドカード、LIKEについて知識があるとより理解が簡単になると思います。
使用方法の説明
具体的な説明に入る前に、全体像を掴むために、今回説明するものがいったいどのようにして使われるのか、どんな機能を持つのか見ておきましょう。
上記のscope〜
を記述することで以下のような使われ方をします。
@notes = Note.search("first")
この形どこかで見たことありませんか? そう、コントローラでDBからモデルの対象のレコードを取得する方法です。 これはクラスに対して実行するクラスメソッドというものですね。
@users = User.all @user = User.find(params[:id])
Note
はモデルです。
ファイルを見てみるとtext
型でmessage
カラムがあります。
create_table "notes", force: :cascade do |t| t.text "message" . . . end
具体的にNote.search("first")
が何をしているのかというと、searchとあるように、Noteモデルのmessageカラムに文字列"first"を含んだ文字列があるかどうか検索し、あればそのモデルを取得します。
例えば、DBに以下の3つのnoteが登録されているとします。
- note1・・・messageカラムが"first"
- note2・・・messageカラムが"first name"
- note3・・・messageカラムが"second"
そして、@notes = Note.search("first")
とした場合、インスタンス変数@notes
にはnote1とnote2が格納されますが、note3は文字列"first"を含まないので取得されません。
ここまで全体像を説明してきましたが、ポイントは2つです。
① scopeを使うことでクラスメソッドが使えるようになる
② whereの箇所では、指定した文字列を含む文字列がmessageカラムにあるかを検索する機能がある
では①、②の順番で詳しく説明していきます。
scopeについて
まずはscopeの部分。
class Note < ApplicationRecord # ----------------------- scope :search, ->(term) { # ----------------------- where("LOWER(message) LIKE ?", "%#{term.downcase}%") } end
説明したように、上記はモデル内で定義し、コントローラで使われます。
言い換えれば、モデル内で定義をしなくても、コントローラで使用することが可能です。
term = "first" @notes = Note.where("LOWER(message) LIKE ?", "%#{term.downcase}%")
ですが、見てわかる通り、こちら長くないですか?
まず、term = "first"
のように検索したい文字列変数として定義しなければなりません。
Note.where
以降も長くて理解しづらいかと思います。
そこでモデルに定義することで上の文が、あらスッキリ、生まれ変わります。
@notes = Note.search("first")
文字列の定義をせず引数として文字列を指定!
where
メソッド自体もモデルに定義されているので長い文をコントローラに記載せずに使用!
可読性がすごくあがったのをお分かりいただけたかと思います。
search("first")
で何をしているのかが直感的にも理解できますしね。
このようにメソッドとして切り出すことがscopeを使うメリットです。
ここまで説明してscope :search, ->(term)
の部分に関して以下のことも 理解できたのではないのでしょうか。
① searchの部分はクラスメソッドを定義していること
② termの部分はクラスメソッドの引数であること
本来であれば、search(term)
みたいな記述だと理解しやすいのですが、scope
独特の記述方法によって、引数が離れてしまっていますし、searchの箇所も:search
とシンボルで記載しなければなりません。
ここまでわかればもうscope :search, ->(term)
について疑問はないでしょう。
総括すると、 本来であればDBからのレコード取得時にすごく長い文章を書かなければならない場合、scopeでクラスメソッドを定義し、短文でコントローラで使用することができます。
where("LOWER(message) LIKE ?", "%#{term.downcase}%")
そしてとっても難解なこの文章はなんなんでしょう。
class Note < ApplicationRecord scope :search, ->(term) { # ---------------------------------------------------- where("LOWER(message) LIKE ?", "%#{term.downcase}%") # ---------------------------------------------------- } end
3つに分けて説明します。
①whereメソッドについて
②"LOWER(message) LIKE ?"について
③"%#{term.downcase}%"について
whereメソッドについて
まず、whereメソッド
から簡単に説明します。
where(カラム名: 値)
where
メソッドはfind
、find_by
同様にDBからレコードを取得します。
決定的に違うのはfind
、find_by
は1つのレコードを取得するのに対し、where
メソッドは複数のレコードを取得するのです。
例えば、1番最初に紹介した例と同様にDBに以下の3つのnoteが登録されているとします。
- note1・・・messageカラムが"first"
- note2・・・messageカラムが"first name"
- note3・・・messageカラムが"second"
そして、find_by
で文字列"first"を含むmessage
カラムがあるレコードを取得する時は以下のようになると思います。
Note.find_by(message: "first")
ですが、当然これではnote1, note2は同時に取得できません。 理由は簡単。 find_byはレコードを1つしか取得できないからです。
よってwhere
を使って複数取得します。
Note.where(message: "first")
しかしながら、"LOWER(message) LIKE ?"
は本来where
の第一引数であるカラム名とは少し違うようです。
"LOWER(message) LIKE ?"について
まずwhere
の第一引数の"LOWER(message) LIKE ?"
がそもそも何なのかですが、これはSQLの文章です。
まずLOWER
ですがこれはSQLの関数になります。
意味は「引数の文字列を全て小文字にする」、です。
LOWER("FIRST") --> "first"
LOWER
の引数はmessage
となっています。
したがって、message
カラムにある文字列を全て小文字にしているということになります。
なぜ小文字にする必要があるのか?理由は後ほど説明します。
次にLIKE
の部分ですが、これはLIKE演算子の「あいまい検索」と言われるものです。
例えば、DB上の都道府県のテーブルから"島"という文字列を含んだ件名を取得したいとします。 そのような場合SQLの文章は以下のようになります。
SELECT 都道府県名 FROM 都道府県テーブル WHERE 都道府県名 LIKE "%島%"
LIKE
以降にある文字列がLIKE
の手前の文字列に含まれているかを判別しています。
ちなみに%
はワイルドカードと呼ばれるもので、0個以上の任意の文字を表します。
よって、%島%
は島を含んだ文字列となります。
LIKE
の手前はLOWER(message)
です。では後ろは?
後ろは?
ですね笑
?
って何と思われるかもしれませんが、じつは?
には第二引数である"%#{term.downcase}%"
が入ります。
実はこの?
ですがよくrailsでサーバを起動している際に頻出しています。
SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
“users”.“id” = ? の?は[“id”, 1]の1の値が代入される LIMIT ? の?には[“LIMIT”, 1]の1の値が代入される
上記の"LOWER(message) LIKE ?"
の?
は代入される値をSQLの後ろで指定している方法と同じ考え方です。
つまり、where("LOWER(message) LIKE ?", "%#{term.downcase}%")
は以下のSQLの条件に当てはまるレコードを取得しているのです。
WHERE LOWER(message) LIKE "%#{term.downcase}%"
"%#{term.downcase}%"について
そして、最後に"%#{term.downcase}%"
の部分です。
両端の%
は先ほど説明したように、0個以上の任意の文字を表します。
#{term.downcase}
は式展開で、引数のterm
に格納された文字列をメソッドであるdowncase
で全て小文字にしています。
term = "FIRST" "#{term.downcase}" --> # "first"
ここで先ほど紹介したLOWER(message)
でなぜmessage
カラムの値を全て小文字にしているのかの答えにつながります。
引数のtermが"First"、messageカラムの値が"FIRST"の時、それは文字列が一致しないということになり、結果そのmessageカラムのレコードは取得されません。 しかし、完全に一致する検索より、小文字大文字を区別しないほうが使い勝手が良さそうです。 なので、両方を小文字にすることで大文字と小文字を区別しないようにしているのです。
よって"%#{term.downcase}%"
は「termを含んだ文字列」となります。
結論: scopeとwhere("LOWER(message) LIKE ?", "%#{term.downcase}%")とは
長い文章を読んでいただきありがとうございました。
最後に結論として今まで記述したことをまとめて終わりとします。
class Note < ApplicationRecord scope :search, ->(term) { where("LOWER(message) LIKE ?", "%#{term.downcase}%") } end
scope
の部分の説明は再掲します。
① searchの部分はクラスメソッドを定義していること ② termの部分はクラスメソッドの引数であること
よって、コントローラで以下のように使われることができます。
@notes = Note.search("first")
そしてsearch
の内容であるwhere("LOWER(message) LIKE ?", "%#{term.downcase}%")
ですが、意味は
モデルのmessageカラムにtermを含んだ文字列があるかどうか検索し、あればそのモデルを取得
という意味になります。
終わりに
scope
とwhere
メソッドを説明しました。
この2つは今回のようにモデルで使用されることが多く、そのため2つセットで使われる場合が多い気がしています。
見た目は自分の知っているRailsの記述ではなかったので理解に苦労しましたが、仕組みを理解できれば簡単です。 初学者のうちから逃げずにしっかりと理解しておきましょう!
以上、大ちゃんの駆け出し技術ブログでした!