From 6f74feaf18544e1fae8d01eba387c91eba7553bd Mon Sep 17 00:00:00 2001 From: Thomas von Deyen Date: Tue, 19 Nov 2019 19:16:50 +0100 Subject: [PATCH] Add ingredient associations 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. --- app/models/alchemy/essence_page.rb | 10 +++-- app/models/alchemy/essence_picture.rb | 8 +++- app/models/alchemy/picture.rb | 6 ++- lib/alchemy/essence.rb | 40 ++++++++++++++++++- .../test_support/essence_shared_examples.rb | 12 ++++++ spec/models/alchemy/essence_page_spec.rb | 9 +++++ spec/models/alchemy/essence_picture_spec.rb | 9 +++++ 7 files changed, 87 insertions(+), 7 deletions(-) diff --git a/app/models/alchemy/essence_page.rb b/app/models/alchemy/essence_page.rb index c5b083372a..ad369cd975 100644 --- a/app/models/alchemy/essence_page.rb +++ b/app/models/alchemy/essence_page.rb @@ -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 diff --git a/app/models/alchemy/essence_picture.rb b/app/models/alchemy/essence_picture.rb index 611c4fe6c7..013b088797 100644 --- a/app/models/alchemy/essence_picture.rb +++ b/app/models/alchemy/essence_picture.rb @@ -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 diff --git a/app/models/alchemy/picture.rb b/app/models/alchemy/picture.rb index 9248d44b1c..c9c51644e9 100644 --- a/app/models/alchemy/picture.rb +++ b/app/models/alchemy/picture.rb @@ -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 diff --git a/lib/alchemy/essence.rb b/lib/alchemy/essence.rb index ed472b5926..92e0c6e655 100644 --- a/lib/alchemy/essence.rb +++ b/lib/alchemy/essence.rb @@ -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) @@ -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 @@ -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 @@ -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) diff --git a/lib/alchemy/test_support/essence_shared_examples.rb b/lib/alchemy/test_support/essence_shared_examples.rb index 8a53a38917..e1a2af7c30 100644 --- a/lib/alchemy/test_support/essence_shared_examples.rb +++ b/lib/alchemy/test_support/essence_shared_examples.rb @@ -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) diff --git a/spec/models/alchemy/essence_page_spec.rb b/spec/models/alchemy/essence_page_spec.rb index a320bc0a0b..0606e2a88c 100644 --- a/spec/models/alchemy/essence_page_spec.rb +++ b/spec/models/alchemy/essence_page_spec.rb @@ -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 } diff --git a/spec/models/alchemy/essence_picture_spec.rb b/spec/models/alchemy/essence_picture_spec.rb index cec5801fc5..24694cae50 100644 --- a/spec/models/alchemy/essence_picture_spec.rb +++ b/spec/models/alchemy/essence_picture_spec.rb @@ -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