Skip to content

Commit

Permalink
Add ingredient associations
Browse files Browse the repository at this point in the history
An ingredient association is a way to be able to eager load
associated records of essence classes. The EssencePicture for example
`belongs_to :picture`.

In able to eager load we create an `belongs_to :ingredient_association`
for you and add an ActiveRecord hack that skips eager loading on records
not having such relation.

That way we can load a page and include all elements, their contents,
essences and their associated records as well.

This should speed up admin pages with large element lists and API calls
to pages with many elements a lot.
  • Loading branch information
tvdeyen committed Nov 19, 2019
1 parent a87adf2 commit 6f74fea
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 7 deletions.
10 changes: 7 additions & 3 deletions app/models/alchemy/essence_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ class EssencePage < BaseRecord

acts_as_essence(
ingredient_column: :page,
preview_text_method: :name
preview_text_method: :name,
belongs_to: {
class_name: 'Alchemy::Page',
foreign_key: :page_id,
inverse_of: :essence_pages,
optional: true
}
)

belongs_to :page, class_name: 'Alchemy::Page', optional: true

def ingredient=(page)
case page
when PAGE_ID
Expand Down
8 changes: 6 additions & 2 deletions app/models/alchemy/essence_picture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@

module Alchemy
class EssencePicture < BaseRecord
acts_as_essence ingredient_column: 'picture'
acts_as_essence ingredient_column: :picture, belongs_to: {
class_name: 'Alchemy::Picture',
foreign_key: :picture_id,
inverse_of: :essence_pictures,
optional: true
}

belongs_to :picture, optional: true
delegate :image_file_width, :image_file_height, :image_file, to: :picture
before_save :fix_crop_values
before_save :replace_newlines
Expand Down
6 changes: 5 additions & 1 deletion app/models/alchemy/picture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ class Picture < BaseRecord
include Alchemy::Picture::Transformations
include Alchemy::Picture::Url

has_many :essence_pictures, class_name: 'Alchemy::EssencePicture', foreign_key: 'picture_id'
has_many :essence_pictures,
class_name: 'Alchemy::EssencePicture',
foreign_key: 'picture_id',
inverse_of: :ingredient_association

has_many :contents, through: :essence_pictures
has_many :elements, through: :contents
has_many :pages, through: :elements
Expand Down
40 changes: 39 additions & 1 deletion lib/alchemy/essence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
require 'active_record'

module Alchemy #:nodoc:
# A bogus association that skips eager loading for essences not having an ingredient association
class IngredientAssociation < ActiveRecord::Associations::BelongsToAssociation
# Skip eager loading if called by Rails' preloader
def klass
if caller.any? { |line| line =~ /preloader\.rb/ }
nil
else
super
end
end
end

module Essence #:nodoc:
def self.included(base)
base.extend(ClassMethods)
Expand Down Expand Up @@ -31,6 +43,8 @@ def acts_as_essence(options = {})
ingredient_column: 'body'
}.update(options)

@_classes_with_ingredient_association ||= []

class_eval <<-RUBY, __FILE__, __LINE__ + 1
attr_writer :validation_errors
include Alchemy::Essence::InstanceMethods
Expand Down Expand Up @@ -66,8 +80,31 @@ def preview_text_column
'#{configuration[:preview_text_column] || configuration[:ingredient_column]}'
end
RUBY

if configuration[:belongs_to]
class_eval <<-RUBY, __FILE__, __LINE__ + 1
belongs_to :ingredient_association, #{configuration[:belongs_to]}
alias_method :#{configuration[:ingredient_column]}, :ingredient_association
alias_method :#{configuration[:ingredient_column]}=, :ingredient_association=
RUBY

@_classes_with_ingredient_association << self
end
end

# Overwrite ActiveRecords method to return a bogus association class that skips eager loading
# for essence classes that do not have an ingredient association
def _reflect_on_association(name)
if name == :ingredient_association && !in?(@_classes_with_ingredient_association)
OpenStruct.new(association_class: Alchemy::IngredientAssociation)
else
super
end
end

private

# Register the current class as has_many association on +Alchemy::Page+ and +Alchemy::Element+ models
def register_as_essence_association!
klass_name = model_name.to_s
Expand Down Expand Up @@ -231,4 +268,5 @@ def has_tinymce?
end
end
end
ActiveRecord::Base.class_eval { include Alchemy::Essence } if defined?(Alchemy::Essence)

ActiveRecord::Base.include(Alchemy::Essence)
12 changes: 12 additions & 0 deletions lib/alchemy/test_support/essence_shared_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
let(:content) { Alchemy::Content.new(name: 'foo') }
let(:content_definition) { {'name' => 'foo'} }

describe 'eager loading' do
before do
2.times { described_class.create! }
end

it 'does not throw error if eager loaded' do
expect {
described_class.all.includes(:ingredient_association).to_a
}.to_not raise_error
end
end

it "touches the content after update" do
element = FactoryBot.create(:alchemy_element)
content = FactoryBot.create(:alchemy_content, element: element, essence: essence, essence_type: essence.class.name)
Expand Down
9 changes: 9 additions & 0 deletions spec/models/alchemy/essence_page_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
let(:ingredient_value) { page }
end

describe 'eager loading' do
let!(:essence_pages) { create_list(:alchemy_essence_page, 2) }

it 'eager loads pages' do
essences = described_class.all.includes(:ingredient_association)
expect(essences[0].association(:ingredient_association)).to be_loaded
end
end

describe 'ingredient=' do
subject(:ingredient) { essence.page }

Expand Down
9 changes: 9 additions & 0 deletions spec/models/alchemy/essence_picture_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ module Alchemy
let(:ingredient_value) { Picture.new }
end

describe 'eager loading' do
let!(:essence_pictures) { create_list(:alchemy_essence_picture, 2) }

it 'eager loads pictures' do
essences = described_class.all.includes(:ingredient_association)
expect(essences[0].association(:ingredient_association)).to be_loaded
end
end

it_behaves_like "has image transformations" do
let(:picture) { build_stubbed(:alchemy_essence_picture) }
end
Expand Down

0 comments on commit 6f74fea

Please sign in to comment.