📝

Railsの"メモ化"の罠

に公開

メモ化

Railsの「メモ化」というテクニックを使うことで、重い処理を複数回呼び出すことなく値を保持させ続けることができます。

  def foo
    @foo ||= #重い処理 
  end

これによって、一度 foo を呼び出すと、その後も foo メソッドを呼び出した際には初回の値が記憶され続けるので重い処理が走らなくて済みます。
パフォーマンス向上において非常に有効なテクニックではありますが、正しい使い方を理解していないと危険なテクニックであるとも言えます。

いつまで記憶されるの?

これを理解していないと、めちゃめちゃバグを起こし散らかします。
結論を言うと、「そのクラス内の処理が終了するまで」です。
以下に具体例を挙げて解説していきます。

バグコード

カレンダーのスケジュールに関する通知を送る実装を例に、解説します。

  • calendar 1 - N schedules(カレンダーに紐づくスケジュール)
  • calendar 1 - N users(カレンダーをフォローしているユーザー)
      def call!
        return if calendars.empty?

        calendars.each do |c|
          target_schedules = schedules(calendar_id: c.id)
          next if target_schedules.empty?

          target_users = users(calendar_id: c.id)
          next if target_users.empty?

          send_messages(target_users, target_schedules)
        end
      end

      private

      def calendars
        @calendars ||= # GET calendars
      end
      
      def schedules(calendar_id:)
        @schedules ||= # GET schedules by calendar_id
      end
      
      def users(calendar_id:)
        @users ||= # GET users by calendar_id
      end

Q. このコードを実行すると、どんなバグが起こるでしょうか??

A. calendarsの先頭要素のカレンダーに関するスケジュールのみが、calendarsの先頭要素のカレンダーをフォローしているユーザーに、calendars.count回通知が飛ぶ。😱

改善したコード

      def call!
        return if calendars.empty?

        calendars.each do |c|
          target_schedules = schedules(calendar_id: c.id)
          next if target_schedules.empty?

          target_users = users(calendar_id: c.id)
          next if target_users.empty?

          send_messages(target_users, target_schedules)
        end
      end

      private

      # クラス内で値は常に同じであるため、メモ化で良い。
      def calendars
        @calendars ||= # GET calendars
      end

      # ここでメモ化は行わない
      def schedules(calendar_id:)
        # GET schedules by calendar_id
      end

      # ここでメモ化は行わない
      def users(calendar_id:)
        # GET users by calendar_id
      end

改善したコードはこうなります。これで正しい通知が飛ぶようになりました🎉
(実際は、クエリの数を考えると最初に一気に取得してループ内でフィルタリングするのが一番いいです。メモ化の解説のためこの形で掲載してます)

まとめ

メモ化は同一クラス内の実行開始〜実行終了まで同じ値を保持され続けます。
なお、この実装のあるクラスが終了すると、メモ化に使用していたメモリが解放されて、再度同じクラスが走った際にはまた新しい値で保持されます。

いわゆる、「クラス内定数」と思ってもいいかもしれません。

パフォーマンス向上のためには欠かせないテクニックですが、とりあえず使うのは危険です。ちゃんと使う理由を明示して使うように心がけましょう。

参考

https://qiita.com/isaka1022/items/1c98a92097d074544a42

Discussion