#33984: Related managers cache gets stale after saving a fetched model with new
PK
-------------------------------------+-------------------------------------
Reporter: joeli | Owner: nobody
Type: Bug | Status: new
Component: Database layer | Version: 4.1
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Description changed by joeli:
Old description:
> I'm upgrading our codebase from Django 3.2 to 4.1, and came upon a
> regression when running our test suite. I bisected it down to commit
> [4f8c7fd9d9 Fixed #32980 -- Made models cache related
> managers](https://github.com/django/django/commit/4f8c7fd9d91b35e2c2922de4bb50c8c8066cbbc6).
>
> The main problem is that when you have fetched a model containing a m2m
> field from the database, and access its m2m field the manager gets
> cached. If you then set the model's `.pk` to `None` and do a `.save()` to
> save it as a copy of the old one, the related manager cache is not
> cleared. Here's some code with inline comments to demonstrate:
>
> {{{
> # models.py
> from django.db import models
>
> class Tag(models.Model):
> tag = models.SlugField(max_length=64, unique=True)
>
> def __str__(self):
> return self.tag
>
> class Thing(models.Model):
> tags = models.ManyToManyField(Tag, blank=True)
>
> def clone(self) -> 'Thing':
> # To clone a thing, we first save a list of the tags it has
> tags = list(self.tags.all())
>
> # Then set its pk to none and save, creating the copy
> self.pk = None
> self.save()
>
> # In django 3.2 the following sets the original tags for the new
> instance.
> # In 4.x it's a no-op because self.tags returns the old
> instance's manager.
> self.tags.set(tags)
> return self
>
> @staticmethod
> def has_bug():
> one, _ = Tag.objects.get_or_create(tag='one')
> two, _ = Tag.objects.get_or_create(tag='two')
>
> obj = Thing.objects.create()
> obj.tags.set([one, two])
> new_thing = obj.clone()
>
> # new_thing.tags.all() returns the expected tags, but it is
> actually returning obj.tags.all()
> # If we fetch new_thing again it returns the actual tags for
> new_thing, which is empty.
> #
> # Even `new_thing.refresh_from_db()` -- which is not required
> with 3.x -- does not help.
> # `new_thing._state.related_managers_cache.clear()` works, but
> feels like something I
> # shouldn't have to do.
> return list(new_thing.tags.all()) !=
> list(Thing.objects.get(pk=new_thing.pk).tags.all())
> }}}
New description:
I'm upgrading our codebase from Django 3.2 to 4.1, and came upon a
regression when running our test suite. I bisected it down to commit
4f8c7fd9d9 Fixed #32980 -- Made models cache related managers:
[https://github.com/django/django/commit/4f8c7fd9d91b35e2c2922de4bb50c8c8066cbbc6]
The main problem is that when you have fetched a model containing a m2m
field from the database, and access its m2m field the manager gets cached.
If you then set the model's `.pk` to `None` and do a `.save()` to save it
as a copy of the old one, the related manager cache is not cleared. Here's
some code with inline comments to demonstrate:
{{{
# models.py
from django.db import models
class Tag(models.Model):
tag = models.SlugField(max_length=64, unique=True)
def __str__(self):
return self.tag
class Thing(models.Model):
tags = models.ManyToManyField(Tag, blank=True)
def clone(self) -> 'Thing':
# To clone a thing, we first save a list of the tags it has
tags = list(self.tags.all())
# Then set its pk to none and save, creating the copy
self.pk = None
self.save()
# In django 3.2 the following sets the original tags for the new
instance.
# In 4.x it's a no-op because self.tags returns the old instance's
manager.
self.tags.set(tags)
return self
@staticmethod
def has_bug():
one, _ = Tag.objects.get_or_create(tag='one')
two, _ = Tag.objects.get_or_create(tag='two')
obj = Thing.objects.create()
obj.tags.set([one, two])
new_thing = obj.clone()
# new_thing.tags.all() returns the expected tags, but it is
actually returning obj.tags.all()
# If we fetch new_thing again it returns the actual tags for
new_thing, which is empty.
#
# Even `new_thing.refresh_from_db()` -- which is not required with
3.x -- does not help.
# `new_thing._state.related_managers_cache.clear()` works, but
feels like something I
# shouldn't have to do.
return list(new_thing.tags.all()) !=
list(Thing.objects.get(pk=new_thing.pk).tags.all())
}}}
--
--
Ticket URL: <https://code.djangoproject.com/ticket/33984#comment:1>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
--
You received this message because you are subscribed to the Google Groups
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/010701831293b3f0-c095dc7c-9530-4fc6-9faa-069cc76e7780-000000%40eu-central-1.amazonses.com.