-
Notifications
You must be signed in to change notification settings - Fork 148
Use serializer to determine relationship IDs when possible #155
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
base: master
Are you sure you want to change the base?
Use serializer to determine relationship IDs when possible #155
Conversation
2e1702a to
021b1e3
Compare
|
@tlconnor Thanks, agreed that something should happen here long-term. I wrote #45 to address it but couldn't get everyone behind that particular implementation. My concern is that this will be a change in behavior (and therefore a breaking change) for people using I'm thinking the right approach would be to remove the part in Last thing. When a relationship is being included for serialization, it's no problem to get the ID from the object and serializer. When it's not being included, it gets more complicated from a performance standpoint, because we don't necessarily want to fetch all the records / models just to get the IDs if it can be avoided. ActiveRecord has a shortcut where, if a There are many things to consider here if we are going to try to maintain performance and backward compatibility. Happy to assist you and @stas . Some basic pseudo-code might look something like this: def fetch_ids(record)
if object_block.nil?
# if an object block is specified, avoid these quick attempts in favor of fetching the objects below
# try the id_method_name that was specified by the developer in the relationship's properties
return record.send(id_method_name) if id_method_name.present? && record.respond_to?(id_method_name)
# try the conventional relationship_ids method name, e.g. author_ids or publisher_id
return record.send(relationship_id_method_name) if record.respond_to?(relationship_id_method_name)
end
if has_many?
# iterate over the objects and get the IDs
rel = fetch_associated_object(record)
if rel.respond_to?(:map)
return rel.map { |o| relationship_object_id_for(o) }
end
# not sure what else should happen if a has_many relationship does not respond to map ... error?
end
if belongs_to? || has_one?
# iterate over the objects and get the IDs
rel = fetch_associated_object(record)
return relationship_object_id_for(rel)
end
end
def relationship_object_id_for(rel)
if id_method_name.present?
return rel.send(id_method_name)
else
return serializer_for(rel).id_for(rel)
end
end
def relationship_id_method_name
@relationship_id_method_name ||= has_many? "#{name}_ids" : "#{name}_id"
endThe main point of uncertainty is, if the developer specifies |
ff80353 to
348b23f
Compare
|
Thanks @stas / @christophersansone for the feedback. I have updated this PR to make the following changes:
What this means is the existing behavior is now maintained, except that instead of we now use the serializer to look up the IDs. |
348b23f to
43fd2e5
Compare
The `Relationship` class currently has two possible methods for determining the ID for a relationship: 1. It can use the `serializer` associated with the relationship. 2. It has an option called `id_method_name`, where `id_method_name` is the name of a method on the parent record that can used to look up the ID of the related record without loading the related record. The `id_method_name` is not a reliable method of determining IDs for a couple of reasons: 1. A value is not always provided and the calculated default is not always accurate. 2. The parent doesn't necessarily always have a method for finding the related ID directly, and there is no guarantee that it will be performant. 3. If the method does not exist, the user will get a not very helpful error message. Since a serializer is always the source of truth for determining how a record gets serialized, it would generally make sense to use that where possible. This PR updates the `Relationship` class to use the serializer class to calculate IDs in cases where an `id_method_name` is not provided and the related record and serializer are both available.
43fd2e5 to
62d1861
Compare
| # This code path should be removed when `id_method_name` is replaced with `foreign_key` | ||
| # | ||
| if id_method_name | ||
| if has_many? && object.first.respond_to?(id_method_name) |
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.
Thanks for taking a stab at this, I think it's really close!
I'm a bit concerned about calling first here. If it's an Array, no big deal. If it's an ActiveRecord relationship, and the relationship has not been loaded yet, then first will call its own SELECT ... LIMIT 1 query. Then the full query of all records would follow when map is called. So you'd get two queries here, which we should avoid.
Also, what happens if the developer intends for id_method_name to be called on the child, but the relationship has no associated records? It will fall through to attempting the id_method_name on the parent, which may result in incorrect output.
I think we can solve for both this way:
if has_many?
if object.length > 0 && object.first.respond_to?(id_method_name)
return object.map { |item| item.public_send(id_method_name) }
else
return []
end
...
endAlso, what should happen if id_method_name is specified but neither the parent nor child respond to it? I tend to think it should throw an exception as opposed to falling through to the next attempts. If the developer specifies it, it should tell the developer if it's wrong.
| # explicit. | ||
| # | ||
| foreign_key = id_method_name | ||
| foreign_key ||= (has_many? ? "#{@name.to_s.singularize}_ids" : "#{@name}_id") |
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.
For performance, we should memoize this computation, as we may be iterating over hundreds of records or more, and it is constant once the relationship is defined. Either on init or as a memoized method.
@computed_id_method_name = has_many? ? "#{@name.to_s.singularize}_ids" : "#{@name}_id"|
@tlconnor 👏 Thanks for the updates! I added a couple comments, but in general I think it's as good as it can be if we go down this path. You definitely opened a can of worms though! My concern is that there still will be edge cases where it causes a breaking change: the order in which the process flows, the removal of the hardcoded @stas, how important is it to maintain perfect backward compatibility here, at least until a major version bump? Is there a new major release on the horizon soon? Ultimately, my opinion is to deprecate |
Good question. #141 is pretty much ready for a beta. I'm stuck finding some time to finish up the inline-docs and general documentation. And this issue is already handled fully (:crossed_fingers:) in the that branch. Saying that, I really don't know if I have time and energy to introduce an intermediary significant release and deal with the requests for that. Honestly, I'd push for #141 as a v.3-beta release, even the way it is now, close a bunch of issues it handles and keep the good work on top of it. But I also don't want to sound like 💩 and not consider the contributions from the community like what @tlconnor is doing (where I apologise for not taking time to look into yet 🙈)... |
|
@stas Sweet, I just did a quick review of #141 and see that @tlconnor Thanks again for tackling this... twice! Because of the potential for breaking changes with certain use cases, I'm thinking #141 with the major version bump is the better option. Are you able to solve your specific issue today using the current version of this library, either with |
I am not blocked, I've added my 2 fixes to my app as monkey patches for now. The main area of concern for me is the non-rails use cases where relationships aren't backed by ActiveRecord. I'm happy to drop this change in favor of #141, as long as some sort of forward progress is being made there. If you release it as a beta I would be happy to switch to the beta version. @stas @christophersansone would one of you also be able to look at #154? It is a simpler bug fix that fixes another blocking bug for me. |
The
id_method_nameis not a reliable method of determining IDs for relationships. It has built in assumptions about primary key / foreign key naming and can result inundefined methoderrors if the assumptions are incorrect.Since a serializer is always the source of truth for determining how a record gets serialized, it would generally make sense to use that where possible.
This PR updates the
Relationshipclass to use the serializer class to calculate IDs if anid_method_nameis not provided, or if it does not map to an existing foreign key / primary key.Closes #64
What is the current behavior?
object_blockis provided, the object is loaded and a method will be called to retrieve the ID using eitherid_method_name, or"id". This will raise an exception if the method cannot be found.object_blockis not provided, a foreign key is calculated based onid_method_nameand ActiveRecord conventions. The foreign key method is then caled on the parent record. This will raise an exception if the method cannot be found.What is the new behavior?
id_method_nameis provided, it will attempt to use that as a foreign keyid_method_nameis not provided, or does not correspond to a method on the parent record, a foreign key is guessed based on ActiveRecord conventions.id_method_nameis used as a possible method for look up the ID from the child object.id_method_namedoes not map to a method available on the child record, the serializer for the relationship is used to look up the IDs.Checklist
Please make sure the following requirements are complete:
features)