[Django] #30828: Add ManyToMany relationships in bulk

21 views
Skip to first unread message

Django

unread,
Oct 2, 2019, 1:08:18 AM10/2/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David | Owner: David Foster
Foster |
Type: New | Status: assigned
feature |
Component: Database | Version: master
layer (models, ORM) |
Severity: Normal | Keywords:
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
Given the following example model:

{{{
class M1(models.Model):
m2_set = models.ManyToManyField('M2')
}}}

It is already possible to associate one M1 with many M2s with a single DB
query:

{{{
m1.m2_set.add(*m2s)
}}}

However it's more difficult to associate many M1s with many M2s,
particularly if you want to skip associations that already exist:

{{{
# NOTE: Does NOT skip associations that already exist!
m1_and_m2_id_tuples = [(m1_id, m2_id), ...]
M1_M2 = M1.m2_set.through
M1_M2.objects.bulk_create([
M1_M2(m1_id=m1_id, m2_id=m2_id)
for (m1_id, m2_id) in
m1_and_m2_id_tuples
])
}}}

I propose adding the following APIs to bulk-associate relationships:

{{{
M1.m2_set.add_pairs(*[(m1, m2), ...], assert_no_collisions=False)
# --- OR ---
M1.m2_set.add_pair_ids(*[(m1_id, m2_id), ...], assert_no_collisions=False)
}}}

I also propose to add the following paired APIs to bulk-disassociate
relationships:

{{{
M1.m2_set.remove_pairs(*[(m1, m2), ...])
# --- OR ---
M1.m2_set.remove_pair_ids(*[(m1_id, m2_id), ...])
}}}

I have already written code for both of these cases and have been using it
in production for a few years. It probably needs to be extended to support
non-default database connections. Documentation+tests need to be added of
course.

Related thread on Django-developers:
https://groups.google.com/forum/#!topic/django-developers/n8ZN5uuuM_Q

API docstrings, with further details:

{{{
def add_pairs(
self: ManyToManyDescriptor, # M1.m2_set
m1_m2_tuples: 'List[Tuple[M1, M2]]',
*, assert_no_collisions: bool=False) -> None:
"""
Creates many (M1, M2) associations with O(1) database queries.

If any requested associations already exist, then they will be left
alone.

If you assert that none of the requested associations already exist,
you can pass assert_no_collisions=True to save 1 database query.
"""

def remove_pairs(
self: ManyToManyDescriptor, # M1.m2_set
m1_m2_tuples: 'List[Tuple[M1, M2]]') -> None:
"""
Deletes many (M1, M2) associations with O(1) database queries.
"""
}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/30828>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Oct 2, 2019, 10:49:37 AM10/2/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
| Foster
Type: New feature | Status: assigned
Component: Database layer | Version: master
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Carlton Gibson):

* stage: Unreviewed => Accepted


Comment:

I'll accept this given the preliminary discussion on the mailing list.
Let's see the patch. :)
Thanks David.

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:1>

Django

unread,
Oct 2, 2019, 3:04:52 PM10/2/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
| Foster
Type: New feature | Status: assigned
Component: Database layer | Version: master
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Patrick Cloke):

* cc: Patrick Cloke (added)


--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:2>

Django

unread,
Oct 6, 2019, 7:32:43 PM10/6/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
| Foster
Type: New feature | Status: assigned
Component: Database layer | Version: master
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------

Comment (by David Foster):

Draft documentation has been written. Implementation and tests are
pending. Will post PR once I have all three.

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:3>

Django

unread,
Oct 10, 2019, 2:01:12 AM10/10/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
| Foster
Type: New feature | Status: assigned
Component: Database layer | Version: master
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by David Foster):

* has_patch: 0 => 1


Comment:

[https://github.com/django/django/pull/11899 PR] created.

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:4>

Django

unread,
Oct 14, 2019, 2:05:10 PM10/14/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
| Foster
Type: New feature | Status: assigned
Component: Database layer | Version: master
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------

Comment (by felixxm):

> However it's more difficult to associate many M1s with many M2s, ...

You can use `set()` on a reverse relationship, e.g.
{{{
m2.m1_set.add(*m1s)
}}}
Right?

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:5>

Django

unread,
Oct 14, 2019, 9:37:27 PM10/14/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
| Foster
Type: New feature | Status: assigned
Component: Database layer | Version: master
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------

Comment (by David Foster):

Yes you can bulk add on a reverse relationship to associate many items
with one other item in reverse. But you can't do some thing like adding
the relations {(a1, b1), (a1, b2), (a2, b3), (a4, b4)} all at once.

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:6>

Django

unread,
Oct 14, 2019, 9:49:51 PM10/14/19
to django-...@googlegroups.com
#30828: Add ManyToMany relationships in bulk
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
| Foster
Type: New feature | Status: assigned
Component: Database layer | Version: master
(models, ORM) |
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------

Comment (by Simon Charette):

> But you can't do some thing like adding the relations ... all at once.

`.bulk_create(ignore_conflicts=True)`
[https://github.com/django/django/pull/11899#pullrequestreview-301073824
works reasonably well for this purpose] with a small boilerplate increase
at the profit of readability. The latter also works for manually defined
`through` with fields without defaults.

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:7>

Django

unread,
Oct 15, 2019, 1:56:17 AM10/15/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".

-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: assigned
Component: Documentation | Version: master

Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by felixxm):

* type: New feature => Cleanup/optimization
* component: Database layer (models, ORM) => Documentation


Comment:

Let's change this to a documentation issue. I think we should add a
paragraph about adding `ManyToManyField`'s relations with `bulk_create()`
for manually defined `through` and for different objects (on LHS and RHS)
to the "Insert in bulk" section (`docs/topics/db/optimization.txt`).

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:8>

Django

unread,
Oct 15, 2019, 4:35:28 AM10/15/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: assigned
Component: Documentation | Version: master
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 1 | Patch needs improvement: 1

Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by felixxm):

* needs_better_patch: 0 => 1
* needs_tests: 0 => 1


--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:9>

Django

unread,
Oct 15, 2019, 4:35:46 AM10/15/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: assigned
Component: Documentation | Version: master
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 1
Needs tests: 0 | Patch needs improvement: 1

Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by felixxm):

* needs_better_patch: 0 => 1

* needs_docs: 0 => 1

Django

unread,
Oct 20, 2019, 3:43:40 PM10/20/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: assigned
Component: Documentation | Version: master
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by David Foster):

* needs_better_patch: 1 => 0
* needs_docs: 1 => 0


Comment:

I have a new patch in [https://github.com/django/django/pull/11948 PR
beta] that provides a documentation workaround. Comments requested.

The proposed documentation recommends that users inline a fair bit of
boilerplate to perform bulk-associate and bulk-disassociate operations,
which feels a bit verbose to me, so I suggest still considering an
approach where we add dedicated methods for these operations.

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:10>

Django

unread,
Oct 28, 2019, 8:42:03 AM10/28/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: assigned
Component: Documentation | Version: master
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 1

Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by felixxm):

* needs_better_patch: 0 => 1


--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:11>

Django

unread,
Nov 12, 2019, 1:00:48 AM11/12/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: assigned
Component: Documentation | Version: master
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by David Foster):

* needs_better_patch: 1 => 0


Comment:

Patch revised and ready for another review.

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:12>

Django

unread,
Nov 12, 2019, 7:14:05 AM11/12/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: closed
Component: Documentation | Version: master
Severity: Normal | Resolution: fixed
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Mariusz Felisiak <felisiak.mariusz@…>):

* status: assigned => closed
* resolution: => fixed


Comment:

In [changeset:"6a04e69e686cf417b469d7676f93c2e3a9c8d6a3" 6a04e69e]:
{{{
#!CommitTicketReference repository=""
revision="6a04e69e686cf417b469d7676f93c2e3a9c8d6a3"
Fixed #30828 -- Added how to remove/insert many-to-many relations in bulk
to the database optimization docs.
}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:13>

Django

unread,
Nov 12, 2019, 7:14:45 AM11/12/19
to django-...@googlegroups.com
#30828: Document how to add ManyToMany relationships in bulk for different objects
and for manually defined "through".
-------------------------------------+-------------------------------------
Reporter: David Foster | Owner: David
Type: | Foster
Cleanup/optimization | Status: closed
Component: Documentation | Version: master
Severity: Normal | Resolution: fixed
Keywords: | Triage Stage: Accepted
Has patch: 1 | Needs documentation: 0

Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------

Comment (by Mariusz Felisiak <felisiak.mariusz@…>):

In [changeset:"afde973061a3e6477f3c454f4471842d37e73494" afde9730]:
{{{
#!CommitTicketReference repository=""
revision="afde973061a3e6477f3c454f4471842d37e73494"
[2.2.x] Fixed #30828 -- Added how to remove/insert many-to-many relations


in bulk to the database optimization docs.

Backport of 6a04e69e686cf417b469d7676f93c2e3a9c8d6a3 from master
}}}

--
Ticket URL: <https://code.djangoproject.com/ticket/30828#comment:14>

Reply all
Reply to author
Forward
0 new messages