Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Custom relation with caching and preloading (ActiveRecord)
require "active_support/concern"
require "loadable/relation"
module Loadable
module Model extend ActiveSupport::Concern
module ClassMethods
def loadable(name, options)
loadable_relations[name] = build_relation(name, options)
define_reader(name)
end
def loadable_relations(name=nil)
@loadable_relations ||= {}
if name
@loadable_relations.fetch(name).new
else
@loadable_relations
end
end
private
def build_relation(name, options)
Relation.build(self, name, options)
end
def define_reader(name)
self.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_relation
loadable_relations(:#{name})
end
def #{name}
#{name}_relation.load
end
CODE
end
end
def loadable_relations(name)
@loadable_loaded_relations ||= {}
@loadable_loaded_relations[name] ||=
self.class.loadable_relations(name).tap do |relation|
relation.instance = self
end
end
end
end
module Loadable
class Relation
extend Forwardable
class << self
def build(model_class, name, options)
Class.new(self) do
self.model = model_class
self.name = name
self.loader = options.fetch(:with).new
end
end
attr_accessor :model, :name, :loader
end
attr_accessor :instance
def load(collection=nil)
if instance.present?
load_instance
elsif collection.present?
load_collection(collection)
end
end
def loaded?
!!@elements
end
def loaded!(elements)
@elements = elements
end
private
def load_instance
return @elements if loaded?
loader.on_instance(instance).tap { |elements| loaded!(elements) }
end
def load_collection(collection)
loader.on_collection(collection).tap do |elements|
collection.each do |instance|
matches = elements.select { |element| loader.related?(instance, element) }
instance.__send__("#{name}_relation").loaded!(matches)
end
end
end
def_delegators "self.class", :model, :name, :loader
end
end
class Keyword < ActiveRecord::Base
has_and_belongs_to_many :articles
end
class Article < ActiveRecord::Base
has_and_belongs_to_many :keywords
include ::Loadable::Model
loadable :related_articles, with: Loader::RelatedArticles
end
articles = Article.where(author: author).load
# Load the related article using the `RelatedArticles#on_instance` method
articles.first.related_articles # Triggers a SQL query
# Preload all the related articles of the Article array using the
# `RelatedArticles#on_collection` method then use the `RelatedArticles#relaed?`
# method to dispatch the related_articles into the `articles` array.
Article.loadable_relations(:related_articles).load(articles)
# Everything is already loaded
articles.last.related_articles # Does not trigger any SQL query
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment