Skip to content

Commit cbd1fb5

Browse files
authored
Enable creation via reverse ForeignKey relations parent.children.create() (#1991)
1 parent c4f601e commit cbd1fb5

File tree

6 files changed

+77
-1
lines changed

6 files changed

+77
-1
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ Changelog
77

88
.. rst-class:: emphasize-children
99

10+
0.26
11+
====
12+
13+
0.26.0 (unreleased)
14+
-------------------
15+
Added
16+
^^^^^
17+
- Add `create()` method to reverse ForeignKey relations, enabling `parent.children.create()` syntax
18+
1019
0.25
1120
====
1221

CONTRIBUTORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ Contributors
6565
* Markus Beckschulte ``@markus-96``
6666
* Frederic Aoustin ``@fraoustin``
6767
* Ludwig Hähne ``@pankrat``
68+
* Christian Tanul ``@scriptogre``
6869

6970
Special Thanks
7071
==============

README.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ With the Tortoise initialized, the models are available for use:
152152
team = await Team.create(name='Team {}'.format(i + 1))
153153
participants.append(team)
154154
155+
# One to Many (ForeignKey) relations support creating related objects
156+
another_event = await tournament.events.create(name='Another Event')
157+
155158
# Many to Many Relationship management is quite straightforward
156159
# (there are .remove(...) and .clear() too)
157160
await event.participants.add(*participants)

docs/query.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ The related objects can be filtered:
5050
5151
which will return you a QuerySet object with predefined filter.
5252

53+
You can also create related objects directly through the reverse ForeignKey relation:
54+
55+
.. code-block:: python3
56+
57+
# Create a related object automatically setting the foreign key
58+
new_event = await team.events.create(name='New Event')
59+
# Equivalent to:
60+
# new_event = await Event.create(name='New Event', team=team)
61+
5362
QuerySet
5463
========
5564

tests/test_relations.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919
from tortoise.contrib import test
2020
from tortoise.contrib.test.condition import NotIn
21-
from tortoise.exceptions import FieldError, NoValuesFetched
21+
from tortoise.exceptions import FieldError, NoValuesFetched, OperationalError
2222
from tortoise.functions import Count, Trim
2323

2424

@@ -479,3 +479,25 @@ async def test_o2o_fk_model_with_m2m_field(self):
479479
self.assertEqual(await obj.nodes.all(), [])
480480
await obj.nodes.add(node)
481481
self.assertEqual(await obj.nodes.all(), [node])
482+
483+
async def test_reverse_relation_create_fk(self):
484+
tournament = await Tournament.create(name="Test Tournament")
485+
self.assertEqual(await tournament.events.all(), [])
486+
487+
event = await tournament.events.create(name="Test Event")
488+
489+
await tournament.fetch_related("events")
490+
491+
self.assertEqual(len(tournament.events), 1)
492+
self.assertEqual(event.name, "Test Event")
493+
self.assertEqual(event.tournament_id, tournament.id)
494+
self.assertEqual(tournament.events[0].event_id, event.event_id)
495+
496+
async def test_reverse_relation_create_fk_errors_for_unsaved_instance(self):
497+
tournament = Tournament(name="Unsaved Tournament")
498+
499+
# Should raise OperationalError since tournament isn't saved
500+
with self.assertRaises(OperationalError) as cm:
501+
await tournament.events.create(name="Test Event")
502+
503+
self.assertIn("hasn't been instanced", str(cm.exception))

tortoise/fields/relational.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,38 @@ def offset(self, offset: int) -> QuerySet[MODEL]:
127127
"""
128128
return self._query.offset(offset)
129129

130+
async def create(self, using_db: BaseDBAsyncClient | None = None, **kwargs: Any) -> MODEL:
131+
"""
132+
Create a related record in the DB and returns the object, automatically setting the
133+
foreign key relationship to the parent instance.
134+
135+
.. code-block:: python3
136+
137+
tournament = await Tournament.create(name="...")
138+
event = await tournament.events.create(...)
139+
140+
Equivalent to:
141+
142+
.. code-block:: python3
143+
144+
tournament = await Tournament.create(name="...")
145+
event = await Event.create(tournament=tournament, ...)
146+
147+
:param using_db: Specific DB connection to use instead of default bound.
148+
:param kwargs: Model parameters for the new object.
149+
:raises OperationalError: If parent instance is not saved to the database.
150+
"""
151+
if not self.instance._saved_in_db:
152+
raise OperationalError(
153+
"This objects hasn't been instanced, call .save() before calling related queries"
154+
)
155+
156+
# Inject foreign key relationship automatically
157+
kwargs[self.relation_field] = getattr(self.instance, self.from_field)
158+
159+
# Call remote model's create method
160+
return await self.remote_model.create(using_db=using_db, **kwargs)
161+
130162
def _set_result_for_query(self, sequence: list[MODEL], attr: str | None = None) -> None:
131163
self._fetched = True
132164
self.related_objects = sequence

0 commit comments

Comments
 (0)