Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exposing control over return_values in Model.update() #1050

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

chkothe
Copy link

@chkothe chkothe commented May 26, 2022

Problem

Currently, invoking .update() on a model will always read back the entire document from the DB with no option to override this behavior. If one's object is very large (e.g., contains long lists), one may not wish to receive everything (or anything at all) back after performing an update (e.g., in the common use case of a REST API handler). That's a use case for us at Intheon.

DynamoDB has of course a feature for that, the ReturnValues request parameter (which can take the values NONE, ALL_NEW, ALL_OLD, UPDATED_NEW and UPDATED_OLD), and that feature is already exposed in PynamoDB, but so far only in the low-level TableConnection.update_item() method. Unfortunately, falling back to that in user code would require the user to duplicate some internal PynamoDB logic and/or to invoke non-public methods of the model, so it's not really an alternative that can be recommended.

This PR

Currently, Model.update() simply hard-codes this value to ALL_NEW. This PR proposes to make that a new parameter instead, which can be overridden as in item.update(actions=[...], read_back=NONE) to read nothing back from the DB. At this time, the PR is trivial and permits only either ALL_NEW (default, and equivalent to the current behavior) or NONE, which reads nothing back and consequently returns None and leaves the local item unaltered.

I've pondered whether it should be called return_values instead, but that seems rather confusing to the user, who may assume that the flag affects only the return values of the update() method, while the local item might still be updated in-place as usual. Calling it read_back makes that clearer since it talks about what information comes back from the DB. That interpretation also holds up when in the future someone decides to add support for other DynamoDB modes like ALL_OLD, in which case we'd expect that the local copy will be populated with whatever the DB state was before the update happened (rather than only the return value receiving the old data).

While this PR is very simple, in the future someone could implement support for partial updates of the local record (via UPDATED_NEW), and this argument would allow the user to selectively choose one read-back mode or another, which would be an application-dependent choice (e.g., in the presence of concurrency), so there's some future-proofing in there.

A unit test that checks these semantics is provided.

:raises ModelInstance.DoesNotExist: if the object to be updated does not exist
:raises pynamodb.exceptions.UpdateError: if the `condition` is not met
"""
if not isinstance(actions, list) or len(actions) == 0:
raise TypeError("the value of `actions` is expected to be a non-empty list")
if read_back not in (ALL_NEW, NONE):
raise ValueError("expected `read_back` to be `ALL_NEW` or `NONE`, but was: {}".format(read_back))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise ValueError("expected `read_back` to be `ALL_NEW` or `NONE`, but was: {}".format(read_back))
raise ValueError(f"expected `read_back` to be `ALL_NEW` or `NONE`, but was: '{read_back}'")

@@ -921,6 +921,28 @@ def test_update(self, mock_time):
assert item.views is None
self.assertEqual({'bob'}, item.custom_aliases)

def test_update_readback(self):
self.init_table_meta(SimpleUserModel, SIMPLE_MODEL_TABLE_DATA)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.init_table_meta(SimpleUserModel, SIMPLE_MODEL_TABLE_DATA)

No longer needed 😅

'ExpressionAttributeValues': {':0': {'S': 'changed@example.com'}},
'ReturnConsumedCapacity': 'TOTAL'
}
args = req.call_args[0][1]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
args = req.call_args[0][1]
args = req.call_args[0].args

actions: List[Action],
condition: Optional[Condition] = None,
settings: OperationSettings = OperationSettings.default,
read_back: str = ALL_NEW,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should make it an enum. This library isn't awfully modern, but maybe there's an opportunity to change that (especially as we're establishing a new API here).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say an enum is a better choice, too. Let's start to use strong typing whenever possible.

@ikonst
Copy link
Contributor

ikonst commented Nov 5, 2022

Sorry for overlooking this PR for so long. This is a very valid addition!

@ikonst
Copy link
Contributor

ikonst commented Feb 6, 2023

I definitely should merge this before 6.x.

@ikonst ikonst added this to the 6.0 milestone Feb 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants