#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.

Reply via email to