Skip to content
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

ActiveRecord::refresh() should not unset all relations but only the ones whose corresponding foreign key has changed #19785

Open
PowerGamer1 opened this issue Mar 11, 2023 · 6 comments

Comments

@PowerGamer1
Copy link
Contributor

PowerGamer1 commented Mar 11, 2023

The #13618 introduced "magic" functionality that makes it look like changing foreign key attribute of a model also changes loaded relation model dependent on this foreign key. This was achieved by simply unsetting the affected relation (whose foreign key has changed).

This was actually a BC breaking change later documented in UPGRADE.md as:

Active Record relations are now being reset when corresponding key fields are changed.

Unfortunately, the original implementation didn't follow its own logic thoroughly and left the ActiveRecord::refresh() implemented incorrectly - unsetting ALL relations regardless of changes to the foreign keys. And so, if relations loaded before refresh() are used again after refresh() additional queries against database are executed to fetch the same data recently unset by refresh().

What steps will reproduce the problem?

$project = Project::findOne(123);
$project->manager; // Load relation.
$project->manager_id = 456; // Change relation foreign key.
var_dump($project->isRelationPopulated('manager')); // false - OK: foreign key changed, previously loaded relation is considered outdated and is unset.

$project = Project::findOne(123);
$project->manager;
$project->refresh(); // Assuming manager_id has not changed.
var_dump($project->isRelationPopulated('manager')); // false - BUG (expected true): foreign key is the same but relation is unset.
$project->manager; // If the relation is used later, another query must be performed against DB that loads the same data (unset by refresh()).

What is the expected result?

false
true

What do you get instead?

false
false

Additional info

Q A
Yii version 2.0.48
PHP version irrelevant
Operating system irrelevant
@samdark
Copy link
Member

samdark commented Mar 13, 2023

Yes. Ideally it should behave like you've described. Do you have time for fixing it?

@PowerGamer1
Copy link
Contributor Author

PowerGamer1 commented Mar 13, 2023

Yes. Ideally it should behave like you've described. Do you have time for fixing it?

This particular issue causes a bunch of problems in my production code, I will submit PR shortly.

@rhertogh
Copy link
Contributor

@PowerGamer1 Just to be sure, your example states var_dump($project->isRelationPopulated('project')); should that have been var_dump($project->isRelationPopulated('manager'));?

@PowerGamer1
Copy link
Contributor Author

@PowerGamer1 Just to be sure, your example states var_dump($project->isRelationPopulated('project')); should that have been var_dump($project->isRelationPopulated('manager'));?

Yes, you are correct, I edited my original post and fixed it.

@rhertogh
Copy link
Contributor

@samdark

Yes. Ideally it should behave like you've described.

Are you sure about that? The BaseActiveRecord::refresh() description states "Repopulates this active record with the latest data".
To me that reads as that the entire state of the ActiveRecord should be invalidated and synchronized with the database.
Since we don't know the state of the relations it makes sense to me that they are cleared.

@PowerGamer1
Copy link
Contributor Author

PowerGamer1 commented Jul 29, 2023

The BaseActiveRecord::refresh() description states "Repopulates this active record with the latest data". To me that reads as that the entire state of the ActiveRecord should be invalidated and synchronized with the database.

Your quote from docs mentions only "... THIS active record ..." and the relations are DIFFERENT active records. But that is irrelevant - see below.

Since we don't know the state of the relations it makes sense to me that they are cleared.

You don't know but the user does. So why are you screwing the user by force-clearing his relations instead of letting him handle the clearing himself if needed (see example 7 from #19788)?

P.S. The problem in this issue is a part of much bigger problem in #19788 and must not be considered in separation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants