diff --git a/AUTHORS b/AUTHORS index 6359e50154e..72e26ebc3e8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -141,6 +141,7 @@ answer newbie questions, and generally made Django that much better: Bernd Schlapsi Bernhard Essl berto + Bhuvnesh Sharma Bill Fenner Bjørn Stabell Bo Marchman diff --git a/django/db/models/base.py b/django/db/models/base.py index 584db319da6..2eb7ba7e9b1 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -4,6 +4,8 @@ import warnings from functools import partialmethod from itertools import chain +from asgiref.sync import sync_to_async + import django from django.apps import apps from django.conf import settings @@ -737,6 +739,9 @@ class Model(metaclass=ModelBase): self._state.db = db_instance._state.db + async def arefresh_from_db(self, using=None, fields=None): + return await sync_to_async(self.refresh_from_db)(using=using, fields=fields) + def serializable_value(self, field_name): """ Return the value of the field name for this instance. If the field is @@ -810,6 +815,18 @@ class Model(metaclass=ModelBase): save.alters_data = True + async def asave( + self, force_insert=False, force_update=False, using=None, update_fields=None + ): + return await sync_to_async(self.save)( + force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields, + ) + + asave.alters_data = True + def save_base( self, raw=False, @@ -1111,6 +1128,14 @@ class Model(metaclass=ModelBase): delete.alters_data = True + async def adelete(self, using=None, keep_parents=False): + return await sync_to_async(self.delete)( + using=using, + keep_parents=keep_parents, + ) + + adelete.alters_data = True + def _get_FIELD_display(self, field): value = getattr(self, field.attname) choices_dict = dict(make_hashable(field.flatchoices)) diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index f0306a7460b..f9f11cac0d6 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -132,6 +132,9 @@ value from the database:: >>> obj.field # Loads the field from the database .. method:: Model.refresh_from_db(using=None, fields=None) +.. method:: Model.arefresh_from_db(using=None, fields=None) + +*Asynchronous version*: ``arefresh_from_db()`` If you need to reload a model's values from the database, you can use the ``refresh_from_db()`` method. When this method is called without arguments the @@ -188,6 +191,10 @@ all of the instance's fields when a deferred field is reloaded:: A helper method that returns a set containing the attribute names of all those fields that are currently deferred for this model. +.. versionchanged:: 4.2 + + ``arefresh_from_db()`` method was added. + .. _validating-objects: Validating objects @@ -406,6 +413,9 @@ Saving objects To save an object back to the database, call ``save()``: .. method:: Model.save(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) +.. method:: Model.asave(force_insert=False, force_update=False, using=DEFAULT_DB_ALIAS, update_fields=None) + +*Asynchronous version*: ``asave()`` For details on using the ``force_insert`` and ``force_update`` arguments, see :ref:`ref-models-force-insert`. Details about the ``update_fields`` argument @@ -416,6 +426,10 @@ method. See :ref:`overriding-model-methods` for more details. The model save process also has some subtleties; see the sections below. +.. versionchanged:: 4.2 + + ``asave()`` method was added. + Auto-incrementing primary keys ------------------------------ @@ -644,6 +658,9 @@ Deleting objects ================ .. method:: Model.delete(using=DEFAULT_DB_ALIAS, keep_parents=False) +.. method:: Model.adelete(using=DEFAULT_DB_ALIAS, keep_parents=False) + +*Asynchronous version*: ``adelete()`` Issues an SQL ``DELETE`` for the object. This only deletes the object in the database; the Python instance will still exist and will still have data in @@ -660,6 +677,10 @@ Sometimes with :ref:`multi-table inheritance ` you may want to delete only a child model's data. Specifying ``keep_parents=True`` will keep the parent model's data. +.. versionchanged:: 4.2 + + ``adelete()`` method was added. + Pickling objects ================ diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 4f7bda70807..ba01bf12e5a 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -239,6 +239,10 @@ Models * :class:`F() ` expressions that output ``BooleanField`` can now be negated using ``~F()`` (inversion operator). +* ``Model`` now provides asynchronous versions of some methods that use the + database, using an ``a`` prefix: :meth:`~.Model.adelete`, + :meth:`~.Model.arefresh_from_db`, and :meth:`~.Model.asave`. + Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/async.txt b/docs/topics/async.txt index 9e2785c351f..2b9b1a85d9e 100644 --- a/docs/topics/async.txt +++ b/docs/topics/async.txt @@ -91,10 +91,20 @@ Detailed notes can be found in :ref:`async-queries`, but in short: * ``async for`` is supported on all QuerySets (including the output of ``values()`` and ``values_list()``.) +Django also supports some asynchronous model methods that use the database:: + + async def make_book(...): + book = Book(...) + await book.asave(using="secondary") + Transactions do not yet work in async mode. If you have a piece of code that needs transactions behavior, we recommend you write that piece as a single synchronous function and call it using :func:`sync_to_async`. +.. versionchanged:: 4.2 + + Asynchronous model interface was added. + Performance ----------- diff --git a/tests/async/test_async_model_methods.py b/tests/async/test_async_model_methods.py new file mode 100644 index 00000000000..94e0370e356 --- /dev/null +++ b/tests/async/test_async_model_methods.py @@ -0,0 +1,25 @@ +from django.test import TestCase + +from .models import SimpleModel + + +class AsyncModelOperationTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.s1 = SimpleModel.objects.create(field=0) + + async def test_asave(self): + self.s1.field = 10 + await self.s1.asave() + refetched = await SimpleModel.objects.aget() + self.assertEqual(refetched.field, 10) + + async def test_adelete(self): + await self.s1.adelete() + count = await SimpleModel.objects.acount() + self.assertEqual(count, 0) + + async def test_arefresh_from_db(self): + await SimpleModel.objects.filter(pk=self.s1.pk).aupdate(field=20) + await self.s1.arefresh_from_db() + self.assertEqual(self.s1.field, 20)