Skip to content

Commit 85ee5f9

Browse files
committed
cache values for defined properties and relationships [performance optimization]
1 parent f97ce89 commit 85ee5f9

File tree

8 files changed

+192
-48
lines changed

8 files changed

+192
-48
lines changed

lib/json_api_client/associations/belongs_to.rb

-9
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
module JsonApiClient
22
module Associations
33
module BelongsTo
4-
extend ActiveSupport::Concern
5-
6-
module ClassMethods
7-
def belongs_to(attr_name, options = {})
8-
# self.associations = self.associations + [HasOne::Association.new(attr_name, self, options)]
9-
self.associations += [BelongsTo::Association.new(attr_name, self, options)]
10-
end
11-
end
12-
134
class Association < BaseAssociation
145
include Helpers::URI
156
def param

lib/json_api_client/associations/has_many.rb

-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
module JsonApiClient
22
module Associations
33
module HasMany
4-
extend ActiveSupport::Concern
5-
6-
module ClassMethods
7-
def has_many(attr_name, options = {})
8-
self.associations = self.associations + [HasMany::Association.new(attr_name, self, options)]
9-
end
10-
end
11-
124
class Association < BaseAssociation
135
end
146
end

lib/json_api_client/associations/has_one.rb

-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
module JsonApiClient
22
module Associations
33
module HasOne
4-
extend ActiveSupport::Concern
5-
6-
module ClassMethods
7-
def has_one(attr_name, options = {})
8-
self.associations += [HasOne::Association.new(attr_name, self, options)]
9-
end
10-
end
11-
124
class Association < BaseAssociation
135
def from_result_set(result_set)
146
result_set.first

lib/json_api_client/helpers.rb

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ module Helpers
44
autoload :Dirty, 'json_api_client/helpers/dirty'
55
autoload :DynamicAttributes, 'json_api_client/helpers/dynamic_attributes'
66
autoload :URI, 'json_api_client/helpers/uri'
7+
autoload :Associatable, 'json_api_client/helpers/associatable'
78
end
89
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
module JsonApiClient
2+
module Helpers
3+
module Associatable
4+
extend ActiveSupport::Concern
5+
6+
included do
7+
class_attribute :associations, instance_accessor: false
8+
self.associations = []
9+
attr_accessor :__cached_associations
10+
end
11+
12+
module ClassMethods
13+
def _define_association(attr_name, association_klass, options = {})
14+
attr_name = attr_name.to_sym
15+
association = association_klass.new(attr_name, self, options)
16+
self.associations += [association]
17+
18+
define_method(attr_name) do
19+
_cached_relationship(attr_name) do
20+
relationship_definition = relationship_definition_for(attr_name)
21+
relationship_data_for attr_name, relationship_definition, association
22+
end
23+
end
24+
25+
define_method("#{attr_name}=") do |value|
26+
_clear_cached_relationship(attr_name)
27+
relationships.public_send("#{attr_name}=", value)
28+
end
29+
end
30+
31+
def belongs_to(attr_name, options = {})
32+
_define_association(attr_name, JsonApiClient::Associations::BelongsTo::Association, options)
33+
end
34+
35+
def has_many(attr_name, options = {})
36+
_define_association(attr_name, JsonApiClient::Associations::HasMany::Association, options)
37+
end
38+
39+
def has_one(attr_name, options = {})
40+
_define_association(attr_name, JsonApiClient::Associations::HasOne::Association, options)
41+
end
42+
end
43+
44+
def _cached_associations
45+
self.__cached_associations ||= {}
46+
end
47+
48+
def _clear_cached_relationships
49+
self.__cached_associations = {}
50+
end
51+
52+
def _clear_cached_relationship(attr_name)
53+
_cached_associations.delete(attr_name)
54+
end
55+
56+
def _cached_relationship(attr_name)
57+
return _cached_associations[attr_name] if _cached_associations.has_key?(attr_name)
58+
_cached_associations[attr_name] = yield
59+
end
60+
61+
end
62+
end
63+
end

lib/json_api_client/helpers/dynamic_attributes.rb

+1-6
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,7 @@ def has_attribute?(attr_name)
3939

4040
def method_missing(method, *args, &block)
4141
if has_attribute?(method)
42-
self.class.class_eval do
43-
define_method(method) do
44-
attributes[method]
45-
end
46-
end
47-
return send(method)
42+
return attributes[method]
4843
end
4944

5045
normalized_method = safe_key_formatter.unformat(method.to_s)

lib/json_api_client/resource.rb

+35-17
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Resource
1212

1313
include Helpers::DynamicAttributes
1414
include Helpers::Dirty
15+
include Helpers::Associatable
1516

1617
attr_accessor :last_result_set,
1718
:links,
@@ -29,7 +30,6 @@ class Resource
2930
:relationship_linker,
3031
:read_only_attributes,
3132
:requestor_class,
32-
:associations,
3333
:json_key_format,
3434
:route_format,
3535
:request_params_class,
@@ -45,7 +45,6 @@ class Resource
4545
self.relationship_linker = Relationships::Relations
4646
self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
4747
self.requestor_class = Query::Requestor
48-
self.associations = []
4948
self.request_params_class = RequestParams
5049
self.keep_request_params = false
5150

@@ -55,10 +54,6 @@ class Resource
5554
#:underscored_route, :camelized_route, :dasherized_route, or custom
5655
self.route_format = :underscored_route
5756

58-
include Associations::BelongsTo
59-
include Associations::HasMany
60-
include Associations::HasOne
61-
6257
class << self
6358
extend Forwardable
6459
def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
@@ -250,6 +245,12 @@ def member_endpoint(name, options = {})
250245
# @option options [Symbol] :default The default value for the property
251246
def property(name, options = {})
252247
schema.add(name, options)
248+
define_method(name) do
249+
attributes[name]
250+
end
251+
define_method("#{name}=") do |value|
252+
set_attribute(name, value)
253+
end
253254
end
254255

255256
# Declare multiple properties with the same optional options
@@ -428,8 +429,10 @@ def save
428429
self.attributes = updated.attributes
429430
self.links.attributes = updated.links.attributes
430431
self.relationships.attributes = updated.relationships.attributes
432+
self.relationships.last_result_set = last_result_set
431433
clear_changes_information
432434
self.relationships.clear_changes_information
435+
_clear_cached_relationships
433436
end
434437
true
435438
end
@@ -445,6 +448,9 @@ def destroy
445448
false
446449
else
447450
self.attributes.clear
451+
self.relationships.attributes.clear
452+
self.relationships.last_result_set = nil
453+
_clear_cached_relationships
448454
true
449455
end
450456
end
@@ -480,27 +486,39 @@ def reset_request_select!(*resource_types)
480486

481487
protected
482488

483-
def method_missing(method, *args)
484-
association = association_for(method)
489+
def relationship_definition_for(name)
490+
relationships[name] if relationships && relationships.has_attribute?(name)
491+
end
485492

486-
return super unless association || (relationships && relationships.has_attribute?(method))
493+
def included_data_for(name, relationship_definition)
494+
last_result_set.included.data_for(name, relationship_definition)
495+
end
487496

488-
return nil unless relationship_definitions = relationships[method]
497+
def relationship_data_for(name, relationship_definition, association)
498+
return unless relationship_definition
489499

490500
# look in included data
491-
if relationship_definitions.key?("data")
492-
return last_result_set.included.data_for(method, relationship_definitions)
501+
if relationship_definition.key?("data")
502+
return included_data_for(name, relationship_definition)
493503
end
494504

495-
if association = association_for(method)
496-
# look for a defined relationship url
497-
if relationship_definitions["links"] && url = relationship_definitions["links"]["related"]
498-
return association.data(url)
499-
end
505+
url = relationship_definition["links"]["related"]
506+
if relationship_definition["links"] && url
507+
return association.data(url)
500508
end
509+
501510
nil
502511
end
503512

513+
def method_missing(method, *args)
514+
association = association_for(method)
515+
relationship_definition = relationship_definition_for(method)
516+
517+
return super unless association || relationship_definition
518+
519+
relationship_data_for(method, relationship_definition, association)
520+
end
521+
504522
def respond_to_missing?(symbol, include_all = false)
505523
return true if relationships && relationships.has_attribute?(symbol)
506524
return true if association_for(symbol)

test/unit/updating_test.rb

+92
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,49 @@ def test_can_update_single_relationship
212212
assert article.save
213213
end
214214

215+
def test_can_update_single_relationship_via_setter
216+
articles = Article.find(1)
217+
article = articles.first
218+
219+
stub_request(:patch, "http://example.com/articles/1")
220+
.with(headers: {content_type: "application/vnd.api+json", accept: "application/vnd.api+json"}, body: {
221+
data: {
222+
id: "1",
223+
type: "articles",
224+
relationships: {
225+
author: {
226+
data: {
227+
type: "people",
228+
id: "1"
229+
}
230+
}
231+
},
232+
attributes: {}
233+
}
234+
}.to_json)
235+
.to_return(headers: {status: 200, content_type: "application/vnd.api+json"}, body: {
236+
data: {
237+
type: "articles",
238+
id: "1",
239+
attributes: {
240+
title: "Rails is Omakase"
241+
},
242+
relationships: {
243+
author: {
244+
links: {
245+
self: "/articles/1/links/author",
246+
related: "/articles/1/author",
247+
},
248+
data: { type: "people", id: "1" }
249+
}
250+
}
251+
}
252+
}.to_json)
253+
254+
article.author = Person.new(id: "1")
255+
assert article.save
256+
end
257+
215258
def test_can_update_single_relationship_with_all_attributes_dirty
216259
articles = Article.find(1)
217260
article = articles.first
@@ -313,6 +356,55 @@ def test_can_update_has_many_relationships
313356
assert article.save
314357
end
315358

359+
def test_can_update_has_many_relationships_via_setter
360+
articles = Article.find(1)
361+
article = articles.first
362+
363+
stub_request(:patch, "http://example.com/articles/1")
364+
.with(headers: {content_type: "application/vnd.api+json", accept: "application/vnd.api+json"}, body: {
365+
data: {
366+
id: "1",
367+
type: "articles",
368+
relationships: {
369+
comments: {
370+
data: [{
371+
type: "comments",
372+
id: "2"
373+
},{
374+
type: "comments",
375+
id: "3"
376+
}]
377+
}
378+
},
379+
attributes: {}
380+
}
381+
}.to_json)
382+
.to_return(headers: {status: 200, content_type: "application/vnd.api+json"}, body: {
383+
data: {
384+
id: "1",
385+
type: "articles",
386+
relationships: {
387+
author: {
388+
links: {
389+
self: "/articles/1/links/author",
390+
related: "/articles/1/author",
391+
},
392+
data: { type: "people", id: "1" }
393+
}
394+
},
395+
attributes: {
396+
title: "Rails is Omakase"
397+
}
398+
}
399+
}.to_json)
400+
401+
article.comments = [
402+
Comment.new(id: "2"),
403+
Comment.new(id: "3")
404+
]
405+
assert article.save
406+
end
407+
316408
def test_can_update_has_many_relationships_with_all_attributes_dirty
317409
articles = Article.find(1)
318410
article = articles.first

0 commit comments

Comments
 (0)