From 867af0212a68ab014975b5882756c6dd6206900c Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:06:26 -0500 Subject: [PATCH 1/9] Reduce number of string allocations in LinkBuilder --- lib/jsonapi/link_builder.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/jsonapi/link_builder.rb b/lib/jsonapi/link_builder.rb index d78f414e..23b94a4d 100644 --- a/lib/jsonapi/link_builder.rb +++ b/lib/jsonapi/link_builder.rb @@ -49,8 +49,8 @@ def query_link(query_params) def relationships_related_link(source, relationship, query_params = {}) if relationship._routed - url = "#{ self_link(source) }/#{ route_for_relationship(relationship) }" - url = "#{ url }?#{ query_params.to_query }" if query_params.present? + url = +"#{ self_link(source) }/#{ route_for_relationship(relationship) }" + url << "?#{ query_params.to_query }" if query_params.present? url else if JSONAPI.configuration.warn_on_missing_routes && !relationship._warned_missing_route @@ -129,18 +129,14 @@ def resources_path(source_klass) @_resources_path[source_klass] ||= formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s) end - def resource_path(source) + def resource_url(source) if source.class.singleton? - resources_path(source.class) + "#{ base_url }#{ engine_mount_point }#{ resources_path(source.class) }" else - "#{resources_path(source.class)}/#{source.id}" + "#{ base_url }#{ engine_mount_point }#{resources_path(source.class)}/#{source.id}" end end - def resource_url(source) - "#{ base_url }#{ engine_mount_point }#{ resource_path(source) }" - end - def route_for_relationship(relationship) format_route(relationship.name) end From 755142c0e0da00eb53a4338bf66e5cc4b803e1df Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:26:39 -0500 Subject: [PATCH 2/9] Consistently access `include_related` --- lib/jsonapi/resource_tree.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi/resource_tree.rb b/lib/jsonapi/resource_tree.rb index 5cbd830e..d26f1264 100644 --- a/lib/jsonapi/resource_tree.rb +++ b/lib/jsonapi/resource_tree.rb @@ -68,13 +68,13 @@ def add_resource(resource, include_related) private def init_included_relationships(fragment, include_related) - include_related && include_related.each_key do |relationship_name| + include_related&.each_key do |relationship_name| fragment.initialize_related(relationship_name) end end def load_included(resource_klass, source_resource_tree, include_related, options) - include_related.try(:each_key) do |key| + include_related&.each_key do |key| relationship = resource_klass._relationship(key) relationship_name = relationship.name.to_sym From 3121ccb4e4c3553c84f3142f9e92e2b84d67c005 Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:27:12 -0500 Subject: [PATCH 3/9] Remove unused class variable --- lib/jsonapi/resource_tree.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/jsonapi/resource_tree.rb b/lib/jsonapi/resource_tree.rb index d26f1264..1e93d3fb 100644 --- a/lib/jsonapi/resource_tree.rb +++ b/lib/jsonapi/resource_tree.rb @@ -159,7 +159,6 @@ def initialize(parent_relationship, source_resource_tree) @related_resource_trees ||= {} @parent_relationship = parent_relationship - @parent_relationship_name = parent_relationship.name.to_sym @source_resource_tree = source_resource_tree end From a83920e3cc734c376925c4e100892f762350aee1 Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:28:41 -0500 Subject: [PATCH 4/9] Cache `id` after retrieving it from the model --- lib/jsonapi/resource_common.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/resource_common.rb b/lib/jsonapi/resource_common.rb index 5c10f07b..2bc8b10e 100644 --- a/lib/jsonapi/resource_common.rb +++ b/lib/jsonapi/resource_common.rb @@ -39,7 +39,7 @@ def _model end def id - _model.public_send(self.class._primary_key) + @id ||= _model.public_send(self.class._primary_key) end def identity From f7c58cc7bb3b2b0ea55aed41cab5f577e0360c6f Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:30:09 -0500 Subject: [PATCH 5/9] Cache `module_path` --- lib/jsonapi/resource_common.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/jsonapi/resource_common.rb b/lib/jsonapi/resource_common.rb index 2bc8b10e..69f1e0c6 100644 --- a/lib/jsonapi/resource_common.rb +++ b/lib/jsonapi/resource_common.rb @@ -1132,11 +1132,11 @@ def _has_sort?(sorting) end def module_path - if name == 'JSONAPI::Resource' - '' - else - name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : '' - end + @module_path ||= if name == 'JSONAPI::Resource' + '' + else + name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : '' + end end def default_sort From aad31ba0d8994d71e9d4275425142308bee7c0ca Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:40:50 -0500 Subject: [PATCH 6/9] Cache resource_klass_for and resource_type_for --- lib/jsonapi/resource_common.rb | 42 +++++++++++++++++++++-------- test/controllers/controller_test.rb | 4 +++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/jsonapi/resource_common.rb b/lib/jsonapi/resource_common.rb index 69f1e0c6..df15a17e 100644 --- a/lib/jsonapi/resource_common.rb +++ b/lib/jsonapi/resource_common.rb @@ -511,6 +511,9 @@ def inherited(subclass) subclass._warned_missing_route = false subclass._clear_cached_attribute_options + subclass._clear_cached_resource_types_for_model + subclass._clear_cached_resource_klasses_for_type + subclass._clear_fields_cache subclass._resource_retrieval_strategy_loaded = @_resource_retrieval_strategy_loaded @@ -533,15 +536,19 @@ def rebuild_relationships(relationships) end def resource_klass_for(type) + @_cached_resource_klasses_for_type ||= {} type = type.underscore - type_with_module = type.start_with?(module_path) ? type : module_path + type - resource_name = _resource_name_from_type(type_with_module) - resource = resource_name.safe_constantize if resource_name - if resource.nil? - fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)" + @_cached_resource_klasses_for_type.fetch(type) do + type_with_module = type.start_with?(module_path) ? type : module_path + type + + resource_name = _resource_name_from_type(type_with_module) + resource_klass = resource_name.safe_constantize if resource_name + if resource_klass.nil? + fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)" + end + @_cached_resource_klasses_for_type[type] = resource_klass end - resource end def resource_klass_for_model(model) @@ -553,11 +560,16 @@ def _resource_name_from_type(type) end def resource_type_for(model) - model_name = model.class.to_s.underscore - if _model_hints[model_name] - _model_hints[model_name] - else - model_name.rpartition('/').last + @_cached_resource_types_for_model.fetch(model.class) do + model_name = model.class.name.underscore + + resource_type = if _model_hints[model_name] + _model_hints[model_name] + else + model_name.rpartition('/').last + end + + @_cached_resource_types_for_model[model.class] = resource_type end end @@ -1218,6 +1230,14 @@ def _clear_cached_attribute_options @_cached_attribute_options = {} end + def _clear_cached_resource_types_for_model + @_cached_resource_types_for_model = {} + end + + def _clear_cached_resource_klasses_for_type + @_cached_resource_klasses_for_type = {} + end + def _clear_fields_cache @_fields_cache = nil end diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index 12e3cbcc..9f64db3d 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -4028,6 +4028,8 @@ def test_immutable_update_not_supported class Api::V7::ClientsControllerTest < ActionController::TestCase def test_get_namespaced_model_not_matching_resource_using_model_hint + Api::V7::ClientResource._clear_cached_resource_types_for_model + Api::V7::ClientResource._clear_cached_resource_klasses_for_type assert_cacheable_get :index assert_response :success assert_equal 'clients', json_response['data'][0]['type'] @@ -4037,6 +4039,8 @@ def test_get_namespaced_model_not_matching_resource_using_model_hint def test_get_namespaced_model_not_matching_resource_not_using_model_hint Api::V7::ClientResource._model_hints.delete('api/v7/customer') + Api::V7::ClientResource._clear_cached_resource_types_for_model + Api::V7::ClientResource._clear_cached_resource_klasses_for_type assert_cacheable_get :index assert_response :success assert_equal 'customers', json_response['data'][0]['type'] From 5cd779c5bf75009cc6eeade2cf2d300526156c04 Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:41:21 -0500 Subject: [PATCH 7/9] Remove no longer used method _setup_relationship --- lib/jsonapi/resource_common.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/jsonapi/resource_common.rb b/lib/jsonapi/resource_common.rb index df15a17e..19331345 100644 --- a/lib/jsonapi/resource_common.rb +++ b/lib/jsonapi/resource_common.rb @@ -1181,18 +1181,6 @@ def _add_relationship(klass, *attrs) end end - def _setup_relationship(klass, *attrs) - _clear_fields_cache - - options = attrs.extract_options! - options[:parent_resource] = self - - relationship_name = attrs[0].to_sym - check_duplicate_relationship_name(relationship_name) - - define_relationship_methods(relationship_name.to_sym, klass, options) - end - # ResourceBuilder methods def define_relationship_methods(relationship_name, relationship_klass, options) relationship = register_relationship( From c4eedf2f4b5a95a72e46bedbb9a976fe8f3c8ef0 Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Mon, 15 Jan 2024 11:48:40 -0500 Subject: [PATCH 8/9] Delete nil values without creating a new object --- lib/jsonapi/resource_common.rb | 4 ++-- lib/jsonapi/resource_serializer.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/jsonapi/resource_common.rb b/lib/jsonapi/resource_common.rb index 19331345..e104445f 100644 --- a/lib/jsonapi/resource_common.rb +++ b/lib/jsonapi/resource_common.rb @@ -927,11 +927,11 @@ def _has_attribute?(attr) end def _updatable_attributes - _attributes.map { |key, options| key unless options[:readonly] }.compact + _attributes.map { |key, options| key unless options[:readonly] }.delete_if {|v| v.nil? } end def _updatable_relationships - @_relationships.map { |key, relationship| key unless relationship.readonly? }.compact + @_relationships.map { |key, relationship| key unless relationship.readonly? }.delete_if {|v| v.nil? } end def _relationship(type) diff --git a/lib/jsonapi/resource_serializer.rb b/lib/jsonapi/resource_serializer.rb index 1e7d5102..6f876ffb 100644 --- a/lib/jsonapi/resource_serializer.rb +++ b/lib/jsonapi/resource_serializer.rb @@ -262,7 +262,7 @@ def links_hash(source) if !links.key?('self') && !source.class.exclude_link?(:self) links['self'] = link_builder.self_link(source) end - links.compact + links.delete_if {|k,v| v.nil? } end def custom_links_hash(source) @@ -340,7 +340,7 @@ def default_relationship_links(source, relationship) links = {} links['self'] = self_link(source, relationship) unless relationship.exclude_link?(:self) links['related'] = related_link(source, relationship) unless relationship.exclude_link?(:related) - links.compact + links.delete_if {|k,v| v.nil? } end def to_many_linkage(rids) From d7c78a2330daba03a4c8703392d17b6230f3824f Mon Sep 17 00:00:00 2001 From: lgebhardt Date: Tue, 16 Jan 2024 10:00:01 -0500 Subject: [PATCH 9/9] Rework resource naming for method caches --- lib/jsonapi/resource_common.rb | 47 ++++++++++++++++++----------- test/controllers/controller_test.rb | 8 ++--- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/jsonapi/resource_common.rb b/lib/jsonapi/resource_common.rb index e104445f..0cd29051 100644 --- a/lib/jsonapi/resource_common.rb +++ b/lib/jsonapi/resource_common.rb @@ -510,9 +510,9 @@ def inherited(subclass) subclass._routed = false subclass._warned_missing_route = false - subclass._clear_cached_attribute_options - subclass._clear_cached_resource_types_for_model - subclass._clear_cached_resource_klasses_for_type + subclass._attribute_options_cache = {} + subclass._model_class_to_resource_type_cache = {} + subclass._resource_type_to_class_cache = {} subclass._clear_fields_cache @@ -536,10 +536,10 @@ def rebuild_relationships(relationships) end def resource_klass_for(type) - @_cached_resource_klasses_for_type ||= {} + @_resource_type_to_class_cache ||= {} type = type.underscore - @_cached_resource_klasses_for_type.fetch(type) do + @_resource_type_to_class_cache.fetch(type) do type_with_module = type.start_with?(module_path) ? type : module_path + type resource_name = _resource_name_from_type(type_with_module) @@ -547,7 +547,7 @@ def resource_klass_for(type) if resource_klass.nil? fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)" end - @_cached_resource_klasses_for_type[type] = resource_klass + @_resource_type_to_class_cache[type] = resource_klass end end @@ -560,7 +560,7 @@ def _resource_name_from_type(type) end def resource_type_for(model) - @_cached_resource_types_for_model.fetch(model.class) do + @_model_class_to_resource_type_cache.fetch(model.class) do model_name = model.class.name.underscore resource_type = if _model_hints[model_name] @@ -569,13 +569,24 @@ def resource_type_for(model) model_name.rpartition('/').last end - @_cached_resource_types_for_model[model.class] = resource_type + @_model_class_to_resource_type_cache[model.class] = resource_type end end - attr_accessor :_attributes, :_relationships, :_type, :_model_hints, :_routed, :_warned_missing_route, + attr_accessor :_attributes, + :_relationships, + :_type, + :_model_hints, + :_routed, + :_warned_missing_route, :_resource_retrieval_strategy_loaded - attr_writer :_allowed_filters, :_paginator, :_allowed_sort + + attr_writer :_allowed_filters, + :_paginator, + :_allowed_sort, + :_model_class_to_resource_type_cache, + :_resource_type_to_class_cache, + :_attribute_options_cache def create(context) new(create_model, context) @@ -602,7 +613,7 @@ def attributes(*attrs) end def attribute(attribute_name, options = {}) - _clear_cached_attribute_options + _clear_attribute_options_cache _clear_fields_cache attr = attribute_name.to_sym @@ -915,7 +926,7 @@ def verify_relationship_filter(filter, raw, _context = nil) # quasi private class methods def _attribute_options(attr) - @_cached_attribute_options[attr] ||= default_attribute_options.merge(@_attributes[attr]) + @_attribute_options_cache[attr] ||= default_attribute_options.merge(@_attributes[attr]) end def _attribute_delegated_name(attr) @@ -1214,16 +1225,16 @@ def register_relationship(name, relationship_object) @_relationships[name] = relationship_object end - def _clear_cached_attribute_options - @_cached_attribute_options = {} + def _clear_attribute_options_cache + @_attribute_options_cache&.clear end - def _clear_cached_resource_types_for_model - @_cached_resource_types_for_model = {} + def _clear_model_to_resource_type_cache + @_model_class_to_resource_type_cache&.clear end - def _clear_cached_resource_klasses_for_type - @_cached_resource_klasses_for_type = {} + def _clear_resource_type_to_klass_cache + @_resource_type_to_class_cache&.clear end def _clear_fields_cache diff --git a/test/controllers/controller_test.rb b/test/controllers/controller_test.rb index 9f64db3d..2fe181d4 100644 --- a/test/controllers/controller_test.rb +++ b/test/controllers/controller_test.rb @@ -4028,8 +4028,8 @@ def test_immutable_update_not_supported class Api::V7::ClientsControllerTest < ActionController::TestCase def test_get_namespaced_model_not_matching_resource_using_model_hint - Api::V7::ClientResource._clear_cached_resource_types_for_model - Api::V7::ClientResource._clear_cached_resource_klasses_for_type + Api::V7::ClientResource._clear_model_to_resource_type_cache + Api::V7::ClientResource._clear_resource_type_to_klass_cache assert_cacheable_get :index assert_response :success assert_equal 'clients', json_response['data'][0]['type'] @@ -4039,8 +4039,8 @@ def test_get_namespaced_model_not_matching_resource_using_model_hint def test_get_namespaced_model_not_matching_resource_not_using_model_hint Api::V7::ClientResource._model_hints.delete('api/v7/customer') - Api::V7::ClientResource._clear_cached_resource_types_for_model - Api::V7::ClientResource._clear_cached_resource_klasses_for_type + Api::V7::ClientResource._clear_model_to_resource_type_cache + Api::V7::ClientResource._clear_resource_type_to_klass_cache assert_cacheable_get :index assert_response :success assert_equal 'customers', json_response['data'][0]['type']