From 331d1b433dfbc975cfa6b6069379e6ed329b06e0 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Wed, 18 Sep 2024 08:37:02 -0400 Subject: [PATCH 1/6] prototype for `UpdateSelf` --- piccolo/table.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/piccolo/table.py b/piccolo/table.py index af0fed2fe..a781f3c94 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -525,6 +525,12 @@ def save( == getattr(self, self._meta.primary_key._meta.name) ) + def update_self( + self, + values: t.Optional[t.Dict[t.Union[Column, str], t.Any]] = None, + ): + pass + def remove(self) -> Delete: """ A proxy to a delete query. From e8bc31961e1fd15b391621e3a7c9e7c59ca7f7c8 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Thu, 19 Sep 2024 07:45:08 -0400 Subject: [PATCH 2/6] fleshed out implementation --- piccolo/query/methods/objects.py | 88 ++++++++++++++++++++++++++++++++ piccolo/table.py | 9 ++-- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/piccolo/query/methods/objects.py b/piccolo/query/methods/objects.py index 7f2b5aaed..725778527 100644 --- a/piccolo/query/methods/objects.py +++ b/piccolo/query/methods/objects.py @@ -27,6 +27,7 @@ if t.TYPE_CHECKING: # pragma: no cover from piccolo.columns import Column + from piccolo.table import Table ############################################################################### @@ -173,6 +174,93 @@ def run_sync(self, *args, **kwargs) -> TableInstance: return run_sync(self.run(*args, **kwargs)) +class UpdateSelf: + """ + This allows the user to update a single object - useful when the values + are derived from the database in some way. + + For example, if we have the following table:: + + class Concert(Table): + name = Varchar(unique=True) + tickets_available = Integer() + + And we fetch an object:: + + >>> concert = await Concert.objects().get(name="Amazing concert") + + We could use the typical syntax for updating the object:: + + >>> concert.tickets_available += -1 + >>> await concert.save() + + The problem with this, is what if another object has already decremented + ``tickets_available``? It would overide the value. + + Instead we can do this: + + >>> await concert.update_self({ + ... Concert.tickets_available: Concert.tickets_available - 1 + ... }) + + This updates ``tickets_available`` in the database, and also sets the + new value for ``tickets_available`` on the object. + + """ + + def __init__( + self, + row: Table, + values: t.Dict[t.Union[Column, str], t.Any], + ): + self.row = row + self.values = values + + async def run( + self, + node: t.Optional[str] = None, + in_pool: bool = True, + ) -> None: + if not self.row._exists_in_db: + raise ValueError("This row doesn't exist in the database.") + + TableClass = self.row.__class__ + + primary_key = TableClass._meta.primary_key + primary_key_value = getattr(self.row, primary_key._meta.name) + + if primary_key_value is None: + raise ValueError("The primary key is None") + + columns = [ + TableClass._meta.get_column_by_name(i) if isinstance(i, str) else i + for i in self.values.keys() + ] + + response = ( + await TableClass.update(self.values) + .where(primary_key == primary_key_value) + .returning(*columns) + .run( + node=node, + in_pool=in_pool, + ) + ) + + for key, value in response[0].items(): + setattr(self.row, key, value) + + def __await__(self) -> t.Generator[None, None, None]: + """ + If the user doesn't explicity call .run(), proxy to it as a + convenience. + """ + return self.run().__await__() + + def run_sync(self, *args, **kwargs) -> None: + return run_sync(self.run(*args, **kwargs)) + + ############################################################################### diff --git a/piccolo/table.py b/piccolo/table.py index a781f3c94..2bdcf4f90 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -46,7 +46,7 @@ ) from piccolo.query.methods.create_index import CreateIndex from piccolo.query.methods.indexes import Indexes -from piccolo.query.methods.objects import First +from piccolo.query.methods.objects import First, UpdateSelf from piccolo.query.methods.refresh import Refresh from piccolo.querystring import QueryString from piccolo.utils import _camel_to_snake @@ -526,10 +526,9 @@ def save( ) def update_self( - self, - values: t.Optional[t.Dict[t.Union[Column, str], t.Any]] = None, - ): - pass + self, values: t.Dict[t.Union[Column, str], t.Any] + ) -> UpdateSelf: + return UpdateSelf(row=self, values=values) def remove(self) -> Delete: """ From 497fefefa46cf165da5c16959a187c0d80ce2de6 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Thu, 19 Sep 2024 07:49:22 -0400 Subject: [PATCH 3/6] add tests --- tests/table/test_update_self.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/table/test_update_self.py diff --git a/tests/table/test_update_self.py b/tests/table/test_update_self.py new file mode 100644 index 000000000..c06afe708 --- /dev/null +++ b/tests/table/test_update_self.py @@ -0,0 +1,27 @@ +from piccolo.testing.test_case import AsyncTableTest +from tests.example_apps.music.tables import Band, Manager + + +class TestUpdateSelf(AsyncTableTest): + + tables = [Band, Manager] + + async def test_update_self(self): + band = Band({Band.name: "Pythonistas", Band.popularity: 1000}) + + # Make sure we get a ValueError if it's not in the database yet. + with self.assertRaises(ValueError): + await band.update_self({Band.popularity: Band.popularity + 1}) + + # Save it, so it's in the database + await band.save() + + # Make sure we can successfully update the object + await band.update_self({Band.popularity: Band.popularity + 1}) + + # Make sure the value was updated on the object + assert band.popularity == 1001 + + # Make sure the value was updated in the database + await band.refresh() + assert band.popularity == 1001 From 7109bdba0f771aea46b3892c54c129d2dcb475b1 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Fri, 20 Sep 2024 19:30:35 -0400 Subject: [PATCH 4/6] update docstring - use `Band` table as an example --- piccolo/query/methods/objects.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/piccolo/query/methods/objects.py b/piccolo/query/methods/objects.py index 725778527..4f8df3b21 100644 --- a/piccolo/query/methods/objects.py +++ b/piccolo/query/methods/objects.py @@ -181,30 +181,30 @@ class UpdateSelf: For example, if we have the following table:: - class Concert(Table): - name = Varchar(unique=True) - tickets_available = Integer() + class Band(Table): + name = Varchar() + popularity = Integer() And we fetch an object:: - >>> concert = await Concert.objects().get(name="Amazing concert") + >>> band = await Band.objects().get(name="Pythonistas") We could use the typical syntax for updating the object:: - >>> concert.tickets_available += -1 - >>> await concert.save() + >>> band.popularity += 1 + >>> await band.save() - The problem with this, is what if another object has already decremented - ``tickets_available``? It would overide the value. + The problem with this, is what if another object has already incremented + ``popularity``? It would overide the value. Instead we can do this: - >>> await concert.update_self({ - ... Concert.tickets_available: Concert.tickets_available - 1 + >>> await band.update_self({ + ... Band.popularity: Band.popularity + 1 ... }) - This updates ``tickets_available`` in the database, and also sets the - new value for ``tickets_available`` on the object. + This updates ``popularity`` in the database, and also sets the new value + for ``popularity`` on the object. """ From c6cdc812766fb9c5d57950ee22d8a65aab8a87c4 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Fri, 20 Sep 2024 19:47:31 -0400 Subject: [PATCH 5/6] improve docs --- docs/src/piccolo/query_types/objects.rst | 35 ++++++++++++++++++------ piccolo/query/methods/objects.py | 32 ---------------------- piccolo/table.py | 32 ++++++++++++++++++++++ 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/docs/src/piccolo/query_types/objects.rst b/docs/src/piccolo/query_types/objects.rst index 968aefcab..9009a37f4 100644 --- a/docs/src/piccolo/query_types/objects.rst +++ b/docs/src/piccolo/query_types/objects.rst @@ -78,6 +78,9 @@ We also have this shortcut which combines the above into a single line: Updating objects ---------------- +``save`` +~~~~~~~~ + Objects have a :meth:`save ` method, which is convenient for updating values: @@ -95,6 +98,22 @@ convenient for updating values: # Or specify specific columns to save: await band.save([Band.popularity]) +``update_self`` +~~~~~~~~~~~~~~~ + +The ``save`` method is fine in the majority of cases, but if you want to set +a new value on the object, based on something in the database, you can use +the :meth:`update_self ` method instead. + +For example, if we want to increment the popularity value in the database, and +assign the new value to the object, we can do this: + +.. code-block:: python + + await band.update_self({ + Band.popularity: Band.popularity + 1 + }).first() + ------------------------------------------------------------------------------- Deleting objects @@ -115,8 +134,8 @@ Similarly, we can delete objects, using the ``remove`` method. Fetching related objects ------------------------ -get_related -~~~~~~~~~~~ +``get_related`` +~~~~~~~~~~~~~~~ If you have an object from a table with a :class:`ForeignKey ` column, and you want to fetch the related row as an object, you can do so @@ -195,8 +214,8 @@ prefer. ------------------------------------------------------------------------------- -get_or_create -------------- +``get_or_create`` +----------------- With ``get_or_create`` you can get an existing record matching the criteria, or create a new one with the ``defaults`` arguments: @@ -239,8 +258,8 @@ Complex where clauses are supported, but only within reason. For example: ------------------------------------------------------------------------------- -to_dict -------- +``to_dict`` +----------- If you need to convert an object into a dictionary, you can do so using the ``to_dict`` method. @@ -264,8 +283,8 @@ the columns: ------------------------------------------------------------------------------- -refresh -------- +``refresh`` +----------- If you have an object which has gotten stale, and want to refresh it, so it has the latest data from the database, you can use the diff --git a/piccolo/query/methods/objects.py b/piccolo/query/methods/objects.py index 4f8df3b21..878116099 100644 --- a/piccolo/query/methods/objects.py +++ b/piccolo/query/methods/objects.py @@ -175,38 +175,6 @@ def run_sync(self, *args, **kwargs) -> TableInstance: class UpdateSelf: - """ - This allows the user to update a single object - useful when the values - are derived from the database in some way. - - For example, if we have the following table:: - - class Band(Table): - name = Varchar() - popularity = Integer() - - And we fetch an object:: - - >>> band = await Band.objects().get(name="Pythonistas") - - We could use the typical syntax for updating the object:: - - >>> band.popularity += 1 - >>> await band.save() - - The problem with this, is what if another object has already incremented - ``popularity``? It would overide the value. - - Instead we can do this: - - >>> await band.update_self({ - ... Band.popularity: Band.popularity + 1 - ... }) - - This updates ``popularity`` in the database, and also sets the new value - for ``popularity`` on the object. - - """ def __init__( self, diff --git a/piccolo/table.py b/piccolo/table.py index 2bdcf4f90..b50855f95 100644 --- a/piccolo/table.py +++ b/piccolo/table.py @@ -528,6 +528,38 @@ def save( def update_self( self, values: t.Dict[t.Union[Column, str], t.Any] ) -> UpdateSelf: + """ + This allows the user to update a single object - useful when the values + are derived from the database in some way. + + For example, if we have the following table:: + + class Band(Table): + name = Varchar() + popularity = Integer() + + And we fetch an object:: + + >>> band = await Band.objects().get(name="Pythonistas") + + We could use the typical syntax for updating the object:: + + >>> band.popularity += 1 + >>> await band.save() + + The problem with this, is what if another object has already + incremented ``popularity``? It would overide the value. + + Instead we can do this: + + >>> await band.update_self({ + ... Band.popularity: Band.popularity + 1 + ... }) + + This updates ``popularity`` in the database, and also sets the new + value for ``popularity`` on the object. + + """ return UpdateSelf(row=self, values=values) def remove(self) -> Delete: From ce6bc690a781a6491aeee50dc37a4ae0779d4583 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Fri, 20 Sep 2024 19:58:28 -0400 Subject: [PATCH 6/6] finish docs --- docs/src/piccolo/query_types/objects.rst | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/src/piccolo/query_types/objects.rst b/docs/src/piccolo/query_types/objects.rst index 9009a37f4..c8d65d923 100644 --- a/docs/src/piccolo/query_types/objects.rst +++ b/docs/src/piccolo/query_types/objects.rst @@ -101,18 +101,32 @@ convenient for updating values: ``update_self`` ~~~~~~~~~~~~~~~ -The ``save`` method is fine in the majority of cases, but if you want to set -a new value on the object, based on something in the database, you can use -the :meth:`update_self ` method instead. +The :meth:`save ` method is fine in the majority of +cases, but there are some situations where the :meth:`update_self ` +method is preferable. -For example, if we want to increment the popularity value in the database, and -assign the new value to the object, we can do this: +For example, if we want to increment the ``popularity`` value, we can do this: .. code-block:: python await band.update_self({ Band.popularity: Band.popularity + 1 - }).first() + }) + +Which does the following: + +* Increments the popularity in the database +* Assigns the new value to the object + +This is safer than: + +.. code-block:: python + + band.popularity += 1 + await band.save() + +Because ``update_self`` increments the current ``popularity`` value in the +database, not the one on the object, which might be out of date. -------------------------------------------------------------------------------