diff --git a/model_bakery/baker.py b/model_bakery/baker.py index 4f5220bc..95b62d91 100644 --- a/model_bakery/baker.py +++ b/model_bakery/baker.py @@ -10,6 +10,7 @@ from django.apps import apps from django.conf import settings +from django.core.exceptions import FieldDoesNotExist from django.db.models import ( AutoField, BooleanField, @@ -971,4 +972,37 @@ def bulk_create(baker: Baker[M], quantity: int, **kwargs) -> list[M]: kwargs[reverse_relation_name] ) + # set many-to-many relations on FK-related objects specified via double-underscore + # syntax (e.g. home__dogs=[dog]). During prepare() the M2M values are stored in + # the sub-baker's m2m_dict but _handle_m2m() is never called because commit=False. + # _save_related_objs() only persists FK objects without touching M2M, so we must + # apply them here after all entries have been bulk-created and FK objects saved. + for kwarg_key, kwarg_value in kwargs.items(): + if "__" not in kwarg_key: + continue + fk_field_name, m2m_field_name = kwarg_key.split("__", 1) + if "__" in m2m_field_name: + continue # only handle one level of nesting + try: + fk_field = baker.model._meta.get_field(fk_field_name) + except FieldDoesNotExist: + continue + if not isinstance(fk_field, (ForeignKey, OneToOneField)): + continue + try: + related_m2m = fk_field.related_model._meta.get_field(m2m_field_name) + except FieldDoesNotExist: + continue + if not isinstance(related_m2m, ManyToManyField): + continue + # skip custom through models — .set() requires an auto-created through table; + # custom through models have extra required fields baker cannot populate here + through_model = related_m2m.remote_field.through + if not through_model._meta.auto_created: + continue + for entry in created_entries: + fk_obj = getattr(entry, fk_field_name, None) + if fk_obj is not None: + getattr(fk_obj, m2m_field_name).set(kwarg_value) + return created_entries diff --git a/tests/generic/models.py b/tests/generic/models.py index a3b49c04..db6d0306 100755 --- a/tests/generic/models.py +++ b/tests/generic/models.py @@ -182,6 +182,10 @@ class Home(models.Model): dogs = models.ManyToManyField("Dog") +class HomeOwner(models.Model): + home = models.ForeignKey(Home, on_delete=models.CASCADE) + + class LonelyPerson(models.Model): only_friend = models.OneToOneField(Person, on_delete=models.CASCADE) diff --git a/tests/test_baker.py b/tests/test_baker.py index 4da2fae9..5e5e878a 100644 --- a/tests/test_baker.py +++ b/tests/test_baker.py @@ -1255,6 +1255,22 @@ def test_make_should_create_objects_using_reverse_name(self): list(s1.classroom_set.all()) == list(s2.classroom_set.all()) == [classroom] ) + @pytest.mark.django_db + def test_create_through_foreign_key_field(self): + """Regression test for M2M fields on FK-related objects with _bulk_create=True. + + When a M2M value is specified via double-underscore lookup (e.g. home__dogs=[dog]), + baker must apply that M2M relationship to the saved FK object after bulk_create + completes. Previously, the M2M was stored in the sub-baker's m2m_dict during + prepare() but was never applied because _handle_m2m() is gated behind commit=True, + and _save_related_objs() only persists FK rows without touching M2M. + """ + dog = baker.make(models.Dog) + baker.make(models.HomeOwner, home__dogs=[dog], _quantity=10, _bulk_create=True) + + h1, h2 = models.HomeOwner.objects.all()[:2] + assert list(h1.home.dogs.all()) == list(h2.home.dogs.all()) == [dog] + class TestBakerSeeded: @pytest.fixture