大ちゃんの駆け出し技術ブログ

RUNTEQ受講生のかわいいといわれるアウトプットブログ

【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メソッドはfindfind_by同様にDBからレコードを取得します。 決定的に違うのはfindfind_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を含んだ文字列があるかどうか検索し、あればそのモデルを取得

という意味になります。

終わりに

scopewhereメソッドを説明しました。 この2つは今回のようにモデルで使用されることが多く、そのため2つセットで使われる場合が多い気がしています。

見た目は自分の知っているRailsの記述ではなかったので理解に苦労しましたが、仕組みを理解できれば簡単です。 初学者のうちから逃げずにしっかりと理解しておきましょう!

以上、大ちゃんの駆け出し技術ブログでした!