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

cosmos spring data - patch support #32630

Merged
merged 29 commits into from
Jan 12, 2023
Merged

cosmos spring data - patch support #32630

merged 29 commits into from
Jan 12, 2023

Conversation

TheovanKraay
Copy link
Member

@TheovanKraay TheovanKraay commented Dec 16, 2022

Description

This PR adds an overload to Respository.save() method that allows entity to be patched (partial update). When this is called, only the fields defined in CosmosPatchOperations will be sent to Cosmos DB for update.

Note: RU cost for patch() is equivalent to the RU for replace() because the physics in terms of physical resources (cpu, memory, etc.) are roughly the same. The benefits from using patch come from being able to limit the number of updated fields sent over the wire, avoiding the need to retrieve the to-be-patched record from the database before patching, and avoiding the need to manually handle etag contention from concurrent updates.

CosmosPatchOperations patchOperations = CosmosPatchOperations
    .create()
    .replace("/size", 5)
    .add("/color", "blue");

User patchedUser = userRepository.save(user.getId(), new PartitionKey(user.getLastName()), User.class, patchOperations);

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, see this page.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

* @return the patched entity
* @throws IllegalArgumentException in case the given {@code id} is {@literal null}.
*/
<S extends T> S save(S entityToPatch, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options);
Copy link
Member

Choose a reason for hiding this comment

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

should we also introduce an API without the requestOptions? save(S entityToPatch, CosmosPatchOperations patchOperations);

Copy link
Member

Choose a reason for hiding this comment

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

No we will not, we want to maintain the least amount of API functions possible. Allowing the requestOptions to be null is acceptable and follows our best practices in Spring.

public void testPatchPreConditionSuccess() {
options.setFilterPredicate("FROM person p WHERE p.lastName = '"+LAST_NAME+"'");
Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options);
assertEquals(patchedPerson.getAge(), patchedPerson.getAge());
Copy link
Member

Choose a reason for hiding this comment

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

This check will always succeed as you are checking "a = a". Let's change the second param to the "Age" variable.

Copy link
Member Author

Choose a reason for hiding this comment

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

Corrected this

try {
options.setFilterPredicate("FROM person p WHERE p.lastName = 'dummy'");
Person patchedPerson = cosmosTemplate.patch(insertedPerson.getId(), new PartitionKey(insertedPerson.getLastName()), Person.class, operations, options);
assertEquals(patchedPerson.getAge(), patchedPerson.getAge());
Copy link
Member

Choose a reason for hiding this comment

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

Same as above.

Copy link
Member Author

Choose a reason for hiding this comment

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

corrected

@TheovanKraay
Copy link
Member Author

/azp run java - cosmos - tests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@TheovanKraay
Copy link
Member Author

/check-enforcer override

* @param <T> type class of domain type
* @return the patched item
*/
<T> T patch(Object id, PartitionKey partitionKey, Class<T> domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options);
Copy link
Member

Choose a reason for hiding this comment

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

If this has been discussed in the API review please just add brief comment and resolve this. But why are we adding an overload with request options here when none of the other operations allow specifying request options? I am not against it - just curious why we now decide t add it and skipped it for all other operations? Is there a more common scenario that would need it? Or is the idea to eventually add overload to other operations with request options as well?

Copy link
Member Author

@TheovanKraay TheovanKraay Jan 11, 2023

Choose a reason for hiding this comment

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

Most of the discussion was around whether to add an overload, or force caller to pass null (we went with overload, so that the method call in most cases will be less verbose). But I think here you are asking why we add request options at all since we don't expose that elsewhere, which is a good question? It is purely to facilitate setFilterPredicate for patch. If we generally don't want to expose request options, then I guess we should encapsulate this and just have a string parameter in the overload, something like String filterPredicate.

Copy link
Member

Choose a reason for hiding this comment

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

my 2c here is to avoid adding overloads with individual parameters and keep one with options if needed.

Copy link
Member

Choose a reason for hiding this comment

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

@lmolkova - actually that's what we had earlier :)
We had one API with requestOptions included and requestOptions can be nullable / optional. However, we got initial customer feedback from the PR review that if they need to pass in null, why are we forcing them to pass null and we should expose one API for simple use case. That's why we switched back to having 2 APIs.

Copy link
Member

Choose a reason for hiding this comment

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

two thanks for the clarification, @kushagraThapar ! Two methods are fine (one with options, one without). I meant that the verbose one (with extra options), should take an extendable options class and not an individual filterPredicate parameter.

Copy link
Member

Choose a reason for hiding this comment

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

I think that is just in the example / doc on the API, not an actual API itself, no?

* @param <T> type class of domain type
* @return Mono with the patched item
*/
<T> Mono<T> patch(Object id, PartitionKey partitionKey, Class<T> domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options);
Copy link
Member

Choose a reason for hiding this comment

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

See comment in CosmosOperations

@FabianMeiswinkel
Copy link
Member

and avoiding possible etag contention from highly concurrent updates
This statement isn't true - the etag condition failures will just result in 449 which will be automatically retried - but it doesn't allow for any higher contention

Thanks for catching this! Is it sufficient to replace the word “avoiding” with “mitigating”? The point being that afaik 449s aren’t retried at all if doing read/replace vs patch - so this is still a benefit of using patch, correct?

Sorry - I seem to have been misunderstood - when using read+replace under high concurrent access you pass the etad form the read as pre-condition failure to the replace and would get a 420 if somebody else updated the document. This needs to be manually retried by the app. With patch instead you would get a 449 if the backend hits the same pre-codnition failure. And because the 449 is retried automatically the dev does not need to do anything. But if there is high contention it would still result in unreasonable latency etc. Bottom-line the contention you can achieve is the same - but concurrent updates (with low contention risk) are easier with patch because the headache of the 412 etag-precondition dance is done). My main point was that we should not set the expectation that highly concurrent updates are a good idea ever with cosmos db. patch or read+replace

Ok - how about "and avoiding the need to manually handle etag contention from highly concurrent updates"? Or you prefer that we remove this completely so as to avoid the suggestion that somehow it will be better with patch?
How about "and avoiding the need to manually handle etag contention from concurrent updates"

@TheovanKraay
Copy link
Member Author

/check-enforcer override

@TheovanKraay
Copy link
Member Author

/check-enforcer override

* @param <T> type class of domain type
* @return the patched item
*/
<T> T patch(Object id, PartitionKey partitionKey, Class<T> domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options);
Copy link
Member

Choose a reason for hiding this comment

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

my 2c here is to avoid adding overloads with individual parameters and keep one with options if needed.

* patches item
* @param id must not be {@literal null}
* @param partitionKey must not be {@literal null}
* @param domainType must not be {@literal null}
Copy link
Member

Choose a reason for hiding this comment

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

would be nice to add a description on what domainType is

Copy link
Member

Choose a reason for hiding this comment

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

agreed.

Copy link
Member

@kushagraThapar kushagraThapar left a comment

Choose a reason for hiding this comment

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

Looks great @TheovanKraay
We should add more tests to repository tests, like AddressRepositoryIT, etc.

* @param <T> type class of domain type
* @return the patched item
*/
<T> T patch(Object id, PartitionKey partitionKey, Class<T> domainType, CosmosPatchOperations patchOperations, CosmosPatchItemRequestOptions options);
Copy link
Member

Choose a reason for hiding this comment

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

@lmolkova - actually that's what we had earlier :)
We had one API with requestOptions included and requestOptions can be nullable / optional. However, we got initial customer feedback from the PR review that if they need to pass in null, why are we forcing them to pass null and we should expose one API for simple use case. That's why we switched back to having 2 APIs.

* patches item
* @param id must not be {@literal null}
* @param partitionKey must not be {@literal null}
* @param domainType must not be {@literal null}
Copy link
Member

Choose a reason for hiding this comment

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

agreed.

@TheovanKraay
Copy link
Member Author

Looks great @TheovanKraay We should add more tests to repository tests, like AddressRepositoryIT, etc.

Added

@trande4884
Copy link
Member

/azp run java - cosmos - tests

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@trande4884
Copy link
Member

/check-enforcer override

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
azure-spring All azure-spring related issues Cosmos
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Spring Cosmos - Support patch/partial update
7 participants