kazumalab tech log

流行りとリラックマと嵐が大好きです。技術的ログ。

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を比較してnot nilだった場合その値を返す
  • @textがnilだった場合は"hello"を@textに代入して@textの値を返す

つまりは@textnilの状態で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は代入されないみたいです。(仕様?)

次に呼ばれたのが、authorsaveするときに呼ばれました。

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への問い合わせが一回で済みますよね。 そういったようにもう少し効果のわかりやすいのにして計測すればよかったですね。

明日のアドカレは!

@tastuoさんのAWS Lambda Layersを使ってみた!(Python)です!