Skip to content

Commit

Permalink
cache values for defined properties and relationships [performance op…
Browse files Browse the repository at this point in the history
…timization]
  • Loading branch information
senid231 committed Oct 5, 2018
1 parent dbdb0f6 commit 3048ea3
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 51 deletions.
9 changes: 0 additions & 9 deletions lib/json_api_client/associations/belongs_to.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
module JsonApiClient
module Associations
module BelongsTo
extend ActiveSupport::Concern

module ClassMethods
def belongs_to(attr_name, options = {})
# self.associations = self.associations + [HasOne::Association.new(attr_name, self, options)]
self.associations += [BelongsTo::Association.new(attr_name, self, options)]
end
end

class Association < BaseAssociation
include Helpers::URI
def param
Expand Down
8 changes: 0 additions & 8 deletions lib/json_api_client/associations/has_many.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
module JsonApiClient
module Associations
module HasMany
extend ActiveSupport::Concern

module ClassMethods
def has_many(attr_name, options = {})
self.associations = self.associations + [HasMany::Association.new(attr_name, self, options)]
end
end

class Association < BaseAssociation
end
end
Expand Down
8 changes: 0 additions & 8 deletions lib/json_api_client/associations/has_one.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
module JsonApiClient
module Associations
module HasOne
extend ActiveSupport::Concern

module ClassMethods
def has_one(attr_name, options = {})
self.associations += [HasOne::Association.new(attr_name, self, options)]
end
end

class Association < BaseAssociation
def from_result_set(result_set)
result_set.first
Expand Down
1 change: 1 addition & 0 deletions lib/json_api_client/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module Helpers
autoload :Dirty, 'json_api_client/helpers/dirty'
autoload :DynamicAttributes, 'json_api_client/helpers/dynamic_attributes'
autoload :URI, 'json_api_client/helpers/uri'
autoload :Associatable, 'json_api_client/helpers/associatable'
end
end
64 changes: 64 additions & 0 deletions lib/json_api_client/helpers/associatable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
module JsonApiClient
module Helpers
module Associatable
extend ActiveSupport::Concern

included do
class_attribute :associations, instance_accessor: false
self.associations = []
attr_accessor :__cached_associations
end

module ClassMethods
def _define_association(attr_name, association_klass, options = {})
attr_name = attr_name.to_sym
association = association_klass.new(attr_name, self, options)
self.associations += [association]

define_method(attr_name) do
_cached_relationship(attr_name) do
relationship_definition = relationship_definition_for(attr_name)
return unless relationship_definition
relationship_data_for(attr_name, relationship_definition)
end
end

define_method("#{attr_name}=") do |value|
_clear_cached_relationship(attr_name)
relationships.public_send("#{attr_name}=", value)
end
end

def belongs_to(attr_name, options = {})
_define_association(attr_name, JsonApiClient::Associations::BelongsTo::Association, options)
end

def has_many(attr_name, options = {})
_define_association(attr_name, JsonApiClient::Associations::HasMany::Association, options)
end

def has_one(attr_name, options = {})
_define_association(attr_name, JsonApiClient::Associations::HasOne::Association, options)
end
end

def _cached_associations
self.__cached_associations ||= {}
end

def _clear_cached_relationships
self.__cached_associations = {}
end

def _clear_cached_relationship(attr_name)
_cached_associations.delete(attr_name)
end

def _cached_relationship(attr_name)
return _cached_associations[attr_name] if _cached_associations.has_key?(attr_name)
_cached_associations[attr_name] = yield
end

end
end
end
7 changes: 1 addition & 6 deletions lib/json_api_client/helpers/dynamic_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,7 @@ def has_attribute?(attr_name)

def method_missing(method, *args, &block)
if has_attribute?(method)
self.class.class_eval do
define_method(method) do
attributes[method]
end
end
return send(method)
return attributes[method]
end

normalized_method = safe_key_formatter.unformat(method.to_s)
Expand Down
51 changes: 33 additions & 18 deletions lib/json_api_client/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Resource

include Helpers::DynamicAttributes
include Helpers::Dirty
include Helpers::Associatable

attr_accessor :last_result_set,
:links,
Expand All @@ -29,7 +30,6 @@ class Resource
:relationship_linker,
:read_only_attributes,
:requestor_class,
:associations,
:json_key_format,
:route_format,
:request_params_class,
Expand All @@ -47,7 +47,6 @@ class Resource
self.relationship_linker = Relationships::Relations
self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
self.requestor_class = Query::Requestor
self.associations = []
self.request_params_class = RequestParams
self.keep_request_params = false
self.add_defaults_to_changes = false
Expand All @@ -58,10 +57,6 @@ class Resource
#:underscored_route, :camelized_route, :dasherized_route, or custom
self.route_format = :underscored_route

include Associations::BelongsTo
include Associations::HasMany
include Associations::HasOne

class << self
extend Forwardable
def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
Expand Down Expand Up @@ -253,6 +248,12 @@ def member_endpoint(name, options = {})
# @option options [Symbol] :default The default value for the property
def property(name, options = {})
schema.add(name, options)
define_method(name) do
attributes[name]
end
define_method("#{name}=") do |value|
set_attribute(name, value)
end
end

# Declare multiple properties with the same optional options
Expand Down Expand Up @@ -430,8 +431,10 @@ def save
self.attributes = updated.attributes
self.links.attributes = updated.links.attributes
self.relationships.attributes = updated.relationships.attributes
self.relationships.last_result_set = last_result_set
clear_changes_information
self.relationships.clear_changes_information
_clear_cached_relationships
end
true
end
Expand All @@ -447,6 +450,9 @@ def destroy
false
else
self.attributes.clear
self.relationships.attributes.clear
self.relationships.last_result_set = nil
_clear_cached_relationships
true
end
end
Expand Down Expand Up @@ -491,27 +497,36 @@ def setup_default_properties
end
end

def method_missing(method, *args)
association = association_for(method)

return super unless association || (relationships && relationships.has_attribute?(method))
def relationship_definition_for(name)
relationships[name] if relationships && relationships.has_attribute?(name)
end

return nil unless relationship_definitions = relationships[method]
def included_data_for(name, relationship_definition)
last_result_set.included.data_for(name, relationship_definition)
end

def relationship_data_for(name, relationship_definition)
# look in included data
if relationship_definitions.key?("data")
return last_result_set.included.data_for(method, relationship_definitions)
if relationship_definition.key?("data")
return included_data_for(name, relationship_definition)
end

if association = association_for(method)
# look for a defined relationship url
if relationship_definitions["links"] && url = relationship_definitions["links"]["related"]
return association.data(url)
end
url = relationship_definition["links"]["related"]
if relationship_definition["links"] && url
return association_for(name).data(url)
end

nil
end

def method_missing(method, *args)
relationship_definition = relationship_definition_for(method)

return super unless relationship_definition

relationship_data_for(method, relationship_definition)
end

def respond_to_missing?(symbol, include_all = false)
return true if relationships && relationships.has_attribute?(symbol)
return true if association_for(symbol)
Expand Down
4 changes: 2 additions & 2 deletions test/unit/resource_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ def test_formatted_key_accessors
def test_associations_as_params
article = Article.new(foo: 'bar', 'author' => {'type' => 'authors', 'id' => 1})
assert_equal(article.foo, 'bar')
assert_equal(article.attributes['author']['type'], 'authors')
assert_equal(article.attributes['author']['id'], 1)
assert_equal({'type' => 'authors', 'id' => 1}, article.relationships.author)
assert article.relationships.attribute_changed?(:author)
end

def test_default_params_overrideable
Expand Down
92 changes: 92 additions & 0 deletions test/unit/updating_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,49 @@ def test_can_update_single_relationship
assert article.save
end

def test_can_update_single_relationship_via_setter
articles = Article.find(1)
article = articles.first

stub_request(:patch, "http://example.com/articles/1")
.with(headers: {content_type: "application/vnd.api+json", accept: "application/vnd.api+json"}, body: {
data: {
id: "1",
type: "articles",
relationships: {
author: {
data: {
type: "people",
id: "1"
}
}
},
attributes: {}
}
}.to_json)
.to_return(headers: {status: 200, content_type: "application/vnd.api+json"}, body: {
data: {
type: "articles",
id: "1",
attributes: {
title: "Rails is Omakase"
},
relationships: {
author: {
links: {
self: "/articles/1/links/author",
related: "/articles/1/author",
},
data: { type: "people", id: "1" }
}
}
}
}.to_json)

article.author = Person.new(id: "1")
assert article.save
end

def test_can_update_single_relationship_with_all_attributes_dirty
articles = Article.find(1)
article = articles.first
Expand Down Expand Up @@ -313,6 +356,55 @@ def test_can_update_has_many_relationships
assert article.save
end

def test_can_update_has_many_relationships_via_setter
articles = Article.find(1)
article = articles.first

stub_request(:patch, "http://example.com/articles/1")
.with(headers: {content_type: "application/vnd.api+json", accept: "application/vnd.api+json"}, body: {
data: {
id: "1",
type: "articles",
relationships: {
comments: {
data: [{
type: "comments",
id: "2"
},{
type: "comments",
id: "3"
}]
}
},
attributes: {}
}
}.to_json)
.to_return(headers: {status: 200, content_type: "application/vnd.api+json"}, body: {
data: {
id: "1",
type: "articles",
relationships: {
author: {
links: {
self: "/articles/1/links/author",
related: "/articles/1/author",
},
data: { type: "people", id: "1" }
}
},
attributes: {
title: "Rails is Omakase"
}
}
}.to_json)

article.comments = [
Comment.new(id: "2"),
Comment.new(id: "3")
]
assert article.save
end

def test_can_update_has_many_relationships_with_all_attributes_dirty
articles = Article.find(1)
article = articles.first
Expand Down

0 comments on commit 3048ea3

Please sign in to comment.