Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions model_bakery/baker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -915,7 +916,7 @@
setattr(objects[i], fk.name, fk_obj)


def bulk_create(baker: Baker[M], quantity: int, **kwargs) -> list[M]:

Check failure on line 919 in model_bakery/baker.py

View workflow job for this annotation

GitHub Actions / linters (ruff check --output-format=github .)

Ruff (C901)

model_bakery/baker.py:919:5: C901 `bulk_create` is too complex (18 > 10)
"""
Bulk create entries and all related FKs as well.

Expand Down Expand Up @@ -971,4 +972,37 @@
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.
Comment on lines +975 to +979
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we revise this comment?
I appreciate you going the extra mile to document things, but it's hard to read here. :(

Suggested change
# 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.
# set M2M on FK-related objects (e.g. `home__dogs=[dog]`)
# `_handle_m2m()` is skipped during `prepare()` since `commit=False`,
# and `_save_related_objs()` only persists FK rows without touching M2M.

Something like this is more readable?

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)
Comment on lines +1003 to +1006
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we consider optimizing this via bulk_create?

This would be less readable than a simple set(); however, we clearly reduce the number of queries used.

Suggested change
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)
through_rows = []
for entry in created_entries:
fk_obj = getattr(entry, fk_field_name, None)
if fk_obj is not None:
through_rows.extend(
through_model(
**{
related_m2m.m2m_field_name(): fk_obj,
related_m2m.m2m_reverse_field_name(): obj,
}
)
for obj in kwarg_value
)
if through_rows:
through_model.objects.bulk_create(through_rows)

We can potentially extend the test with self.assertNumQueries to see its stats.


return created_entries
4 changes: 4 additions & 0 deletions tests/generic/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 16 additions & 0 deletions tests/test_baker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Comment on lines +1271 to +1272
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What stops us from doing a full assert here?

Suggested change
h1, h2 = models.HomeOwner.objects.all()[:2]
assert list(h1.home.dogs.all()) == list(h2.home.dogs.all()) == [dog]
for owner in models.HomeOwner.objects.all():
assert list(owner.home.dogs.all()) == [dog]



class TestBakerSeeded:
@pytest.fixture
Expand Down
Loading