Skip to content

Support for polymorphic associations #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ $ bundle install

```ruby
class Movie
attr_accessor :id, :name, :year, :actor_ids, :owner_id
attr_accessor :id, :name, :year, :actor_ids, :owner_id, :movie_type_id
end
```

Expand Down
16 changes: 13 additions & 3 deletions lib/fast_jsonapi/object_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ def has_many(relationship_name, options = {})
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :has_many,
cached: options[:cached] || false
cached: options[:cached] || false,
polymorphic: fetch_polymorphic_option(options)
}
add_relationship(name, relationship)
end
Expand All @@ -169,7 +170,8 @@ def belongs_to(relationship_name, options = {})
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :belongs_to,
cached: options[:cached] || true
cached: options[:cached] || true,
polymorphic: fetch_polymorphic_option(options)
}
add_relationship(name, relationship)
end
Expand All @@ -191,7 +193,8 @@ def has_one(relationship_name, options = {})
object_method_name: options[:object_method_name] || name,
serializer: compute_serializer_name(serializer_key),
relationship_type: :has_one,
cached: options[:cached] || false
cached: options[:cached] || false,
polymorphic: fetch_polymorphic_option(options)
}
add_relationship(name, relationship)
end
Expand All @@ -202,6 +205,13 @@ def compute_serializer_name(serializer_key)
return (namespace + serializer_name).to_sym if namespace.present?
(serializer_key.to_s.classify + 'Serializer').to_sym
end

def fetch_polymorphic_option(options)
option = options[:polymorphic]
return false unless option.present?
return option if option.respond_to? :keys
{}
end
end
end
end
26 changes: 23 additions & 3 deletions lib/fast_jsonapi/serialization_core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ def ids_hash(ids, record_type)
id_hash(ids, record_type) # ids variable is just a single id here
end

def id_hash_from_record(record, record_types)
# memoize the record type within the record_types dictionary, then assigning to record_type:
record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@christophersansone Not yet a method like you suggested (that's something I can refactor, tho), but caching like a boss...

{ id: record.id.to_s, type: record_type }
end

def ids_hash_from_record_and_relationship(record, relationship)
polymorphic = relationship[:polymorphic]

return ids_hash(record.send(relationship[:id_method_name]), relationship[:record_type]) \
unless polymorphic

object_method_name = relationship.fetch(:object_method_name, relationship[:name])
return unless associated_object = record.send(object_method_name)

return associated_object.map do |object|
id_hash_from_record object, polymorphic
end if associated_object.respond_to? :map

id_hash_from_record associated_object, polymorphic
end

def attributes_hash(record)
attributes_hash = {}
attributes_to_serialize.each do |key, method_name|
Expand All @@ -40,11 +62,9 @@ def relationships_hash(record, relationships = nil)

relationships.each do |_k, relationship|
name = relationship[:key]
id_method_name = relationship[:id_method_name]
record_type = relationship[:record_type]
empty_case = relationship[:relationship_type] == :has_many ? [] : nil
relationships_hash[name] = {
data: ids_hash(record.send(id_method_name), record_type) || empty_case
data: ids_hash_from_record_and_relationship(record, relationship) || empty_case
}
end
relationships_hash
Expand Down
74 changes: 61 additions & 13 deletions spec/lib/object_serializer_performance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
describe FastJsonapi::ObjectSerializer, performance: true do
include_context 'movie class'
include_context 'ams movie class'
include_context 'group class'
include_context 'ams group class'

before(:all) { GC.disable }
after(:all) { GC.enable }

context 'when testing performance of serialization' do
it 'should create a hash of 1000 records in less than 50 ms' do
Expand Down Expand Up @@ -34,29 +39,48 @@
end
end

def print_stats(count, ams_time, our_time)
def print_stats(message, count, ams_time, our_time)
format = '%-15s %-10s %s'
puts ''
puts message
puts format(format, 'Serializer', 'Records', 'Time')
puts format(format, 'AMS serializer', count, ams_time.round(2).to_s + ' ms')
puts format(format, 'Fast serializer', count, our_time.round(2).to_s + ' ms')
end

def run_hash_benchmark(message, movie_count, our_serializer, ams_serializer)
our_time = Benchmark.measure { our_hash = our_serializer.serializable_hash }.real * 1000
ams_time = Benchmark.measure { ams_hash = ams_serializer.as_json }.real * 1000
print_stats(message, movie_count, ams_time, our_time)
end

def run_json_benchmark(message, movie_count, our_serializer, ams_serializer)
our_json = nil
ams_json = nil
our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000
ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000
print_stats(message, movie_count, ams_time, our_time)
return our_json, ams_json
end

context 'when comparing with AMS 0.10.x' do
[1, 25, 250, 1000].each do |movie_count|
speed_factor = 25
it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do
ams_movies = build_ams_movies(movie_count)
movies = build_movies(movie_count)
our_json = nil
ams_json = nil
our_serializer = MovieSerializer.new(movies)
ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies)
our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000
ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000
print_stats(movie_count, ams_time, our_time)

message = "Serialize to JSON string #{movie_count} records"
our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer)

message = "Serialize to Ruby Hash #{movie_count} records"
run_hash_benchmark(message, movie_count, our_serializer, ams_serializer)

expect(our_json.length).to eq ams_json.length
expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times
end
end
end
Expand All @@ -67,20 +91,44 @@ def print_stats(count, ams_time, our_time)
it "should serialize #{movie_count} records atleast #{speed_factor} times faster than AMS" do
ams_movies = build_ams_movies(movie_count)
movies = build_movies(movie_count)
our_json = nil
ams_json = nil

options = {}
options[:meta] = { total: movie_count }
options[:include] = [:actors, :movie_type]

our_serializer = MovieSerializer.new(movies, options)
ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_movies, include: options[:include], meta: options[:meta])
our_time = Benchmark.measure { our_json = our_serializer.serialized_json }.real * 1000
ams_time = Benchmark.measure { ams_json = ams_serializer.to_json }.real * 1000
print_stats(movie_count, ams_time, our_time)

message = "Serialize to JSON string #{movie_count} with includes and meta"
our_json, ams_json = run_json_benchmark(message, movie_count, our_serializer, ams_serializer)

message = "Serialize to Ruby Hash #{movie_count} with includes and meta"
run_hash_benchmark(message, movie_count, our_serializer, ams_serializer)

expect(our_json.length).to eq ams_json.length
expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times
end
end
end

context 'when comparing with AMS 0.10.x and with polymorphic has_many' do
[1, 25, 250, 1000].each do |group_count|
speed_factor = 25
it "should serialize #{group_count} records at least #{speed_factor} times faster than AMS" do
ams_groups = build_ams_groups(group_count)
groups = build_groups(group_count)
options = {}
our_serializer = GroupSerializer.new(groups, options)
ams_serializer = ActiveModelSerializers::SerializableResource.new(ams_groups)

message = "Serialize to JSON string #{group_count} with polymorphic has_many"
our_json, ams_json = run_json_benchmark(message, group_count, our_serializer, ams_serializer)

message = "Serialize to Ruby Hash #{group_count} with polymorphic has_many"
run_hash_benchmark(message, group_count, our_serializer, ams_serializer)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add the jsonapi_serializer to the benchmark. Refer to line 80 in https://github.com/Netflix/fast_jsonapi/blob/dev/spec/lib/object_serializer_performance_spec.rb

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it


expect(our_json.length).to eq ams_json.length
expect { our_serializer.serialized_json }.to perform_faster_than { ams_serializer.to_json }.at_least(speed_factor).times
expect { our_serializer.serializable_hash }.to perform_faster_than { ams_serializer.as_json }.at_least(speed_factor).times
end
end
end
Expand Down
8 changes: 7 additions & 1 deletion spec/lib/serialization_core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

describe FastJsonapi::ObjectSerializer do
include_context "movie class"
include_context 'group class'

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

it 'returns the correct hash when ids_hash_from_record_and_relationship is called for a polymorphic association' do
relationship = { name: :groupees, relationship_type: :has_many, polymorphic: {} }
results = GroupSerializer.send :ids_hash_from_record_and_relationship, group, relationship
expect(results).to include({ id: "1", type: :person }, { id: "2", type: :group })
end

it 'returns correct hash when ids_hash is called' do
inputs = [{ids: %w(1 2 3), record_type: :movie}, {ids: %w(x y z), record_type: 'person'}]
inputs.each do |hash|
Expand Down Expand Up @@ -80,5 +87,4 @@
expect(included_records.size).to eq 3
end
end

end
8 changes: 8 additions & 0 deletions spec/shared/contexts/ams_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ class AMSMovieTypeSerializer < ActiveModel::Serializer
type 'movie_type'
attributes :name
end

class AMSRole < ActiveModelSerializers::Model
attr_accessor :id, :name, :actor, :target
end

class AMSGroup < ActiveModelSerializers::Model
attr_accessor :id, :name
end
end

after(:context) do
Expand Down
87 changes: 87 additions & 0 deletions spec/shared/contexts/ams_group_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
RSpec.shared_context 'ams group class' do
before(:context) do
# models
class AMSPerson < ActiveModelSerializers::Model
attr_accessor :id, :first_name, :last_name
end

class AMSGroup < ActiveModelSerializers::Model
attr_accessor :id, :name, :groupees
end

# serializers
class AMSPersonSerializer < ActiveModel::Serializer
type 'person'
attributes :first_name, :last_name
end

class AMSGroupSerializer < ActiveModel::Serializer
type 'group'
attributes :name
has_many :groupees
end
end

after(:context) do
classes_to_remove = %i[AMSPerson AMSGroup AMSPersonSerializer AMSGroupSerializer]
classes_to_remove.each do |klass_name|
Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name)
end
end

let(:ams_groups) do
group_count = 0
person_count = 0
3.times.map do |i|
group = AMSGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id

person = AMSPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id

child_group = AMSGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id

group.groupees = [person, child_group]
group
end
end

let(:ams_person) do
ams_person = AMSPerson.new
ams_person.id = 3
ams_person
end

def build_ams_groups(count)
group_count = 0
person_count = 0
count.times.map do |i|
group = AMSGroup.new
group.id = group_count + 1
group.name = "Test Group #{group.id}"
group_count = group.id

person = AMSPerson.new
person.id = person_count + 1
person.last_name = "Last Name #{person.id}"
person.first_name = "First Name #{person.id}"
person_count = person.id

child_group = AMSGroup.new
child_group.id = group_count + 1
child_group.name = "Test Group #{child_group.id}"
group_count = child_group.id

group.groupees = [person, child_group]
group
end
end
end
Loading