Skip to content

Commit e36a9ed

Browse files
authored
1073 Add update_self method (#1081)
* prototype for `UpdateSelf` * fleshed out implementation * add tests * update docstring - use `Band` table as an example * improve docs * finish docs
1 parent df86c2e commit e36a9ed

File tree

4 files changed

+162
-9
lines changed

4 files changed

+162
-9
lines changed

docs/src/piccolo/query_types/objects.rst

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ We also have this shortcut which combines the above into a single line:
7878
Updating objects
7979
----------------
8080

81+
``save``
82+
~~~~~~~~
83+
8184
Objects have a :meth:`save <piccolo.table.Table.save>` method, which is
8285
convenient for updating values:
8386

@@ -95,6 +98,36 @@ convenient for updating values:
9598
# Or specify specific columns to save:
9699
await band.save([Band.popularity])
97100
101+
``update_self``
102+
~~~~~~~~~~~~~~~
103+
104+
The :meth:`save <piccolo.table.Table.save>` method is fine in the majority of
105+
cases, but there are some situations where the :meth:`update_self <piccolo.table.Table.update_self>`
106+
method is preferable.
107+
108+
For example, if we want to increment the ``popularity`` value, we can do this:
109+
110+
.. code-block:: python
111+
112+
await band.update_self({
113+
Band.popularity: Band.popularity + 1
114+
})
115+
116+
Which does the following:
117+
118+
* Increments the popularity in the database
119+
* Assigns the new value to the object
120+
121+
This is safer than:
122+
123+
.. code-block:: python
124+
125+
band.popularity += 1
126+
await band.save()
127+
128+
Because ``update_self`` increments the current ``popularity`` value in the
129+
database, not the one on the object, which might be out of date.
130+
98131
-------------------------------------------------------------------------------
99132

100133
Deleting objects
@@ -115,8 +148,8 @@ Similarly, we can delete objects, using the ``remove`` method.
115148
Fetching related objects
116149
------------------------
117150

118-
get_related
119-
~~~~~~~~~~~
151+
``get_related``
152+
~~~~~~~~~~~~~~~
120153

121154
If you have an object from a table with a :class:`ForeignKey <piccolo.columns.column_types.ForeignKey>`
122155
column, and you want to fetch the related row as an object, you can do so
@@ -195,8 +228,8 @@ prefer.
195228
196229
-------------------------------------------------------------------------------
197230

198-
get_or_create
199-
-------------
231+
``get_or_create``
232+
-----------------
200233

201234
With ``get_or_create`` you can get an existing record matching the criteria,
202235
or create a new one with the ``defaults`` arguments:
@@ -239,8 +272,8 @@ Complex where clauses are supported, but only within reason. For example:
239272
240273
-------------------------------------------------------------------------------
241274

242-
to_dict
243-
-------
275+
``to_dict``
276+
-----------
244277

245278
If you need to convert an object into a dictionary, you can do so using the
246279
``to_dict`` method.
@@ -264,8 +297,8 @@ the columns:
264297
265298
-------------------------------------------------------------------------------
266299

267-
refresh
268-
-------
300+
``refresh``
301+
-----------
269302

270303
If you have an object which has gotten stale, and want to refresh it, so it
271304
has the latest data from the database, you can use the

piccolo/query/methods/objects.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
if t.TYPE_CHECKING: # pragma: no cover
2929
from piccolo.columns import Column
30+
from piccolo.table import Table
3031

3132

3233
###############################################################################
@@ -173,6 +174,61 @@ def run_sync(self, *args, **kwargs) -> TableInstance:
173174
return run_sync(self.run(*args, **kwargs))
174175

175176

177+
class UpdateSelf:
178+
179+
def __init__(
180+
self,
181+
row: Table,
182+
values: t.Dict[t.Union[Column, str], t.Any],
183+
):
184+
self.row = row
185+
self.values = values
186+
187+
async def run(
188+
self,
189+
node: t.Optional[str] = None,
190+
in_pool: bool = True,
191+
) -> None:
192+
if not self.row._exists_in_db:
193+
raise ValueError("This row doesn't exist in the database.")
194+
195+
TableClass = self.row.__class__
196+
197+
primary_key = TableClass._meta.primary_key
198+
primary_key_value = getattr(self.row, primary_key._meta.name)
199+
200+
if primary_key_value is None:
201+
raise ValueError("The primary key is None")
202+
203+
columns = [
204+
TableClass._meta.get_column_by_name(i) if isinstance(i, str) else i
205+
for i in self.values.keys()
206+
]
207+
208+
response = (
209+
await TableClass.update(self.values)
210+
.where(primary_key == primary_key_value)
211+
.returning(*columns)
212+
.run(
213+
node=node,
214+
in_pool=in_pool,
215+
)
216+
)
217+
218+
for key, value in response[0].items():
219+
setattr(self.row, key, value)
220+
221+
def __await__(self) -> t.Generator[None, None, None]:
222+
"""
223+
If the user doesn't explicity call .run(), proxy to it as a
224+
convenience.
225+
"""
226+
return self.run().__await__()
227+
228+
def run_sync(self, *args, **kwargs) -> None:
229+
return run_sync(self.run(*args, **kwargs))
230+
231+
176232
###############################################################################
177233

178234

piccolo/table.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
)
4747
from piccolo.query.methods.create_index import CreateIndex
4848
from piccolo.query.methods.indexes import Indexes
49-
from piccolo.query.methods.objects import First
49+
from piccolo.query.methods.objects import First, UpdateSelf
5050
from piccolo.query.methods.refresh import Refresh
5151
from piccolo.querystring import QueryString
5252
from piccolo.utils import _camel_to_snake
@@ -525,6 +525,43 @@ def save(
525525
== getattr(self, self._meta.primary_key._meta.name)
526526
)
527527

528+
def update_self(
529+
self, values: t.Dict[t.Union[Column, str], t.Any]
530+
) -> UpdateSelf:
531+
"""
532+
This allows the user to update a single object - useful when the values
533+
are derived from the database in some way.
534+
535+
For example, if we have the following table::
536+
537+
class Band(Table):
538+
name = Varchar()
539+
popularity = Integer()
540+
541+
And we fetch an object::
542+
543+
>>> band = await Band.objects().get(name="Pythonistas")
544+
545+
We could use the typical syntax for updating the object::
546+
547+
>>> band.popularity += 1
548+
>>> await band.save()
549+
550+
The problem with this, is what if another object has already
551+
incremented ``popularity``? It would overide the value.
552+
553+
Instead we can do this:
554+
555+
>>> await band.update_self({
556+
... Band.popularity: Band.popularity + 1
557+
... })
558+
559+
This updates ``popularity`` in the database, and also sets the new
560+
value for ``popularity`` on the object.
561+
562+
"""
563+
return UpdateSelf(row=self, values=values)
564+
528565
def remove(self) -> Delete:
529566
"""
530567
A proxy to a delete query.

tests/table/test_update_self.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from piccolo.testing.test_case import AsyncTableTest
2+
from tests.example_apps.music.tables import Band, Manager
3+
4+
5+
class TestUpdateSelf(AsyncTableTest):
6+
7+
tables = [Band, Manager]
8+
9+
async def test_update_self(self):
10+
band = Band({Band.name: "Pythonistas", Band.popularity: 1000})
11+
12+
# Make sure we get a ValueError if it's not in the database yet.
13+
with self.assertRaises(ValueError):
14+
await band.update_self({Band.popularity: Band.popularity + 1})
15+
16+
# Save it, so it's in the database
17+
await band.save()
18+
19+
# Make sure we can successfully update the object
20+
await band.update_self({Band.popularity: Band.popularity + 1})
21+
22+
# Make sure the value was updated on the object
23+
assert band.popularity == 1001
24+
25+
# Make sure the value was updated in the database
26+
await band.refresh()
27+
assert band.popularity == 1001

0 commit comments

Comments
 (0)