Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ Changelog

.. rst-class:: emphasize-children

0.26
====

0.26.0 (unreleased)
-------------------
Added
^^^^^
- Add `create()` method to reverse ForeignKey relations, enabling `parent.children.create()` syntax

0.25
====

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Contributors
* Markus Beckschulte ``@markus-96``
* Frederic Aoustin ``@fraoustin``
* Ludwig Hähne ``@pankrat``
* Christian Tanul ``@scriptogre``

Special Thanks
==============
Expand Down
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ With the Tortoise initialized, the models are available for use:
team = await Team.create(name='Team {}'.format(i + 1))
participants.append(team)

# One to Many (ForeignKey) relations support creating related objects
another_event = await tournament.events.create(name='Another Event')

# Many to Many Relationship management is quite straightforward
# (there are .remove(...) and .clear() too)
await event.participants.add(*participants)
Expand Down
9 changes: 9 additions & 0 deletions docs/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ The related objects can be filtered:

which will return you a QuerySet object with predefined filter.

You can also create related objects directly through the reverse ForeignKey relation:

.. code-block:: python3

# Create a related object automatically setting the foreign key
new_event = await team.events.create(name='New Event')
# Equivalent to:
# new_event = await Event.create(name='New Event', team=team)

QuerySet
========

Expand Down
24 changes: 23 additions & 1 deletion tests/test_relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
)
from tortoise.contrib import test
from tortoise.contrib.test.condition import NotIn
from tortoise.exceptions import FieldError, NoValuesFetched
from tortoise.exceptions import FieldError, NoValuesFetched, OperationalError
from tortoise.functions import Count, Trim


Expand Down Expand Up @@ -479,3 +479,25 @@ async def test_o2o_fk_model_with_m2m_field(self):
self.assertEqual(await obj.nodes.all(), [])
await obj.nodes.add(node)
self.assertEqual(await obj.nodes.all(), [node])

async def test_reverse_relation_create_fk(self):
tournament = await Tournament.create(name="Test Tournament")
self.assertEqual(await tournament.events.all(), [])

event = await tournament.events.create(name="Test Event")

await tournament.fetch_related("events")

self.assertEqual(len(tournament.events), 1)
self.assertEqual(event.name, "Test Event")
self.assertEqual(event.tournament_id, tournament.id)
self.assertEqual(tournament.events[0].event_id, event.event_id)

async def test_reverse_relation_create_fk_errors_for_unsaved_instance(self):
tournament = Tournament(name="Unsaved Tournament")

# Should raise OperationalError since tournament isn't saved
with self.assertRaises(OperationalError) as cm:
await tournament.events.create(name="Test Event")

self.assertIn("hasn't been instanced", str(cm.exception))
32 changes: 32 additions & 0 deletions tortoise/fields/relational.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,38 @@ def offset(self, offset: int) -> QuerySet[MODEL]:
"""
return self._query.offset(offset)

async def create(self, using_db: BaseDBAsyncClient | None = None, **kwargs: Any) -> MODEL:
"""
Create a related record in the DB and returns the object, automatically setting the
foreign key relationship to the parent instance.

.. code-block:: python3

tournament = await Tournament.create(name="...")
event = await tournament.events.create(...)

Equivalent to:

.. code-block:: python3

tournament = await Tournament.create(name="...")
event = await Event.create(tournament=tournament, ...)

:param using_db: Specific DB connection to use instead of default bound.
:param kwargs: Model parameters for the new object.
:raises OperationalError: If parent instance is not saved to the database.
"""
if not self.instance._saved_in_db:
raise OperationalError(
"This objects hasn't been instanced, call .save() before calling related queries"
)

# Inject foreign key relationship automatically
kwargs[self.relation_field] = getattr(self.instance, self.from_field)

# Call remote model's create method
return await self.remote_model.create(using_db=using_db, **kwargs)

def _set_result_for_query(self, sequence: list[MODEL], attr: str | None = None) -> None:
self._fetched = True
self.related_objects = sequence
Expand Down