-
-
Notifications
You must be signed in to change notification settings - Fork 102
Fix bulk_create not setting M2M on FK-related objects #583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -915,7 +916,7 @@ | |||||||||||||||||||||||||||||||||||||||
| setattr(objects[i], fk.name, fk_obj) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def bulk_create(baker: Baker[M], quantity: int, **kwargs) -> list[M]: | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| Bulk create entries and all related FKs as well. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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. | ||||||||||||||||||||||||||||||||||||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about we consider optimizing this via This would be less readable than a simple
Suggested change
We can potentially extend the test with |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return created_entries | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What stops us from doing a full assert here?
Suggested change
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| class TestBakerSeeded: | ||||||||||
| @pytest.fixture | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
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. :(
Something like this is more readable?