-
Notifications
You must be signed in to change notification settings - Fork 420
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
Support for polymorphic associations #64
Conversation
Add missing attribute in README example
d325bc9
to
2e8afd4
Compare
@@ -26,6 +26,21 @@ 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) | |||
{ id: record.id.to_s, type: record.class.name.underscore.to_sym } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vovimayhem Thanks for tackling this! I'm wondering if record.class.name.underscore.to_sym
is making assumptions about what the type should be: it must be determined by the class name, it must be underscored, etc. It seems like the canonical value for this record type would be on the serializer itself, e.g. PersonSerializer#record_type
. If the serializer does not exist, it could potentially fall back to the underscored class name.
It looks like the primitives for accessing other serializers are built in... something like compute_serializer_key(record.class.name).constantize
. It still makes the assumption that the serializer name can be inferred from the record class, but there needs to be some sort of convention for a polymorphic lookup...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... What I can see is that the most brutal performance gains this gem is by reducing the assumptions/choices to be made at serializing time, and passing it instead to the serializer definition time... hence why we have here options such as belongs_to :owner, record_type: :user
(here we're assuming the object type is user
, so we don't waste time figuring out the serializer to use at serializing time).
We can't do that with a non-homogeneous collection (i.e. polymorphic association) because there's no way to know exactly which object types are in a collection at definition time...
...but may I suggest something in the middle: We may know some (if not all) of the types we expect to see in a collection. That way, if the type of object in a collection is known, we won't need to figure the record_type/serializer out (that record.class.name.underscore.to_sym
).
I'll give that a try
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thanks for determining the performance bottleneck.
I offered a suggestion in #49 that I think could apply nicely here as well. Can we define a class method for determining the record type, given the record instance? For example:
def self.polymorphic_record_type(record)
...
end
The default implementation can use the same methodology for determining the default record type (and potentially even cache the results as key/value pairs rather than calculating them every time), but it can be overridden for those (rare?) cases where you have a polymorphic association whose type is not the default. It seems like this solution would provide sensible defaults without sacrificing performance, and also have the ability to easily override when the situation requires it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm gonna take a look at this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm deffinitely going to take a look at this... thanks for the idea!
@vovimayhem Looks awesome in general, thanks a lot. I'm wondering why the speed factor changed so much. The implementation looks solid, and nothing jumps out as a performance problem, but that much of a performance differential would seem to indicate there is something that can be optimized. Have you tried to isolate where the performance change might be occurring? Is it perhaps |
@christophersansone I felt like cheating with this one: class GroupSerializer
include FastJsonapi::ObjectSerializer
set_type :group
attributes :name
has_many :groupees, polymorphic: { Person => :person, Group => :group }
end Notice the I'm not tackling the root problem with this, tho. But, the speed factor is back to 25 :) |
929533a
to
0c70d7c
Compare
{ id: record.id.to_s, type: (record_type || record.class.name.underscore.to_sym) } | ||
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 |
There was a problem hiding this comment.
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...
@shishirmk back to 25x, now it looks good. I added the Finally, I followed @christophersansone tip and did a memoization (caching) of the object classes & record types for unexpected record types. I've got a question: Should we keep the |
@vovimayhem Nice work! I think if we had the class method for determining the type, we could just make |
@christophersansone On a second thought, having a However, I'll move the memoization logic to it's own method... ...actually there is a lot of code that may be split in smaller parts throughout the gem! |
This is awesome work @vovimayhem. @christophersansone thank you for helping and explaining more about the performance tests. Do you mind sharing the benchmark numbers for with this branch for 1000 records with and without polymorphic relationships |
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it
@shishirmk Updated the performance specs to include the jsonapi-rb benchmarks... Without polymorphic relationships (original case):Serialize to JSON string 1000 records
Serialize to Ruby Hash 1000 records
With polymorphic has_many (new case):Serialize to JSON string 1000 with polymorphic has_many
Serialize to Ruby Hash 1000 with polymorphic has_many
|
@vovimayhem Thank you for running the benchmarks. |
@vovimayhem Forgot to mention. Do you mind updating the readme with a section about how to set up polymorphic associations in the serializer class?. |
No prob |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One comment on how belongs_to works.
return ids_hash( | ||
record.public_send(relationship[:id_method_name]), | ||
relationship[:record_type] | ||
) unless polymorphic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, thanks so much for this PR. I'd really like to use this feature in a project, but I think the implementation here should change a bit in the case of polymorphic belongs_to.
type_method_name
should be inferred directly from the object_method_name
in the case of a polymorphic association since this value should be saved directly on the record. Something along the lines of this might make that work (I have yet to test this):
type_name = polymorphic ? record.public_send("#{relationship[:object_method_name]}_type") : relationship[:record_type]
return ids_hash(
record.public_send(relationship[:id_method_name]),
type_name,
) if relationship[:relationship_type] == :belongs_to || !polymorphic
This is in opposition to the implementation here of always referring to the record.class
and sending it to id_hash_from_record
in the case of a belongs_to
association. I believe this would save a database query. It would also save having to define a map when your belongs_to
relationship is different than the underlying record's class name. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I tested this out locally and messed it up a little. I'm going to edit my original code sample above for something that worked for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type_name = polymorphic ? record.public_send("#{relationship[:object_method_name]}_type") : relationship[:record_type]
# could be re-written for readability as:
type_name = if polymorphic
type_method_name = "#{relationship[:object_method_name]}_type"
record.public_send(type_method_name)
else
relationship[:record_type]
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Side note: Instead of constructing the type_name
here, you could also define it within the fetch_polymorphic_option
method call.
* 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
This PR addresses #37
It provides support for polymorphic associations. I expect some conversation and/or change requests to be made during review :)
Note that I also added performance benchmarks for polymorphic cases, and the speed_factor is not 25x, but rather 5 :( I don't know if 25x is a hard goal or not. The factor for the other cases remains unchanged.
This implementation iterates over the associated collection's objects, and maps out the ID and the object type whenever it's a
has_many
. I wonder if reading an already-extracted "dictionary" would increase performance, although I'm sure that would be transferring the performance problem to somewhere else.