Proposal for built-in memoization on models

Hey core team

What are your thoughts on including an integrated mechanism for memoizing data on ActiveRecord models? For instance, hooking into the reload method to clear the memoizations. It seems like every project I’ve worked on had this need. It would be nice if it was built-in. Since models end up holding lots of business logic, and are passed throughout the system, they generally tend to be a great place for storing associated data that isn’t necessarily a proper AR relationship (which would already be memoized).

Here’s an example implementation to give you an idea:

module MemoRecord
  # Hash-like object for memoized data
  # Use +memo.fetch+ to memoize nils and false, similar to how +Rails.cache.fetch+ behaves
  def memo
    @memo ||= Memo.new
  end

  def clear_memo(*)
    memo.clear
  end

  def reload(*)
    clear_memo
    super
  end

  class Memo
    def initialize
      @data = {}
    end

    # Read the value for a key
    def [](key)
      @data[key]
    end

    # Write the value for a key
    def []=(key, val)
      @data[key] = val
    end

    # Fetch or store the value for a key
    # @note Used to memoize nil and false values, to avoid extra round trips to the db
    def fetch(key)
      @data.fetch(key) { @data[key] = yield }
    end

    def clear
      @data.clear
    end
  end
end

The usage ends up looking something like this:

class Post < ApplicationRecord
  include MemoRecord

  def latest_comment
    memo.fetch(__method__) { todo }
  end
end

post = Post.new
post.latest_comment
post.reload # => often called from tests to reset cached data

A brief history of memoization in Rails that probably also includes the answer:

Based on the original internal implementation being deprecated and extracted into a gem, I assume the answer today would still be “create a gem instead of putting it into Rails”. (Note: I’m not a core team member; I just knew this history offhand. But it includes previous decisions by the core team which seem to jibe with the recommendations on other recent proposals to add generic behavior like this.)

The recommendation would probably be to either fork and update memoist (as a few have already done in the comment thread linked above), or create your own gem to try to replace memoist.

1 Like

I appreciate your response and insight. Thank you

1 Like

As a counterpoint, I have never felt the need to do this, and haven’t observed that memoization as a deafult behavior produces a valuable result. I have seen that memoization “just because” can produce extremely confusing behavior across threads that serve different requests. I’m not sure where some devs have learned that they should memoize everything, but it is probably better to never memoize until you have profiled your code for performance issues that it could solve.

The reason for Rails deprecation is that having a module was felt to be overcomplicated:

This is being removed because during the Rails 3 refactoring we have removed almost all occurrences of Memoizable for the simpler ruby @var ||= pattern.

It seems that loading data once per request is the hardest problem for applications, at least Rails apps. And memoizing it tends to resolve that.

The @var ||= pattern technique takes you most of the way there. This approach just goes one step further by clearing that ivar when you reload the model. And instead of keeping track of a bunch of ivars and knowing which to clear, we consolidate them into one hash.

Definitely shouldn’t be default behavior for every method… But why not include it by default? So its there when you need it

Note that by default, Rails uses SQL Caching in order to execute the same SQL only once per request.

Yep, I’m aware of the SQL cache. But we should also mention that there are exceptions to it. For instance, it is cleared during a request when any record is reloaded or inserted/deleted/created. This means you may end up loading the same data again for unrelated models. And even when it is coming from the cache, its not instant, and it’s not free.

It’s pretty clear there is a benefit from memoizing data on models, as evidenced by the association_cache. Which has the same characteristics I’m proposing here.

I used to use the heck out of ActiveSupport::Memoizable and was sad to see it go. While I can see some of the complaints against it (overly complicated, changed the method signature, etc) those seemed like impl details that could be addressed.

That being said, there are numerous gems that can add this functionality which seems like a perfectly acceptable solution. I personally use the memery gem. It’s actively maintained and well implemented.

It does require each class to opt-in to the DSL which some folks might see that as a strength. If you do want it globally that is no problem. Just mix it into Object. I do this by adding include Memery to my application.rb before the application is defined.

1 Like