Skip to content

Commit 6d516c2

Browse files
vovimayhemshishirmk
authored andcommitted
Support for polymorphic associations (#64)
* add hash benchmarking to performance tests * Add missing attribute in README example * Disable GC before doing performance test * Enable oj to AM for fair benchmark test * Support for polymorphic associations * Optional dictionary for polymorphic associations * Added polymorphic record types memoization * Updated performance tests for polymorphic examples to include jsonapi-rb
1 parent d4b6216 commit 6d516c2

7 files changed

Lines changed: 403 additions & 5 deletions

File tree

lib/fast_jsonapi/object_serializer.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,8 @@ def has_many(relationship_name, options = {})
177177
object_method_name: options[:object_method_name] || name,
178178
serializer: compute_serializer_name(serializer_key),
179179
relationship_type: :has_many,
180-
cached: options[:cached] || false
180+
cached: options[:cached] || false,
181+
polymorphic: fetch_polymorphic_option(options)
181182
}
182183
add_relationship(name, relationship)
183184
end
@@ -195,7 +196,8 @@ def belongs_to(relationship_name, options = {})
195196
object_method_name: options[:object_method_name] || name,
196197
serializer: compute_serializer_name(serializer_key),
197198
relationship_type: :belongs_to,
198-
cached: options[:cached] || true
199+
cached: options[:cached] || true,
200+
polymorphic: fetch_polymorphic_option(options)
199201
})
200202
end
201203

@@ -212,7 +214,8 @@ def has_one(relationship_name, options = {})
212214
object_method_name: options[:object_method_name] || name,
213215
serializer: compute_serializer_name(serializer_key),
214216
relationship_type: :has_one,
215-
cached: options[:cached] || false
217+
cached: options[:cached] || false,
218+
polymorphic: fetch_polymorphic_option(options)
216219
})
217220
end
218221

@@ -222,6 +225,13 @@ def compute_serializer_name(serializer_key)
222225
return (namespace + serializer_name).to_sym if namespace.present?
223226
(serializer_key.to_s.classify + 'Serializer').to_sym
224227
end
228+
229+
def fetch_polymorphic_option(options)
230+
option = options[:polymorphic]
231+
return false unless option.present?
232+
return option if option.respond_to? :keys
233+
{}
234+
end
225235
end
226236
end
227237
end

lib/fast_jsonapi/serialization_core.rb

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ def ids_hash(ids, record_type)
2727
id_hash(ids, record_type) # ids variable is just a single id here
2828
end
2929

30+
def id_hash_from_record(record, record_types)
31+
# memoize the record type within the record_types dictionary, then assigning to record_type:
32+
record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
33+
{ id: record.id.to_s, type: record_type }
34+
end
35+
36+
def ids_hash_from_record_and_relationship(record, relationship)
37+
polymorphic = relationship[:polymorphic]
38+
39+
return ids_hash(
40+
record.public_send(relationship[:id_method_name]),
41+
relationship[:record_type]
42+
) unless polymorphic
43+
44+
object_method_name = relationship.fetch(:object_method_name, relationship[:name])
45+
return unless associated_object = record.send(object_method_name)
46+
47+
return associated_object.map do |object|
48+
id_hash_from_record object, polymorphic
49+
end if associated_object.respond_to? :map
50+
51+
id_hash_from_record associated_object, polymorphic
52+
end
53+
3054
def attributes_hash(record)
3155
attributes_to_serialize.each_with_object({}) do |(key, method_name), attr_hash|
3256
attr_hash[key] = record.public_send(method_name)
@@ -42,7 +66,7 @@ def relationships_hash(record, relationships = nil)
4266
record_type = relationship[:record_type]
4367
empty_case = relationship[:relationship_type] == :has_many ? [] : nil
4468
hash[name] = {
45-
data: ids_hash(record.public_send(id_method_name), record_type) || empty_case
69+
data: ids_hash_from_record_and_relationship(record, relationship) || empty_case
4670
}
4771
end
4872
end

spec/lib/object_serializer_performance_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
include_context 'ams movie class'
66
include_context 'jsonapi movie class'
77

8+
include_context 'group class'
9+
include_context 'ams group class'
10+
include_context 'jsonapi group class'
11+
812
before(:all) { GC.disable }
913
after(:all) { GC.enable }
1014

@@ -118,4 +122,28 @@ def run_json_benchmark(message, movie_count, our_serializer, ams_serializer, jso
118122
end
119123
end
120124
end
125+
126+
context 'when comparing with AMS 0.10.x and with polymorphic has_many' do
127+
[1, 25, 250, 1000].each do |group_count|
128+
speed_factor = 25
129+
it "should serialize #{group_count} records at least #{speed_factor} times faster than AMS" do
130+
ams_groups = build_ams_groups(group_count)
131+
groups = build_groups(group_count)
132+
options = {}
133+
our_serializer = GroupSerializer.new(groups, options)
134+
ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_groups)
135+
jsonapi_serializer = JSONAPISerializerB.new(jsonapi_groups)
136+
137+
message = "Serialize to JSON string #{group_count} with polymorphic has_many"
138+
our_json, ams_json, jsonapi_json = run_json_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer)
139+
140+
message = "Serialize to Ruby Hash #{group_count} with polymorphic has_many"
141+
run_hash_benchmark(message, group_count, our_serializer, ams_serializer, jsonapi_serializer)
142+
143+
expect(our_json.length).to eq ams_json.length
144+
expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
145+
expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times
146+
end
147+
end
148+
end
121149
end

spec/lib/serialization_core_spec.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
describe FastJsonapi::ObjectSerializer do
44
include_context "movie class"
5+
include_context 'group class'
56

67
context 'when testing class methods of serialization core' do
78
it 'returns correct hash when id_hash is called' do
@@ -16,6 +17,12 @@
1617
expect(result_hash).to be nil
1718
end
1819

20+
it 'returns the correct hash when ids_hash_from_record_and_relationship is called for a polymorphic association' do
21+
relationship = { name: :groupees, relationship_type: :has_many, polymorphic: {} }
22+
results = GroupSerializer.send :ids_hash_from_record_and_relationship, group, relationship
23+
expect(results).to include({ id: "1", type: :person }, { id: "2", type: :group })
24+
end
25+
1926
it 'returns correct hash when ids_hash is called' do
2027
inputs = [{ids: %w(1 2 3), record_type: :movie}, {ids: %w(x y z), record_type: 'person'}]
2128
inputs.each do |hash|
@@ -80,5 +87,4 @@
8087
expect(included_records.size).to eq 3
8188
end
8289
end
83-
8490
end
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
RSpec.shared_context 'ams group class' do
2+
before(:context) do
3+
# models
4+
class AMSPerson < ActiveModelSerializers::Model
5+
attr_accessor :id, :first_name, :last_name
6+
end
7+
8+
class AMSGroup < ActiveModelSerializers::Model
9+
attr_accessor :id, :name, :groupees
10+
end
11+
12+
# serializers
13+
class AMSPersonSerializer < ActiveModel::Serializer
14+
type 'person'
15+
attributes :first_name, :last_name
16+
end
17+
18+
class AMSGroupSerializer < ActiveModel::Serializer
19+
type 'group'
20+
attributes :name
21+
has_many :groupees
22+
end
23+
end
24+
25+
after(:context) do
26+
classes_to_remove = %i[AMSPerson AMSGroup AMSPersonSerializer AMSGroupSerializer]
27+
classes_to_remove.each do |klass_name|
28+
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
29+
end
30+
end
31+
32+
let(:ams_groups) do
33+
group_count = 0
34+
person_count = 0
35+
3.times.map do |i|
36+
group = AMSGroup.new
37+
group.id = group_count + 1
38+
group.name = "Test Group #{group.id}"
39+
group_count = group.id
40+
41+
person = AMSPerson.new
42+
person.id = person_count + 1
43+
person.last_name = "Last Name #{person.id}"
44+
person.first_name = "First Name #{person.id}"
45+
person_count = person.id
46+
47+
child_group = AMSGroup.new
48+
child_group.id = group_count + 1
49+
child_group.name = "Test Group #{child_group.id}"
50+
group_count = child_group.id
51+
52+
group.groupees = [person, child_group]
53+
group
54+
end
55+
end
56+
57+
let(:ams_person) do
58+
ams_person = AMSPerson.new
59+
ams_person.id = 3
60+
ams_person
61+
end
62+
63+
def build_ams_groups(count)
64+
group_count = 0
65+
person_count = 0
66+
count.times.map do |i|
67+
group = AMSGroup.new
68+
group.id = group_count + 1
69+
group.name = "Test Group #{group.id}"
70+
group_count = group.id
71+
72+
person = AMSPerson.new
73+
person.id = person_count + 1
74+
person.last_name = "Last Name #{person.id}"
75+
person.first_name = "First Name #{person.id}"
76+
person_count = person.id
77+
78+
child_group = AMSGroup.new
79+
child_group.id = group_count + 1
80+
child_group.name = "Test Group #{child_group.id}"
81+
group_count = child_group.id
82+
83+
group.groupees = [person, child_group]
84+
group
85+
end
86+
end
87+
end
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
RSpec.shared_context 'group class' do
2+
3+
# Person, Group Classes and serializers
4+
before(:context) do
5+
# models
6+
class Person
7+
attr_accessor :id, :first_name, :last_name
8+
end
9+
10+
class Group
11+
attr_accessor :id, :name, :groupees # Let's assume groupees can be Person or Group objects
12+
end
13+
14+
# serializers
15+
class PersonSerializer
16+
include FastJsonapi::ObjectSerializer
17+
set_type :person
18+
attributes :first_name, :last_name
19+
end
20+
21+
class GroupSerializer
22+
include FastJsonapi::ObjectSerializer
23+
set_type :group
24+
attributes :name
25+
has_many :groupees, polymorphic: true
26+
end
27+
end
28+
29+
30+
# Namespaced PersonSerializer
31+
before(:context) do
32+
# namespaced model stub
33+
module AppName
34+
module V1
35+
class PersonSerializer
36+
include FastJsonapi::ObjectSerializer
37+
# to test if compute_serializer_name works
38+
end
39+
end
40+
end
41+
end
42+
43+
# Movie and Actor struct
44+
before(:context) do
45+
PersonStruct = Struct.new(
46+
:id, :first_name, :last_name
47+
)
48+
49+
GroupStruct = Struct.new(
50+
:id, :name, :groupees, :groupee_ids
51+
)
52+
end
53+
54+
after(:context) do
55+
classes_to_remove = %i[
56+
Person
57+
PersonSerializer
58+
Group
59+
GroupSerializer
60+
AppName::V1::PersonSerializer
61+
PersonStruct
62+
GroupStruct
63+
]
64+
classes_to_remove.each do |klass_name|
65+
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
66+
end
67+
end
68+
69+
let(:group_struct) do
70+
group = GroupStruct.new
71+
group[:id] = 1
72+
group[:name] = 'Group 1'
73+
group[:groupees] = []
74+
75+
person = PersonStruct.new
76+
person[:id] = 1
77+
person[:last_name] = "Last Name 1"
78+
person[:first_name] = "First Name 1"
79+
80+
child_group = GroupStruct.new
81+
child_group[:id] = 2
82+
child_group[:name] = 'Group 2'
83+
84+
group.groupees = [person, child_group]
85+
group
86+
end
87+
88+
let(:group) do
89+
group = Group.new
90+
group.id = 1
91+
group.name = 'Group 1'
92+
93+
person = Person.new
94+
person.id = 1
95+
person.last_name = "Last Name 1"
96+
person.first_name = "First Name 1"
97+
98+
child_group = Group.new
99+
child_group.id = 2
100+
child_group.name = 'Group 2'
101+
102+
group.groupees = [person, child_group]
103+
group
104+
end
105+
106+
def build_groups(count)
107+
group_count = 0
108+
person_count = 0
109+
110+
count.times.map do |i|
111+
group = Group.new
112+
group.id = group_count + 1
113+
group.name = "Test Group #{group.id}"
114+
group_count = group.id
115+
116+
person = Person.new
117+
person.id = person_count + 1
118+
person.last_name = "Last Name #{person.id}"
119+
person.first_name = "First Name #{person.id}"
120+
person_count = person.id
121+
122+
child_group = Group.new
123+
child_group.id = group_count + 1
124+
child_group.name = "Test Group #{child_group.id}"
125+
group_count = child_group.id
126+
127+
group.groupees = [person, child_group]
128+
group
129+
end
130+
end
131+
end

0 commit comments

Comments
 (0)