-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Fix through association nested preloads #4341
Fix through association nested preloads #4341
Conversation
Awesome. This looks good to me but it feels it could be slightly backwards compatible, right? Previously we were always preloading through (as it force: true) and now it make it behave like (force: false), right? |
Yeah definitely it changes the behaviour. In addition to the change of force behaviour it also changes some of the nested preload behaviour. For example, if you had this: schema "posts" do
has_many :comments_authors, through: [:comments, :authors]
has_many :comments
end
schema "comments" do
belongs_to :author
end
schema "authors" do
has_one :permalink
has_many :posts
end
post = Repo.preload(post, :comments_authors)
post = Repo.preload(post, [comments_authors: [:permalink], comments: [authors: [:posts]]]) In the second preload the Now that you mention forcing, if we go ahead with this then I should also take it into account and allow the overwrite if force is true. |
Maybe an alternative solution here is to know if the association has been loaded because it happens via Repo.preload or if it happens via query (i.e. preload+join)? If the preload is filled because of a preload+join, then we can apply the heuristic in this patch, because the user is explicitly saying they want their given struct to win (i.e. it is force: false). If it is a Repo.preload, then can keep the current semantics of filling in the association chain? |
That's a really good idea. What do you think about the last commit? If you like it, I can write the integration tests. |
Wait a second, you mentioned that in this case we will no longer have authors and posts in comments or something.
But wouldn't we be able to know if comments_authors was set before hand (before calling preload) or set by preload itself? In this case, we could:
Would this be able to reduce the backwards incompatible changes without adding a new option? |
Sorry I probably am not understanding properly. Would this be the same as my original commit or something different? |
Sorry, I am telling you my conclusions but not my thought process. There are two concerns in this PR, as far as I know:
What I am saying is that, we only need to change the behaviour of |
Oh I see what you mean now. There is a case to change the behaviour for post = Repo.preload(post, [non_through_assoc: [:nested_assoc]])
post = Repo.preload(post, :non_through_assoc) In the case above, post = Repo.preload(post, [through_assoc: [:nested_assoc]])
post = Repo.preload(post, through_assoc) I don't personally see a reason to expect non-through and through assocs to behave differently here. But to maintain backwards compatibility I thought it might be worth giving them an option instead of outright changing the behaviour. |
Actually I misspoke a bit for this example: post = Repo.preload(post, [through_assoc: [:nested_assoc]])
post = Repo.preload(post, through_assoc) This will still have schema "posts" do
has_many :through_assoc, through: [:through1, through2]
has_one :through1
end Then if you do this, the nested assoc would disappear: post = Repo.preload(post, [through_assoc: [:nested_assoc]])
post = Ecto.reset_fields(post, [:through1])
post = Repo.preload(post, through_assoc) I think what you said makes sense then. As long as you are using |
This reverts commit ddf1c06.
WDYT? |
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.
Looks great, I have only some minor nits.
To make sure we are on the same page. This should be fully backwards compatible. The only chance in behaviour is when you preload a through with joins and regular assocs, which was broken anyway (i.e. it was the original bug report)?
Co-authored-by: José Valim <[email protected]>
Yeah exactly. The following are backwards compatible:
The following is not backwards compatible:
In this case, the differences are:
Before I merge, could you confirm ^ is ok with you as well? Thanks! |
Yup, it is all good to me. Beautifully done! |
Closes #4112
I didn't add tests yet because I wanted to see what you thought about the solution presented here and the alternative solution. This one is really tricky.
Background
When using
Repo.preload
, it is possible for through associations to lose their nested preloads like thisWhat happens is this:
Solution
The solution in this PR is pretty simple. If the through association is already loaded then we preload directly into it. The same way we do for embeds. When determining whether to put the association chain into the through field, we simply don't put the association chain into the through field if it has already been loaded. However, the chain is still calculated and put into the "regular" association fields.
The drawback of this method is that we are adding additional queries for the loaded through associations. Taking into account nested associations, this could end up with a decent amount of extra queries.
Alternative Solution
The alternative solution is to not preload the association chain if the through association has been loaded. I tried this out first but it gets quite complex to the point where I'm not sure there is any sane way to do it. There are two main issues:
Depending on which of these 3 through fields are already loaded, we would have to keep track of which sub-preloads at each nested layer are to be kept.
It also means we have to carry over information several layers deep. For example, a field on
post
might control whethermanager
is preloaded. But by the time we get to manager we've lost all the post information because we preload recursively.