Ruby on Railsガイドを通読してまとめる Part.3
第三弾です。前回はこちら。Ruby on Railsガイドを通読してまとめる Part.2 - エンジニア備忘録
なんだかんだPart.3まで続けられました。
これもひとえに自分の努力の成果です。自分すげえ。(ただまとめてるだけなので大した努力ではない)
読みやすいようにまとめたいと思うんですが、やはり量が膨大になってしまうので、さらっと流し見して気になるところがあれば本家Railsガイドさんを読んでいただければと思います。
- モデル
- ActiveRecord クエリインターフェイス
- DBからオブジェクトを取り出す(1)
- 単一のオブジェクトを取り出す(1.1)
- 複数のオブジェクトをバッチで取り出す(1.2)
- 文字列だけで表した条件で取り出す(2.1)
- プレースホルダーを使用した条件で取り出す(2.2.1)
- 条件の上書き(8)
- Nullリレーション
- 楽観的ロック(optimistic lock)(11.1)
- 悲観的ロック(pessinistic lock)(11.2)
- joinによる結合(12)
- left_outer_joins
- 関連付けを一括読込する(13)
- 複数の関連付けを予め読み込む(13.1)
- ネストした関連付けハッシュ(13.1.2)
- 関連付けの一括読み込みで条件を指定する(13.2)
- スコープ(14)
- 引数を渡す(14.1)
- 条件文を使う(14.2)
- デフォルトスコープ(14.3)
- スコープの解除(14.5)
- Enums(16)
- メソッド
- EXPLAIN
- ActiveRecord クエリインターフェイス
- ビュー
モデル
ActiveRecord クエリインターフェイス
Active Recordは、ユーザーに代わってデータベースにクエリを発行できる。勉強初期の頃は全くピンとこない概念。(今は何とかわかる)
発行されるクエリは多くのデータベースシステム (MySQL、MariaDB、PostgreSQL、SQLiteなど) と互換性がある。
ActiveRecordはORマッパーとしてとても評判がいい。(と聞いたことがある。ソースはないです)
以下、様々なメソッドについての説明。
DBからオブジェクトを取り出す(1)
ActiveRecord::Baseを継承したクラスからオブジェクトを取り出すためのメソッド。たくさんありますね。
find create_with distinct eager_load extending from group having includes joins left_outer_joins limit lock none offset order preload readonly references reorder reverse_order select where
気になったやつだけ抜粋。
単一のオブジェクトを取り出す(1.1)
take
ランダムに1つ取り出す。(例:Client.take)
SQLとしては以下。
SELECT * FROM clients LIMIT 1
take(2)といった形にすれば取り出す数を増やせる。
firstとかはこのSQLにORDER BY idついてるだけなので、そっちでもいい。
(idにインデックス貼ってないことはないだろしtakeとfirstで早さほぼ変わらないはず)
複数のオブジェクトをバッチで取り出す(1.2)
find_each
DBから取り出したデータをメモリを圧迫しないサイズにして、ブロック引数に「取り出したレコード1つ」を格納して処理する。
User.find_each do |user| NewsMailer.weekly(user).deliver_now end
デフォルトでは1000件がバッチとなり、|user|に1つずつレコードが格納されている(はじめに1000回繰り返し、終わったら次。)
オプションによってバッチとする件数を3000とかにカスタマイズ出来たり、どのidからどのidまでで処理を止めるか設定できる。
find_in_batches
DBから取り出したデータをメモリを圧迫しないサイズにして、ブロック引数に「取り出したレコード全て」を格納して処理する。
# 1回あたりadd_invoicesに納品書1000通の配列を渡す Invoice.find_in_batches do |invoices| export.add_invoices(invoices) end
invoicesというブロック引数に1000件分のレコードが詰まっている。その1000件分の情報はたぶんActiveRecord::Relationオブジェクトという形にして保持されていると思うけど、書いていないので詳細は謎。
気になる人は調べてね!
文字列だけで表した条件で取り出す(2.1)
SQLインジェクションを発生させないため、パラメーターを使うときは配列(?を使った表現)を使用しましょう。
Client.where("orders_count = ?", params[:orders])
パラメータを使わないなら可読性上げるために文字列でもいいと思います。
プレースホルダーを使用した条件で取り出す(2.2.1)
配列(引数の順番で渡す)以外にも、ハッシュを使える。
Client.where("created_at >= :start_date AND created_at <= :end_date", {start_date: params[:start_date], end_date: params[:end_date]})
読みづらくね?
条件の上書き(8)
unscope
条件を外せる。カスタムしたスコープから一つだけ条件外したいときとかに使うのかな?
Article.where('id > 10').limit(20).order('id asc').unscope(:order)
reorder
デフォルトスコープの並び順を上書きできる。
class Article < ApplicationRecord has_many :comments, -> { order('posted_at DESC') } end Article.find(10).comments.reorder('name')
#上書きしたSQL SELECT * FROM articles WHERE id = 10 SELECT * FROM comments WHERE article_id = 10 ORDER BY name #上書きする前のSQL SELECT * FROM articles WHERE id = 10 SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
基本unscopeでいけそうだけど、デフォルトスコープに他の条件がたくさんあってorderだけ変えたいときに使える。(あるのか?)
Nullリレーション
noneメソッドはチェイン可能なリレーションオブジェクトを生み出すことができる。
# visible_articles メソッドはリレーションを返すことが期待されている @articles = current_user.visible_articles.where(name: params[:name]) def visible_articles case role when 'Country Manager' Article.where(country: country) when 'Reviewer' Article.published when 'Bad User' Article.none # => []またはnilを返すと、このコード例では呼び出し元のコードを壊してしまう end end
nilじゃなくてActiveRecord::Relation(中身無し)を返したいときに使う。便利そう。
楽観的ロック(optimistic lock)(11.1)
複数のユーザーが同じレコードを編集することを認める。 このロックを使用するためには、テーブルに「lock_version」という名前のInteger型カラムが必要である。
レコードが更新されるたび、lock_versionカラムの数値が1ずつ増える。 ユーザーがレコードを更新するとき、DBのlock_versionが自分が編集しているlock_versionより大きかったらエラー。
楽観的と言うとるわりに普通にエラーは出す。
悲観的ロック(pessinistic lock)(11.2)
DBのロック機構を使用する。 絶対に他のユーザーにUPDATEさせないという前提のもと、「読み出し許可」するかしないか設定できる。
# MySQLの場合は、lockメソッドに「LOCK IN SHARE MODE」を与えれば読み出しは許可できる。 Item.transaction do i = Item.lock("LOCK IN SHARE MODE").find(1) i.increment!(:views) end
たくさんのユーザーが1つのリソースを触るんじゃなく、決まったチームメンバーが1つのリソースを丁寧に時間かけて編集するときは悲観的ロックのほうが親切だろう。(楽観的ロックだと「え?hogeさんも編集してたんすか?あーだったら俺は編集しなかったのに」みたいなのが起き得る)
joinによる結合(12)
内部結合。SQL文を引数に渡すこともできるが、関連付けしてあれば結合クエリを簡単に作れる。
left_outer_joins
外部結合。関連レコードがない場合でもレコードをセットを取得できる。
関連付けを一括読込する(13)
eager loading! ググってもはっきり出てこないやつ。
日本語には「一括読み込み」と訳すようですね。
N+1クエリ問題を解決する
includeで予め読み込んでおく解決策
clients = Client.includes(:address).limit(10) clients.each do |client| puts client.address.postcode end
この場合は、addressesテーブルの外部キーをINで指定しておくことで、先に読み込んでいるようです。
SELECT * FROM clients LIMIT 10 SELECT addresses.* FROM addresses WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
複数の関連付けを予め読み込む(13.1)
2つ引数を指定すればオーケー。
Article.includes(:category, :comments)
ネストした関連付けハッシュ(13.1.2)
Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
上のコードは、id=1のカテゴリを検索し、関連付けられたすべての記事とそのタグやコメント、およびすべてのコメントのゲスト関連付けを一括読み込みする。
「先の先」を取りに行く場合は、ハッシュ構造を使うということですね。「Category.include({article: commets})」で、「categoryに紐づくarticlesを全てとる。そして取ってきたarticleにcommentsを紐づけておく」という動き。
関連付けの一括読み込みで条件を指定する(13.2)
ガイドに書いてあった日本語が難解だった。
「Active Recordでは、joinsのように事前読み込みされた関連付けに対して条件を指定することができますが、joins という方法を使用することをお勧めします。」とのこと。
おそらく意図は「通常、内部結合しにかかりたいならjoinsを使うのが一番いい」ってことかな…?
ただjoin使ってダメなときはincludesにwhereを使って外部結合表現することも可能、とのこと。
Article.includes(:comments).where(comments: { visible: true })
上のコードは下のクエリを発行する。外部結合していますね。
SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)
ハッシュを渡さず文字列でwhereの条件を指定した時、リファレンスを指定して強制的にテーブルをjoinし、SQL断片化を防ぐ必要があるとのこと。
Article.includes(:comments).where("comments.visible = true").references(:comments)
SQL断片化…ってなにって思って調べてもよくわからん…。親が消えたのに残った外部キーのインデックスのことですかねえ。
そしてreferences(:commnets)を付けることでクエリにどう変化がうまれるのかサッパリ。
ここは宿題にさせて下さい(涙目)
スコープ(14)
scopeメソッドにシンボルとlambdaブロックを渡すことで、カスタムクエリ(スコープ)を作れる。
条件を組み合わせてあなただけのクエリを発行しよう!
class Article < ApplicationRecord scope :published, -> { where(published: true) } end
上のコードは、下と同義らしいです。
class Article < ApplicationRecord def self.published where(published: true) end end
つまりscopeなんてかっこつけたところで本質は「ただのクラスメソッド」ってことですね!!なんだビビらせやがって…。(ただ、戻り値の観点で少し差が生まれることがある。少し下の#14.2参照)
スコープ内でスコープをチェインすることも可能。
引数を渡す(14.1)
scopeでも引数を渡せるらしい。
class Article < ApplicationRecord scope :created_before, ->(time) { where("created_at < ?", time) } end
ただ引数を渡す場合は、クラスメソッドとして定義することが推奨されているだとか。「scopeはクラスメソッドを複製した機能である」ということを強調するためらしいですが、scopeの方が明示的に「クエリ用」ってわかりやすいから、こっちでも良い気がしますけど、どうなんでしょう。
条件文を使う(14.2)
scopeとクラスメソッドで挙動が違う。
# これの戻り値は常に「ActiveRecord::Relationオブジェクト」 class Article < ApplicationRecord scope :created_before, ->(time) { where("created_at < ?", time) if time.present? } end # これは条件が「false」のとき、戻り値はnil class Article < ApplicationRecord def self.created_before(time) where("created_at < ?", time) if time.present? end end
デフォルトスコープ(14.3)
前回の記事で、「関連付けのさいの(相手側への)デフォルトスコープ」を学んだが、これは「自分に設定するデフォルトスコープ」
class Client < ApplicationRecord default_scope { where("removed_at IS NULL") } end
こうすると、このモデルへのクエリに「WHERE removed_at IS NULL」がデフォルトで設定される。
デフォルトスコープの条件を複雑に設定したいなら、「self.default_scope」で定義してもOK。
スコープの解除(14.5)
デフォルトスコープ等を完全に解除したいときはunscopedしましょう。
Client.unscoped.all # SELECT "clients".* FROM "clients" Client.where(published: false).unscoped.all # SELECT "clients".* FROM "clients"
大事なことなので2回言いました。
Enums(16)
整数型のカラムを設定可能な値の集合にマッピングしてくれる上に、対応するスコープまで自動的に作成してくれる。
class Book < ApplicationRecord enum availability: [:available, :unavailable] end
0で:available、1で:unavailable。
メソッドの例が以下。
# 下の両方の例で、利用可能な本を問い合わせている Book.available # または Book.where(availability: :available) book = Book.new(availability: :available) book.available? # => true book.unavailable! # => true book.available? # => false
詳細はRailsAPIへ!
メソッド
find_or_create_by(18.1)
レコードを検索し、なければ作成するメソッド。
レコード作成時の各カラムの値については、ブロックを渡すか、create_withメソッドでの指定で対応できる。ブロックのほうが勝手が効きそうなので、こちらだけ紹介。
Client.find_or_create_by(first_name: 'Andy') do |c| c.locked = false end
Clientモデルのlockedカラムをfalseにする処理ができます。
find_or_initialize_by
レコードを検索し、なければ「new」でインスタンスを作成するメソッド。保存処理はしない。
find_by_sql
引数に文字列を渡すと、それをSQLとして実行してくれる。戻り値は常に配列(内部にオブジェクト格納)
select_all
引数のSQLを実行し、結果としてActiveRecord::Resultオブジェクトを返す。to_hashを打つとハッシュが格納された配列が得られる。
pluck
英語の意味は「摘む」。
テーブルから特定のカラムの値だけを配列等で取り出したいときに使える。
mapなどで無理やり取り出すより、pluckで取り出したほうがパフォーマンスが優れている。
Client.select(:id).map { |c| c.id } # または Client.select(:id).map(&:id) # または Client.select(:id, :name).map { |c| [c.id, c.name] }
上の例はAvtiveRecordオブジェクトへの#mapだが、下のようにpluckを使えば、ActiveRecordオブジェクトを準備しなくてもいいし、その上スマートに書ける。最高ですね。
Client.pluck(:id) # または Client.pluck(:id, :name)
ただし、オブジェクトを介していないため、オーバーライドは出来ない。
class Client < ApplicationRecord def name "私は#{super}" end end # Client.select(:name)で、Clientクラスのインスタンスが生まれている。それにnameを打つためオーバーライド出来る。 Client.select(:name).map &:name # => ["私はDavid", "私はJeremy", "私はJose"] # pluckはインスタンス作成を行わない。 Client.pluck(:name) # => ["David", "Jeremy", "Jose"]
他にも、あくまで配列で取り出すため、limitなどを打つことはできないといった注意点がある。
# Arrayにlimitは打てない Client.pluck(:name).limit(1) # => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8> # 先にlimitしてればOK Client.limit(1).pluck(:name) # => ["David"]
exists?
引数がDBに存在するか調べる。1つでもあればtrue、なければfalse
ライバルとして「present?」、「any?」が存在する。そしてそれぞれパフォーマンスが異なるそう。
そして「そのスコープによって検索に引っかかるレコードが存在するか?」という条件分岐だけをしたい際は、下の記事いわくパフォーマンス最強が「exists?」とのこと。
参考記事: Present? Vs Any? Vs Exists? - The Lean Software Boutique
この参考記事だとpresent?は内部結合したあと、レコード自体を取得しにいってるので、「存在するかどうか」だけを調べる場合パフォーマンスが悪いとのこと。
any?、exists?は内部結合するところまでは同じだけど、COUNT(*)でレコード数を取りに言ってるのでパフォーマンス的にpresent?に勝ってる。記事ではany?はLIMIT(1)を付けてないのでその分遅いらしいが、調べてたらRails5.2(?)からLimit付けてて同着らしい。どっちや。
ここまできたら内部のコード見に行った方がいいけどActiveRecord潜りにいく元気は今はねえ!! ということで、これも要検証認定です。おめでとう!!
count, average, minimum, maximum, sum(21)
SQLの集計関数を呼び出す者たち。
EXPLAIN
explainメソッドにより、標準出力などで以下のようなものが得られる
# これを打てば… User.where(id: 1).joins(:articles).explain
↓が得られるそうです
EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1 +----+-------------+----------+-------+---------------+ | id | select_type | table | type | possible_keys | +----+-------------+----------+-------+---------------+ | 1 | SIMPLE | users | const | PRIMARY | | 1 | SIMPLE | articles | ALL | NULL | +----+-------------+----------+-------+---------------+ +---------+---------+-------+------+-------------+ | key | key_len | ref | rows | Extra | +---------+---------+-------+------+-------------+ | PRIMARY | 4 | const | 1 | | | NULL | NULL | NULL | 1 | Using where | +---------+---------+-------+------+-------------+ 2 rows in set (0.00 sec)
使うのかな…?
ビュー
ActionViewの概要
ビュー生成を担当するライブラリ。
Jbuilder(3.1.3)
Railsにデフォルトで含まれるGemの1つ。JSONを生成するのに使用する。
.jbuilderという拡張子を持つテンプレートでは、jsonという名前のJbuilderオブジェクトが自動的に利用できるようになる。
詳しい挙動は公式ドキュメントで。
GitHub - rails/jbuilder: Jbuilder: generate JSON objects with a Builder-style DSL
パーシャルレイアウト(4)
全体のレイアウトとは異なり、ローカル変数を渡せる。パーシャルテンプレートをyieldで評価しながらテンプレートを作る。
articles/show.html.erb
<%= render partial: 'article', layout: 'box', locals: { article: @article } %>
artcicles/_box.html.erb
<div class='box'> <%= yield %> </div>
これでパーシャルテンプレートである_articleを、パーシャルレイアウトである_box内で使ってテンプレートを作成できる。
ActionViewのヘルパーメソッド(6)
大量にありますが、必要に応じて参照すれば良さそうです。(Rails側でテンプレートを作る技術が、今後フロントエンドのフレームワークに置き換わっていく可能性も考え、ここは一旦スルーします。)
レイアウトとレンダリング
コントローラからビューへの結果の渡し方について解説するトピック。
レスポンスを作成する(2)
コントローラー側から見ると、HTTPレスポンスの作成方法は以下の3通り。
- renderを呼び出し、ブラウザに返す完全なレスポンスを作成する
- redirect_toを呼び出し、HTTPリダイレクトコードステータスをブラウザに送信する
- headを呼び出し、HTTPヘッダーのみで構成されたレスポンスを作成してブラウザに送信する
設定より規約(2.1)
コントローラーはrenderを指定しなくても命名規則に従ったものをrenderする。
そういえば、この規約って、「convention」の訳だと思うんですが、どちらかと言えば「慣習」という意図のほうが意図にマッチしてる気がする
別のコントローラからアクションのテンプレートを出力する(2.2.1)
app/views以下のパスを指定すればOK
render "products/show"
appディレクトリ外のテンプレートを使いたい場合は、"/"から始まるフルパスで指定する。
renderのオプション(2.2.12)
以下の5つが一般的。
- content_type
- layout
- location
- status
- formats
content_type
レスポンスのcontent-typeを指定できる。:jsonを渡せば、application/json、:xmlを渡せばapplication/mlなど。
layout
現在のアクションに対して、特定のファイルをレイアウト指定できる。
出力時に、デフォルトのレイアウトを使用しないよう設定も可能。
render layout :false
location
HTTPのlocationヘッダーを設定できる。300などで飛ばす場所のこと。
render xml: photo, location: photo_url(photo)
status
HTTPステータスコードを明示的に指定したい場合に使用する。
formats
リクエストで指定されたフォーマットに応じてレスポンスを返す。
render formats: [:json, :xml]
レイアウトの探索経路(2.2.13)
レイアウトの探索経路は、「そのコントローラ管轄のviewsにlayoutディレクトリがあるか探し、なければ共通のレイアウトを使用する」という流れです。
たとえば、PhotosControllerクラスのアクションから出力するのであれば、app/views/layouts/photos.html.erbまたはapp/views/layouts/photos.builderを探します。該当のコントローラに属するレイアウトがない場合、app/views/layouts/application.html.erbまたはapp/views/layouts/application.builderを使用します。
設定次第で、任意のレイアウトを指定できる。
例えば、そのコントローラーのデフォルトのレイアウトを変更したい場合は
class ProductsController < ApplicationController layout "inventory" #... end
シンボルで指定すれば、レイアウトを遅延させて決定できる。
class ProductsController < ApplicationController layout :products_layout def show @product = Product.find(params[:id]) end private def products_layout @current_user.special? ? "special" : "products" end end
メソッドの戻り値に文字列をセットしておいて、あとで評価するということですね。
Procオブジェクトを渡せば、リクエストの内容に応じてレイアウトを指定することもできる。
class ProductsController < ApplicationController layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" } end
コントローラーインスタンスがリクエスト内容を保持しているようです。
二重レンダリングエラーを避ける(2.2.14)
and returnを使えば、レンダー先を指定したところで処理を終えるのが簡単。
def show @book = Book.find(params[:id]) if @book.special? render action: "special_show" and return end render action: "regular_show" end
redirect_toを使用する(2.3)
redirect_toメソッドは、別のURLに対して改めてリクエストを再送信するよう、ブラウザに指令を出すためのもの。
redirect_backを使う場合、戻り先の保証はHTTP_REFERERヘッダーによって行われるが、ブラウザが常に保証しているとは限らないので以下のようにして、失敗時どこに飛ぶかの設定は必要。
redirect_back(fallback_location: root_path)
headを使用する(2.4)
headを指定し、bodyなしのレスポンスを返すことが出来る。
head :bad_request
以下のようにヘッダーが生成される。
HTTP/1.1 400 Bad Request Connection: close Date: Sun, 24 Jan 2010 12:15:53 GMT Transfer-Encoding: chunked Content-Type: text/html; charset=utf-8 X-Runtime: 0.013483 Set-Cookie: _blog_session=...snip...; path=/; HttpOnly Cache-Control: no-cache
感想
ActiveRecordについてのトピックが終わって、ActionViewに関するトピックに入りました。
ビューに関するトピックはもう1つだけあるのですが、飛ばします!!
というのが、次に作ろうとしてるアプリはRailsAPI+Vue.jsの予定なので、ビューの知識がすぐに活きないからです。
そして、知識としても結構細々してるので、必要に応じて参照すればいいかな、と。
コントローラーはガッツリ使うので、読んでいこうと思います。
要検証
- present?, any?, exists? の違い
- where説を文字列で指定するincludeについて、referencesメソッドを打つか打たないかでのSQL文の違い
Article.includes(:comments).where("comments.visible = true").references(:comments)