Skip to content

Instantly share code, notes, and snippets.

@zhandao
Last active November 10, 2023 07:05
Show Gist options
  • Save zhandao/f24f18e1fe1ef95e85ad79fcc0ff3180 to your computer and use it in GitHub Desktop.
Save zhandao/f24f18e1fe1ef95e85ad79fcc0ff3180 to your computer and use it in GitHub Desktop.
[Ruby on Rails] Let ActiveRecord take record instance directly from memory (cached)

inspired by [https://sourcediving.com/this-rails-cache-is-not-your-friend-512871c138aa]

Let ActiveRecord take record instance directly from memory (cached), instead of instantiate from result set, for reducing memory allocation times and time.

It is very simple implementation of Identity Map.

It is suitable for reading is much more frequent than updating, like hot posts (preferably non editable).

Better to use with ActiveRecord::QueryCache

# lib/memory_take.rb

module MemoryTake
  extend ActiveSupport::Concern

  class_methods do
    attr_accessor :memory_take_store

    # @todo
    #   - locking
    #   - freeze?
    #   - expire when updated
    #   - enable (block)
    # @example
    #   User.find(1).object_id # => abc
    #   User.find(1).object_id # => abc (same object)
    #
    #   User.find(1).posts
    #   User.find(1).posts        # `.posts` without SQL ~
    #   User.find(1).posts.hidden # scope will make SQL
    def memory_take_by(identity_key = :id, expires_in: 30.seconds)
      self.memory_take_store = MemoryTake::Store.new(expires_in:)
      identity_key = identity_key.to_s

      define_singleton_method(:instantiate_instance_of) do |klass, attributes, column_types = { }, &block|
        identifier = attributes[identity_key]
        cached = klass.memory_take_store.read(identifier)
        return cached if cached.present?

        instantiated = super(klass, attributes, column_types, &block)
        klass.memory_take_store.write(identifier, instantiated)
        instantiated
      end
    end
  end
end
# lib/memory_take/store.rb

class MemoryTake::Store < ActiveSupport::Cache::MemoryStore
  module NotDupCoder
    module_function

    def dump(entry) = entry
    def dump_compressed(entry, _threshold) = entry
    def load(entry) = entry
  end

  private def default_coder = NotDupCoder
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment