﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
30828	"Document how to add ManyToMany relationships in bulk for different objects and for manually defined ""through""."	David Foster	David Foster	"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.
    """"""
}}}
"	Cleanup/optimization	closed	Documentation	dev	Normal	fixed		Patrick Cloke	Accepted	1	0	0	0	0	0
