#32563: Cannot override database used with RelatedManager
-------------------------------------+-------------------------------------
     Reporter:  Lucas Gruber         |                    Owner:  nobody
         Type:  Bug                  |                   Status:  new
    Component:  Database layer       |                  Version:  dev
  (models, ORM)                      |
     Severity:  Normal               |               Resolution:
     Keywords:  RelatedManager       |             Triage Stage:
  mullti databases                   |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
Description changed by Lucas Gruber:

Old description:

> I suppose an app with models:
> {{{
> ############################
> class Blog(models.Model):
>     title = models.CharField(max_length=100)
>
> class Person(models.Model):
>     name = models.CharField(max_length=100)
>     subscribed_blogs = models.ManyToManyField(Blog,
> related_name="subscribers", through="Subscription")
>
> class Subscription(models.Model):
>     person = models.ForeignKey(Person, related_name="subscriptions")
>     blog = models.ForeignKey(Blog, related_name="subscriptions")
> ############################
> }}}
>
> In thes case we are using multiple databases and we need to create custom
> migration, we have to write something like
> {{{
> ############################
> def custom_migrations(apps, schema_editor):
>     db_alias = schema_editor.connection.alias
>
>     person = Person.objects.using(db_alias).get(pk=1)
>     blog = Blog.objects.using(db_alias).get(pk=1)
>     blog.subscribers.set([person])
>
>     blog.save(using=db_alias)
>
> ############################
> }}}
>
> The line blog.subscribers.set(...) does not permit to add parameter for
> overriding database to use.
> The source code for this function is in
> django.db.models.fields.related_descriptors when we can see:
> {{{
> [...]
>         def set(self, objs, *, bulk=True, clear=False):
>             # Force evaluation of `objs` in case it's a queryset whose
> value
>             # could be affected by `manager.clear()`. Refs #19816.
>             objs = tuple(objs)
>
>             if self.field.null:
>                 db = router.db_for_write(self.model,
> instance=self.instance)
>                 with transaction.atomic(using=db, savepoint=False):
>                     if clear:
>                         self.clear(bulk=bulk)
>                         self.add(*objs, bulk=bulk)
>                     else:
>                         old_objs = set(self.using(db).all())
>                         new_objs = []
>                         for obj in objs:
>                             if obj in old_objs:
>                                 old_objs.remove(obj)
>                             else:
>                                 new_objs.append(obj)
>
>                         self.remove(*old_objs, bulk=bulk)
>                         self.add(*new_objs, bulk=bulk)
>             else:
>                 self.add(*objs, bulk=bulk)
>         set.alters_data = True
> [...]
> }}}
>
> Code always calls database router, but in migration process, the router
> can not find the appropriate database because we just use without request
> :
> {{{
> python manage.py migrate --database db2
> }}}
>
> I noticed that all the methods of ''RelatedManager'' directly call the
> router object to find the database while the ''Manager'' objects always
> exploits the possibility of overriding the database with the call to
> ''using()'' on the QuerySet or to pass parameter ''using=db'' for save
> model method for example.
>

> Thank you in advance for your answer

New description:

 I suppose an app with models:
 {{{
 ############################
 class Blog(models.Model):
     title = models.CharField(max_length=100)

 class Person(models.Model):
     name = models.CharField(max_length=100)
     subscribed_blogs = models.ManyToManyField(Blog,
 related_name="subscribers", through="Subscription")

 class Subscription(models.Model):
     person = models.ForeignKey(Person, related_name="subscriptions")
     blog = models.ForeignKey(Blog, related_name="subscriptions")
 ############################
 }}}

 In thes case we are using multiple databases and we need to create custom
 migration, we have to write something like
 {{{
 ############################
 def custom_migrations(apps, schema_editor):
     db_alias = schema_editor.connection.alias

     person = Person.objects.using(db_alias).get(pk=1)
     blog = Blog.objects.using(db_alias).get(pk=1)
     blog.subscribers.set([person])

     blog.save(using=db_alias)

 ############################
 }}}

 The line blog.subscribers.set(...) does not permit to add parameter for
 overriding database to use.
 The source code for this function is in
 django.db.models.fields.related_descriptors when we can see:
 {{{
 [...]
         def set(self, objs, *, clear=False, through_defaults=None):
             # Force evaluation of `objs` in case it's a queryset whose
 value
             # could be affected by `manager.clear()`. Refs #19816.
             objs = tuple(objs)

             db = router.db_for_write(self.through, instance=self.instance)
             with transaction.atomic(using=db, savepoint=False):
                 if clear:
                     self.clear()
                     self.add(*objs, through_defaults=through_defaults)
                 else:
                     old_ids =
 set(self.using(db).values_list(self.target_field.target_field.attname,
 flat=True))

                     new_objs = []
                     for obj in objs:
                         fk_val = (
 self.target_field.get_foreign_related_value(obj)[0]
                             if isinstance(obj, self.model) else obj
                         )
                         if fk_val in old_ids:
                             old_ids.remove(fk_val)
                         else:
                             new_objs.append(obj)

                     self.remove(*old_ids)
                     self.add(*new_objs, through_defaults=through_defaults)
         set.alters_data = True
 [...]
 }}}

 Code always calls database router, but in migration process, the router
 can not find the appropriate database because we just use without request
 :
 {{{
 python manage.py migrate --database db2
 }}}

 I noticed that all the methods of ''RelatedManager'' directly call the
 router object to find the database while the ''Manager'' objects always
 exploits the possibility of overriding the database with the call to
 ''using()'' on the QuerySet or to pass parameter ''using=db'' for save
 model method for example.


 Thank you in advance for your answer

--

-- 
Ticket URL: <https://code.djangoproject.com/ticket/32563#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/069.26f043c4a67df961b7dc9ababe0a7755%40djangoproject.com.

Reply via email to