-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
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
The engine should not attempt to force synchronization between relations and corresponding foreign keys #19788
Comments
@rob006 @samdark can you provide some insights on this issue. Have not got time to go indepth in the code (I will find time to do that) but reading OP's explanation and example it seems like the best way is to remove the behavior and document how to achieve it. It seems to me (from the explanations) that it is hard to get "the right solution" and in this case my opinion would be offload that from Yii and tell users how to achieve it, if they want to do it. What are you guys thinking, since you were involved in feature in discussion? @yiisoft/reviewers Any thoughts on this? |
I think that most critics can be summarized "in some cases it does not work", and removing this feature would change it to "it never works", which is not an improvement. Also I think that some "holes", like ignoring this for eager loading or Behavior of As for performance implications - I would rather se some benchmarks on real examples, as overhead really looks negligible. |
More like "in MOST cases it does not work". Making design contradictory and confusing to say the least. Can you formulate what guarantee engine currently provides with regard of unsetting or not unsetting relations?
It is a SIGNIFICANT improvement, because the design becomes CONSISTENT, CLEAR and SIMPLE: "Engine guarantees: once set ActiveRecord relations are never automatically or silently changed/unset by the engine". That gives user full control and ability to solve all cases I mentioned in my original post.
I don't think this was intentional. I think it was a usual case when someone implemented feature for his own isolated use-case (which is lazy loading) without regards of how other parts of the engine he never used will be affected (or how overall design of the engine will be affected). Here is a recent example of the same shortsightedness: #13567 - a change to Besides, as I illustrated by my examples, these "holes" are insufficient to have full control in all cases. The only way to have such control is to make user responsible for the data synchronization.
You talk about fixing #19785 as if that is the ONLY current place where engine fails to enforce synchronization, what about the rest of them (which are plenty)? Also, if you read my original post carefully, you may have noticed that inconsistent state
Here is a simple example on memory usage: As for the performance it may only be negligable in comparison with everything else ActiveRecord does (like the amount of other processing inside |
I think that general rule of thumb is: if you rely on magic lazy loading, magic reload of these magically loaded relations also comes into play. If you explicitly configure loading relation either by eager loading or by using
I'm not sure which case you have in mind (you have a lot of examples in first post, some of them contradicts each other), but if your workflow relies on model being in inconsistent state, this is more of a problem with workflow than with AR.
AFAIK it is not really true - PHP uses COW for arrays, so memory consumption increases only with actual changes to model.
To be fair, I don't think that #19785 needs to be fixed. If you asked model to refresh, you have refreshed model. Relations are part of model, so they're refreshed too. Looks like perfectly valid behavior for me. |
Thanks @rob006 and @PowerGamer1 for the civil and constructive discussion. As far as I see, we need to pick one of the options (Remove or stay with the status quo) and document it. So far documenting the current behavior, where magic work and where magic is not working and user must explicitly do it himself seems to be enough to remove uncertanities. Any thoughts on that? @rob006 @PowerGamer1 |
There are still things that could be improved in current solution:
I think we also need to verify some of the claims in this issue before taking any action. Based on description of this issue I assumed that refreshing relations does not work for |
By the way, here you actually CONFIRMED the conclusion I came to - only the user HIMSELF can decide when to unset relations and which exactly.
So just because user decided to use a more efficient eager loading (remember the "N+1" problem, for ex.) that makes him somehow not WORTHY of your "magic" behaviour? Also, have you tried to NOT rely on lazy loading in Yii2? Unlike, for example, Eloquent ORM, which has option to DISABLE lazy loading completely (and error out if relation is not loaded) there is no such option in Yii2 and so avoiding lazy loading becomes a practically impossible nightmare. Anyway, in practice it is always a mix of the two (eager and lazy loading) and they should not behave so much different on a conceptual level.
It's not the EXAMPLES that contradict each other - it's the ENGINE BEHAVIOUR in them.
No workflow relies on inconsistent state, the state may become inconsistent during transition from one consistent state to the next consistent state (study example 7 again). Due to that attempts by the engine to ALWAYS keep the state consistent are doomed to fail.
You might be right here.
Yeah, except there is no light and fast alternative to AR in the engine or do you suggest that people should go back to the stone age and use arrays instead of object classes? Regardless, performance is just an icing on the cake and NOT the main reason for the change (the poor implementation quality and inconsistency in the engine are).
"Staying with the status quo" simply won't do because:
P.S. Can I please hear the opinions of other maintainers (esp. @samdark) other than rob006? Because I can already see that rob006 cares not about engine consistency - only about survival of his specific isolated use-case. P.P.S. You may start with the statement if you agree that both lazy AND eager loading should work CONSISTENTLY with regard of "magic" synching. Because I don't want to waste tons of my time spelling obvious things for you such as: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! As rob006 considers to be acceptable for "magic" to work on lazy loading ONLY. Imagine user had lazy loading in place and relied on "magic" synching and then for whatever VALID reason decides to switch to eager loading (such as realizing he has "N+1" problem and deciding to make his code more efficient; or ensuring specific relations are loaded inside the transaction rather than lazily outside) - and BAM - his code breaks because "magic" synching suddenly stops working for him ??? How can you even consider something like THAT to be acceptable ??? !!!!!!!!!! THE BASIS FOR ANY FURTHER DISCUSSION !!!!!!!!! |
In example 2 you complain that relations loaded with At that point I would rather see a unit tests that would confirm the "holes" in implementation. And then, if it is possible, we could focus on fixing them and making behavior consistent. |
In example 2 I complain that "magic" behaviour is inconsistent (lazy loading vs So, as you can see, no contradiction in my examples at all.
You may start with the fact that "magic" synching is not working for eager loading and |
If is not heavy on you, can you propose a PR with what you see as perfect/better implementation? |
Such implementation is simple (see #19918): the rollback of #13618 with additional removal of relation unsetting in Yes, it will be a BC break, but you can think of it as undoing the BC break done by #13618 in the past. There is no other way around it. As for the part with |
Since PHP 8.2 has deprecated dynamic properties I had to switch to storing relation data in ActiveRecord model using
ActiveRecord::populateRelation()
method. This lead to surfacing of surprising problems in my code the root cause of whichwas tracked to a feature introduced in #13618. While attempting to make a fix in #19785 I studied and tested the implementation of #13618 extensively and provide my findings and conclusion below.
The concept.
Yii2 allows to define foreign key dependencies between corresponding database tables and conveniently fetch data from related tables using lazy or eager loading.
Before the #13618, once the relation was loaded the engine never attempted to synchronize the values of foreign key in ActiveRecord model with the data of corresponding relation. EXCEPT a very single isolated case which provided such rudimentary synchronization: calling
ActiveRecord::refresh()
. That call simply unset all loaded relations (if used again the relations would be lazy loaded from DB and if they changed the user will get the updated data). This mechanic already had negative consequences in some cases, for example:Later, the functionality of #13618 (referred to as "magic" below) attempted to introduce proper automatic synchronization between the foreign key values of a model and corresponding relations in all other cases.
The analysis.
Unfortunately, the implementation of the "magic" has failed miserably in its goal and a few instances where it actually works come with significant performance and memory price to ActiveRecord. The following examples illustrate cases where "magic" currently fails to work correctly, has negative consequences or will be impossible to implement properly at all.
Currently, the "magic" simply does not work in many cases were it should:
Example 1. Relations loaded through
with()
aren't "magically" synced.Example 2. Relations set manually through
populateRelation()
aren't "magically" synced.or "works" where it shouldn't (by unsetting synchronized relations):
Example 3. If the foreign key value remains the same but changes its type from int to string (for ex., submitted form data comes as string) the "magic" considers relation to be de-synced (leading to extra query into DB to fetch the same data).
Example 4. On ActiveRecord::refresh() all relations are unset regardless of synchronization state (see #19785).
or has other issues due to poor implementation quality:
Example 5. In some cases the housekeeping data of the "magic" lingers even when relation is no longer loaded.
Upon deeper consideration it becomes obvious that the concept of "forced synchronization" is flawed and unachievable. It is because some desynchronized states are perfectly fine and unavoidable.
Example 6. The enforcement of the rule (unsetting relation when foreign key value changes) upon which the "magic" implementation is based interferes with user intentions.
Setting the relation manually (which actually is often very useful) in general case involves two separate steps:
populateRelation()
;These steps can be performed in any order and each one of them may be optional (why perform both steps if only relation OR the foreign key attribute is used later in the code).
Example 7. User prepares new data of primary model and its relations for insertion into the database.
Another case where "desynchronized" state is very inherent and is OK.
Example 8. The synchronization the "magic" is currently tries to achieve is very one-sided.
Example 9. How should synchronization look when used together with other existing features of ActiveRecord - for ex.
markAttributeDirty()
?Example 10. How should synchronization look when used together with other existing features of ActiveRecord - for ex. fetching a PARTIAL set of attributes from DB (when COMPOSITE foreign key is fetches partially)?
I am sure there are many more examples like this and this is just a tip of the iceberg.
The price.
Another side of the story that the examples above do not show is the PRICE paid to achieve the "magic" (obvious from implementation code):
_relationDependencies
). Anyone who tried to handle a few thousand ARs would NOT be welcoming another memory footprintincrease of AR that could have been easily avoided.
$project->name = 'New name';
).Summary and solution.
So, lets see what we currently have:
Now, lets take a look on a situation before enforcing of synchronization was attempted. Was it really so bad?
The WHOLE "magic" above could have been easily achieved by something as simple as a single line of
unset()
in a user code when user expects ActiveRecord's foreign key attributes to change !!!The conclusion.
The only sensible solution is giving up on an attempt to enforce automatic synchronization between relation and corresponding foreign key attributes. This means that the only guarantee the engine should provide is: once set ActiveRecord relations are never automatically or silently changed/unset by the engine. Which means it is user responsibility to ensure his data stays synchronized to whatever extent user deems necessary and to explicitly call
unset()
on relations that are expected to change and need to be reloaded.The text was updated successfully, but these errors were encountered: