Railsにおけるメモ化についてちょっと覗き見してみる
こんにちは! みんなのウェディングのエンジニア@kazumalabです。 この記事はくふうカンパニーアドベントカレンダーの3日目になります。
Rubyにおけるメモ化とは
||=
を利用して、オブジェクトのキャッシュを行うことができます。
例えば、
def say "hello" end
この場合say
メソッドを呼ぶ場合、振る舞いは同じなのに毎回違うStringクラスのインスタンスが生成されてしまいます。
確認してみます。
irb(main):010:0> say.__id__ => 70107590117540 irb(main):011:0> say.__id__ => 70107599432980
どうでしょうか。それではメモ化してみます。
def say @text ||= "hello" end
このメモ化の流れの振る舞い以下のようになっています。
つまりは@text
がnilの状態でsayメソッドが呼ばれると@text
に"hello"が代入されて、それ以降その@textに入っているオブジェクトを呼び続けるということになります。
irb(main):015:0> say.__id__ => 70107599401820 irb(main):016:0> say.__id__ => 70107599401820 irb(main):017:0> say.__id__ => 70107599401820
何度呼び出しても同じオブジェクトのIDが返ってくることがわかります。
Railsでメモ化を使うケース
まずはメモ化をどういったところで使うのかを考えてみます。
- Formオブジェクトのgetter
- Decoratorパターンでメール送信をするときなど
- APIを利用するとき
いろんなユースケースがあると思います。 さて、Railsでメモ化するとどれぐらいいい感じになるんでしょうか。
Attributesの呼ばれる回数
今回のケースとして、Formオブジェクトで利用する場合を例題にあげてみます。
Book
モデルとAuthor
モデルがあり、それを同時に生成するFormオブジェクトを以下のように記述してみます。
class BookForm include ActiveModel::Model attr_writer :name attr_accessor :title validates :title, :name, presence: true def save return unless valid? ActiveRecord::Base.transaction do book.save! author.save! end end def name @name ||= "NoName" end private def book @book ||= Book.new(title: title) end def author @author ||= book.authors.build(name: name) end end
作者名を入力しない場合はNoNameを入れる仕様にしています。
流れ
/books/new
にアクセス/books
にデータをPOST/books/:id
にリダイレクト
/books/new
にアクセス
さて、この作者の名前を返すnameメソッドにbinding.pry
を仕組み、動きをみてみます。
まず止まったところは/books/new
にアクセスしたときです。
/books/new
ではform_with
を利用した以下のようなフォームを設置しています。
= form_with model: @book_form, url: books_path, method: :post, local: true do |f| - if f.object.errors.present? - f.object.errors.full_messages.each do |message| %p= message = f.label :title = f.text_field :title = f.label :name = f.text_field :name, value: @book.name = f.submit
text_fieldメソッドの中でAttributesが展開されているので、ここでまずは一回呼ばれました。この時点で@name
の中身はnilなのでメモ化されることになります。
exit
でpryを抜けると/books/new
が表示されました。
しかし、ここで一旦このオブジェクトの寿命は以上になります。
/books
にデータをPOSTする
さて、データを入力して、POSTをします。
Controllerでパラメータを受け取り、BookForm
のインスタンスを生成します。
その後保存するためのsave
メソッドを呼びますが、その中でvalidationが走ります。
def save return unless valid? # here ActiveRecord::Base.transaction do book.save! author.save! end end
まずはそこで呼ばれました。まぁそうですね。nil
だった場合はここでNoName
が代入されることになりますが、今回の検証で初めてわかったのですが、空文字だった場合はNoName
は代入されないみたいです。(仕様?)
次に呼ばれたのが、author
をsave
するときに呼ばれました。
def author @author ||= book.authors.build(name: name) end
空文字だった場合はメモ化が使えないのは以外でした。
このオブジェクトはバリデーションに失敗した時にはrender :new
を呼ぶため、/books/new
にアクセスしたときと同じ回数がプラスで呼ばれることになりそうです。今回の場合、validationに失敗した場合も、しなかった場合も3回呼ばれました。
最後に
(時間が間に合わなかったー) 今回の検証方法ではあまりメモ化の良さみたいなところがわかりにくかったと思います。 例えば
def book @book ||= Book.find_by(title: title) end
みたいな感じでメモ化するとViewで何度も呼ばれてもDBへの問い合わせが一回で済みますよね。 そういったようにもう少し効果のわかりやすいのにして計測すればよかったですね。