Transaction Support for Spring Data Couchbase.
Closes #1145.
commit e2df8e73777a80ea71c15d77dd51252f6766fa94
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Mon Jun 27 18:29:23 2022 -0700
Rebased and tidied transaction branch.
In addition to transaction support, some inconsitencies in
fluent APIs were remedied.
Documentation to follow.
Closes #1145.
commit e20ed0814e770e26ec34634429f3ac7710acd8d3
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Fri Jun 24 17:21:45 2022 -0700
Rebase with main.
commit edadede7a01997a364c1f5169a36e8bb3e4eee11
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Fri Jun 24 10:45:21 2022 -0700
Add support for Couchbase Transactions.
The fluent operations are common for options that are common to operations
with and without transactions. Once there is a transaction(ctx), or an
option specific to without-transactions (such as scanConsistency), the
interfaces are bifurcated, so that an non-transaction option cannot be
applied to a transaction operation, and a transaction(ctx) cannot be
applied where a non-transaction option has already been applied.
Closes #1145.
Support for @Transactional for blocking and reactive Transactionmanager.
Closes 1145.
Transaction Support.
Transaction Support.
Porting to SDK-integrated version of transactions
The transactions logic exists in the Java SDK as of 3.3.0,
with a slightly different API.
This is the first effort at the port, which literally just
compiles. It will not run as crucial code has been
commented and todo-ed. There is work remaining to figure
out how to complete the port, as some crucial parts (such
as ctx.commit() and ctx.rollback()) have been intentionally
removed.
Continuing work to get the ExtSDKIntegration port working
Trying to transition to CallbackPreferring manager.
Added CouchbaseSimpleCallbackTransactionManager, the simplest
possible implementation of CallbackPreferringTransactionManager,
combined with a simpler approach to ThreadLocal storage in
ReactiveInsertByIdSupport.
Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now
passing.
Adding WIP get-and-replace @Transactional support
(Not yet working as CAS/version field in
Person is not populated correctly.)
Datacouch 1145 transaction support (#1423)
* Porting to SDK-integrated version of transactions
The transactions logic exists in the Java SDK as of 3.3.0,
with a slightly different API.
This is the first effort at the port, which literally just
compiles. It will not run as crucial code has been
commented and todo-ed. There is work remaining to figure
out how to complete the port, as some crucial parts (such
as ctx.commit() and ctx.rollback()) have been intentionally
removed.
* Continuing work to get the ExtSDKIntegration port working
Trying to transition to CallbackPreferring manager.
* Added CouchbaseSimpleCallbackTransactionManager, the simplest
possible implementation of CallbackPreferringTransactionManager,
combined with a simpler approach to ThreadLocal storage in
ReactiveInsertByIdSupport.
Test 'commitShouldPersistTxEntriesOfTxAnnotatedMethod' is now
passing.
* Adding WIP get-and-replace @Transactional support
(Not yet working as CAS/version field in
Person is not populated correctly.)
Commit before pulling Graham's changes.
Merge branch 'datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into datacouch_1145_transaction_support
Transitioning to use CoreTransactionAttemptContext.
Tests may fail.
Removing AttemptContextReactiveAccessor
Don't think we need this, as we can pass around
CoreTransactionAttemptContext instead, which gives
access to a lot of internals.
Removing TransactionsReactive
Would prefer not to C&P a huge class out of the transaction
internals, and don't think we need it.
Removing some files not currently used
To reduce & simplify the amount of code to look at.
Some don't seem to be used in any branch, some just
aren't used in this branch.
Removing CouchbaseTransactionInterceptor
As per offline discussion, CallbackPreferringPlatformTransactionManager
is perhaps the optimal solution.
Copying @Transactional tests out into separate class
Tidyup
Tidyup test names
Verify GenericSupport is on same thread before and after transactional operation
Refactoring CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage management
Using latest java-client
ReactiveReplaceByIdSupport - Fixing use of CAS now have CoreTransactionAttemptContext.
Removing unused code.
ReactiveInsertByIdSupport - fixing use of reactive vs non-reactive, and CAS
Merging upstream
Remove incorrect thread check (.doOnNext could execute on a different thread)
Get scope and collection from pseudoArgs and some cleanup.
Completing merge from upstream
Removing unused classes
Give GenericSupport a better name
Reject at runtime options that aren't supported in a transaction
Fixing some small todos, partly by removing unused coe
Fix runtime option checks
Simplifying CouchbaseSimpleCallbackTransactionManager ThreadLocalStorage
Standardising on ReactiveCouchbaseResourceHolder rather than
holding CoreTransactionAttemptContext too
Removing version from CouchbaseDocument
Can't recall why I added this, and tests pass without it
Improving CouchbaseTransactionalIntegrationTests and adding more tests
Reject operations that aren't allowed in a transaction (upsertById etc.)
Improve handling of CAS mismatch
By calling CoreTransactionAttemptContext.operationFailed,
it ensures that internal state is set. So even if
the user catches the exception, the transaction still behaves
as it should.
Removing a now-redundant non-transactional check on upsertById
I missed this when adding TransactionalSupport.verifyNotInTransaction
here.
Support @Transactional options timeout and isolation level
Add ReactiveTransactionWrapper/TransactionWrapper and a bunch of cleanup.
Merge branch 'datacouch_1145_transaction_support' of https://github.com/programmatix/spring-data-couchbase into programmatix-datacouch_1145_transaction_support
Fixed up merge issues.
Datacouch 1145 transaction support (#1447)
* Move CouchbaseTransactionalOperator to use SLF4J, same as
rest of the code.
* Handle all propagation levels
* Adding new tests for repository calls inside @Transactional
One test is failure due to what looks like a bug elsewhere.
* Rename CouchbaseTransactionalIntegrationTests, and check
after each test that we're not in a transaction.
Remove unnecessary methods From ReactiveCouchbaseClientFactory.
Also rationalized naming of methods and other changes.
Cleanup of test classes.
Datacouch 1145 transaction support (#1448)
* Move CouchbaseTransactionalOperator to use SLF4J, same as
rest of the code.
* Handle all propagation levels
* Adding new tests for repository calls inside @Transactional
One test is failure due to what looks like a bug elsewhere.
* Rename CouchbaseTransactionalIntegrationTests, and check
after each test that we're not in a transaction.
* Remove unnecessary methods From ReactiveCouchbaseClientFactory.
Also rationalized naming of methods and other changes.
* Cleanup of test classes.
* Removing unused classes
(Reducing the cognitive burden)
* Removing version from CouchbaseDocument
This change was done previously - it must have slipped back in
a merge.
* Adding and removing TODOs
* Adding and removing TODOs
* DRYing CouchbaseTransactionalPropagationIntegrationTests
* Check propagation tests retry as expected
* Tidy up PersonWithoutVersion to the minimum required
* Removing unused code
This should all be non-destructive. Just removing code IntelliJ
declares unused. Intent is to make it easier to figure out
how the CoreTransactionAttemptContext TLS is working.
* Adding tests for @Transactional removeByQuery and findByQuery
Failing as they aren't being executed transactionally - investigating why.
Co-authored-by: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
change references to resource holder
change refs to resource holder
Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support
Removing core transaction attempt context bound couchbase client factory rebased (#1449)
* Move ReactiveTransactionsWrapper tests into a new file
* (Temporarily?) disabling tests using CouchbaseTransactionOperation or TransactionalOperator
As I feel we should be removing/not-supporting these, and
on this branch I've broken them.
* Make all transaction tests call assertNotInTransaction
* Removing unused code
* Instead of binding the transaction AttemptContext to a
CouchbaseClientFactory, fetch it from ThreadLocalStorage
(or the reactive context) instead.
This allows a lot of simplifying:
* The non-trivial ReactiveCouchbaseClientUtils can be removed
* As can CoreTransactionAttemptContextBoundCouchbaseClientFactory
Also removing TransactionalSupport.one as it wasn't providing
as much DRY utility as I thought it would - only used in two
places.
This change won't compile on its own. To reduce the complexity
of this patchset, the Reactive*OperationSupport changes will
go into a separate commit.
* Reactive*OperationSupport changes to support the previous commit.
* Fixing ReactiveRemoveByQuerySupport.
Both to support the changes to TransactionalSupport.
And to fix the TODO where the query resuls were
not being handled.
* Disabling a test
* Adding CouchbaseTransactionsWrapperTemplateIntegrationTests
* Another advantage of removing CoreTransactionAttemptContextBoundCouchbaseClientFactory
is we can remove Cluster and ClusterInterface.
* Adding CouchbaseReactiveTransactionsWrapperTemplateIntegrationTests
Some of these tests are currently failing - tracking down where the
issue is.
Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support
manual merges for PR
manual merges for PR
Fix a bunch of test cases and remove unused bits.
Removing CouchbaseTransactionInterceptor
As discussed on Slack.
Reenabling some tests that are passing
(Unclear why these were disabled?)
Verified that CallbackPreferringPlatformTransactionManager getTransaction/commit/rollback are never called
Adding tests for TransactionTemplate, which works fine with CouchbaseSimpleCallbackTransactionManager
Whether we actually document this support is another matter - it's
yet another way of doing transactions.
Small fixes to support TransactionTemplate
* Handle if the user has set isRollbackOnly on the TransactionStatus
(which is only available - I think - when using TransactionTemplate)
* Supply a `transaction` object to CouchbaseTransactionStatus so that
status.isNewTransaction() correctly returns true. (This method requires
that transaction to be set.)
Clarifying that direct use of PlatformTransactionManager is not supported
Adding further TransactionTemplateIntegrationTests tests
Fixing removeByQuery queryOptions creation
Removing now-fixed TODO (no longer key off Cluster)
Adding overload to CouchbaseSimpleCallbackTransactionManager to allow
it to be constructed without a TransactionOptions.
Adding CouchbaseSimpleTransactionalOperator, the simplest possible
implementation of TransactionalOperator.
Adding retry tests
Just making sure that error handling and retries are done correctly throughout.
Updating and adding some TODOs
Have CouchbaseTransactionManager support CouchbaseResourceHolder.class binding
Adding more tests for CouchbaseTransactionManager.
These tests fail, and are known to fail. I'm adding them as a solid demonstration of
why I don't feel we can have this CouchbaseTransactionManager: it doesn't
provide the crucial 'core loop' functionality, including error handling
and retries. We should standardise on CouchbaseSimpleCallbackTransactionManager
instead.
CouchbaseSimpleCallbackTransactionManager.executeNewReactiveTransaction now
buffers results rather than trying to stream a Flux from out of a
completed lambda (which I doubt is even possible.)
Removing comment that has been resolved.
(As per Slack, we will live with this limitation.)
Adding CouchbaseSimpleTransactionInterceptor, a very
simple TransactionInterceptor implemention that
defers to CouchbaseSimpleCallbackTransactionManager
if that is the provided TransactionManager, and otherwise
just calls super.
This allows reactive @Transactional - though all
@Transactional methods including blocking will now flow
through it.
There are two rather divergent approaches in the code currently:
1. CouchbaseTransactionManager, ReactiveTransactionManager, CouchbaseTransactionalOperator, CouchbaseTransactionInterceptor
2. CouchbaseSimpleCallbackTransactionManager, CouchbaseSimpleTransactionalOperator, CouchbaseSimpleTransactionInterceptor
I know the intent is to remove some aspects of (1), but until
that's done it's proving tricky to have tests for both
concurrently - I've hit several issues on adding
CouchbaseSimpleTransactionInterceptor, with 'multiple
transaction manager beans in config' being common.
So, temporarily moving some beans from
AbstractCouchbaseConfiguration into the test Config class,
renaming it, and having two separately TransactionsConfig classes
for the two approaches.
Once we've aligned the approaches more, can move what beans
survive back into AbstractCouchbaseConfiguration.
Safety check in CouchbaseSimpleCallbackTransactionManager
that the blocking run is not accidentally
running a reactive @Transactional somehow.
Adding tests for reactive @Transactional, which now works
(including error handling and retries) as of the
CouchbaseSimpleTransactionInterceptor.
With TransactionsConfigCouchbaseSimpleTransactionManager change,
can now simplify @Transactional(transactionManager = ...) to
just @Transactional.
Tidying TODOs
I saw in a PR comment that getResources no longer uses TransactionOptions
Removing configureTransactions from config
As per PR discussion
Removing some code that has now been refactored into 3.3.1 SDK
Removing TODOs that are TODONE already
Removing transactionsOptions() bean from AbstractCouchbaseConfiguration.
As per comment, this feels unnecessary: any options you'd configure at
this config level, you'd surely provide at the global (Cluster) level
instead?
Reinstating some commented-out code
This looks pretty crucial - can't recall why I commented it in first place
Removing TransactionResult
This was from a now-abandoned idea of storing transactional
metadata in the entity class.
Fix recent removal of TransactionOptions bean
Switch some IllegalStateException for more accurate UnsupportedOperationException
Tidying tests to remove old GenericApplicationContext method
Tidying some TODOs
(todo gp == in code that I think we should remove)
(todo gpx == needs looking at)
Tidying TransactionsConfigCouchbaseSimpleTransactionManager
Provide SpringTransactionAttemptContext and ReactiveSpringTransactionAttemptContext wrappers
For use by TransactionsWrapper and reactive equivalent.
Pro: it's an abstraction layer. It lets us add Spring-specific functionality, or hide
functionality that for whatever reason doesn't work with Spring
(which is why it's composition rather than inheritance, beyond that being
a best practice anyway).
Con: any new API added will also have to be added to these wrappers. But
that's a small amount of work and API is added very infrequently.
Cleanup tests mostly. Temporary fix for CouchbaseSimpleCallbackTransactionManager.
Merge branch 'programmatix-datacouch_1145_transaction_support' of github.com:spring-projects/spring-data-couchbase into programmatix-datacouch_1145_transaction_support
Tidying up test cases.
Fixes to over-zealous manual merging.
- keep CouchbaseTransactionManager as Graham is still using it.
- fix tests that were expecting SimulateFailure to be nested.
Add ReactiveTransactionWrapper and enable tests that use it.
More tidying. Removed CouchbaseTransactionManager.
Only wrap exceptions in CouchbaseSimpleTransactionInterceptor if they are not RuntimeExceptions.
Removed CouchbaseTransactionalOperator and ReactiveCouchbaseTransactionalOperator.
Fixed non-transactions regression introduced in previous commit.
Removing the two transactions config classes
Now we've landed on a single agreed approach there's no
need for this separation any more, and any beans can be
moved back into AbstractCouchbaseConfiguration.
Rename CouchbaseSimpleTransactionInterceptor
The "Simple" moniker is no longer useful since we now
only have one of these.
Rename CouchbaseSimpleCallbackTransactionManager
The "Simple" moniker is no longer useful since we now
only have one of these.
Rename CouchbaseSimpleTransactionalOperator
The "Simple" moniker is no longer useful since we now
only have one of these.
Change CouchbaseTransactionalOperator construction to static
As per Slack discussion.
Remove comments related to another database
Removing a test comment that doesn't seem to apply anymore
Test passes for me at least
Simplify TransactionsWrapper and AttemptContextReactiveAccessor
newCoreTranactionAttemptContext is reimplementing some code
from core plus has some config bugs to resolve. I think it's
simpler to remove it and replace TransactionsWrapper
(the only code still using this method) with the simple code
seen now.
(Note this is similar to how I had it before
1701183b63
- not sure if that commit intentionally reverted things?)
This change also petmits a lot of tidyup & simplification throughout
the codebase. We can just create CouchbaseResourceHolders
directly now.
Tidying a test
No longer needs retryWhen now using the new approaches
Remove ReactiveCouchbaseClientFactory.
It was added to support getting the transaction from the
TransactionSynchronizationManager.forCurrent() and providing a template
with a session containing the transaction.
Fixing some code warnings
Mostly removing unused code
Mark internal classes @Stability.Internal
Removing some tests that have already been previously moved into another file.
Removing the transaction wrappers
Requires JVMCBC-1105 and 3.3.2
Add more tests for native SDK transactions
Move all tests related to native SDK transactions into their own package
It makes it easier to test just that functionality
while iterating.
Removing CouchbaseTransactionManagerTransactionalTemplateIntegrationTests
This test is now redundant. It was created to show
why the original CouchbaseTransactionManager couldn't work
(no retries). Now that has been replaced, this test
is just duplicating others.
Adding some minimal JavaDocs and comments.
Tidying up after moving ThreadLocalStorage into SDK
Deleting CouchbaseTemplateTransactionIntegrationTests
As this relies on Spring test @Transactional, which
we do not support as that Spring logic is not aware
of CallbackPreferringTransactionManager.
Removed some redundant bean names
Tidying up tests and comments
Removing some now-unused reflection code
Starting with mapping TransactionFailedException and
TransactionCommitAmbiguousException, into new errors
TransactionSystemUnambiguousException and
TransactionSystemAmbiguousException.
These will be raised from an @Transactional
transaction.
E.g. to do error handling the user would do:
```
try {
service.transactionalMethod();
}
catch (TransactionSystemAmbiguousException ex) {
// app-specific handling
}
catch (TransactionSystemUnambiguousException ex) {
// app-specific handling
}
class Service {
@Transactional
void transactionalMethod() {
// ...
}
}
```
Mapping TransactionOperationFailedException, which is
an opaque signal raised from transaction operations,
to new exception UncategorizedTransactionDataAccessException.
This depends on some new functionality added into
Java SDK 3.3.2, WrappedTransactionOperationFailedException.
Minor tidyuo
Improving tests for correct operation-level errors
commit b02959dded4cf19b2f8b6f78b016b5bb6afb9ac8
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Mon Mar 14 09:37:51 2022 -0700
Support derived queries on repositories defined with an abstract entity class. (#1366)
Motivation:
Currently an abstract entity class specified in the repository definition can
be used for the predicate typeKey = typeAlias(of abstract entity class) in
queries. Since documents are stored with typeKey = typeAlias(of concrete class)
those queries will never match any documents. To allow this to work,
all of the abstract entity class an all concrete subclasses must use the
same typeAlias.
Once those documents are found, regardless of their concrete class,
they will all have the same typeKey = typeAlias, instead of having the
typeAlias specific to the concrete class. Additional information in
the stored document is needed to identify the concrete class (subtype
in the example test case), as well as a TypeMapper to interpret that
information.
Changes:
This allows a common TypeAlias to be used for the purpose of the predicate
typeKey = typeAlias, and the determination of the concrete type by
implementing an AbstractingMappingCouchbaseConverter that inspects the
'subtype' property.
Closes #1365.
Co-authored-by: Michael Reiche <michael.reiche@couchbase.com>
commit 5f03d1e0ce80e820ff8a3322b2ebfcf6f1bebb8f
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Tue Feb 22 10:09:25 2022 -0800
Fix update to cache documentation. (#1359)
Closes #1358.
commit d5ca390d84a281b31854aa201cc53b4093af075d
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Tue Feb 22 09:56:53 2022 -0800
Reinstate CouchbaseCache documentation. (#1357)
Closes #1356.
commit 9af8ece3708d805c03db8784bfd9cd3c77e76db9
Author: Mark Paluch <mpaluch@vmware.com>
Date: Tue Feb 22 08:44:34 2022 +0100
Use Java 17 to build snapshots for Artifactory.
Closes #1352
commit e83bd41a86528ceb99d2291a23ac07fe6250b75f
Author: Mark Paluch <mpaluch@vmware.com>
Date: Fri Feb 18 11:15:41 2022 +0100
After release cleanups.
See #1307
commit 13d266e6f7ff06901c9359ef7aac5143858d22f1
Author: Mark Paluch <mpaluch@vmware.com>
Date: Fri Feb 18 11:08:48 2022 +0100
Prepare 4.4 M3 (2021.2.0).
See #1307
commit 4a044197aa5d7c4bfc5d9b15f0dc7139bb4b4fc5
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Mon Feb 14 09:10:04 2022 -0800
Add mechanism for save to do one of insert, replace or upsert. (#1316)
Closes #1277.
commit 1fdbf6d5d091225d3297b3aab892013155000e06
Author: Christoph Strobl <cstrobl@vmware.com>
Date: Tue Jan 18 09:21:34 2022 +0100
After release cleanups.
See #1301
commit da7d241e3927412b5ce32b9a1d017a2412c1891e
Author: Christoph Strobl <cstrobl@vmware.com>
Date: Tue Jan 18 09:09:02 2022 +0100
Prepare 4.4 M2 (2021.2.0).
See #1301
commit ebe33cf138970ad47575452bd8b1fc181964fba7
Author: Christoph Strobl <cstrobl@vmware.com>
Date: Fri Jan 14 11:08:04 2022 +0100
After release cleanups.
See #1298
commit ecadff644c31a834d83c2061cb45edb43f2e8983
Author: Christoph Strobl <cstrobl@vmware.com>
Date: Fri Jan 14 10:57:51 2022 +0100
Prepare 4.4 M1 (2021.2.0).
See #1298
commit 5dbf182fd10c5405b7a9361f50188144b76239b8
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Thu Jan 6 15:00:49 2022 -0800
Incorporate changes from 5.0.x and bump Couchbase SDK. (#1287)
Closes #1286.
Co-authored-by: mikereiche <michael.reiche@couchbase.com>
commit ef22b3d7967f805d74ddb92e571899570cd80762
Author: Michael Reiche <48999328+mikereiche@users.noreply.github.com>
Date: Fri Nov 12 07:15:45 2021 -0800
Handle Collection<> parameters to repository query methods. (#1271)
Closes #1270.
Co-authored-by: mikereiche <michael.reiche@couchbase.com>
commit 00e05e4b41414afd815cf08127da13c2bc3481d1
Author: Jens Schauder <jschauder@vmware.com>
Date: Fri Nov 12 10:59:45 2021 +0100
After release cleanups.
See #1257
commit af66ada42cd19ce8d6ac989188686155d3a8181a
Author: Jens Schauder <jschauder@vmware.com>
Date: Fri Nov 12 10:49:16 2021 +0100
Prepare 4.3 GA (2021.1.0).
See #1257
commit 6ec138182da008c7be465fb5e31f0e210e3b00ca
Author: yyfMichaelYan <54760415+yyfMichaelYan@users.noreply.github.com>
Date: Mon Nov 8 13:40:24 2021 -0600
fix flaky test writesAndReadsCustomFieldsConvertedClass (#1264)
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright 2021-2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.couchbase.client.java.transactions;
|
||||
|
||||
import com.couchbase.client.core.annotation.Stability;
|
||||
import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
|
||||
import com.couchbase.client.java.codec.JsonSerializer;
|
||||
|
||||
/**
|
||||
* To access the ReactiveTransactionAttemptContext held by TransactionAttemptContext
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
@Stability.Internal
|
||||
public class AttemptContextReactiveAccessor {
|
||||
public static ReactiveTransactionAttemptContext createReactiveTransactionAttemptContext(
|
||||
CoreTransactionAttemptContext core, JsonSerializer jsonSerializer) {
|
||||
return new ReactiveTransactionAttemptContext(core, jsonSerializer);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors
|
||||
* Copyright 2012-2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -97,9 +97,11 @@ public class SimpleCouchbaseClientFactory implements CouchbaseClientFactory {
|
||||
@Override
|
||||
public Collection getCollection(final String collectionName) {
|
||||
final Scope scope = getScope();
|
||||
if (collectionName == null) {
|
||||
if (!scope.name().equals(CollectionIdentifier.DEFAULT_SCOPE)) {
|
||||
throw new IllegalStateException("A collectionName must be provided if a non-default scope is used!");
|
||||
if (collectionName == null || CollectionIdentifier.DEFAULT_COLLECTION.equals(collectionName)) {
|
||||
if(scope != null ) {
|
||||
if (scope.name() != null && !CollectionIdentifier.DEFAULT_SCOPE.equals(scope.name())) {
|
||||
throw new IllegalStateException("A collectionName must be provided if a non-default scope is used");
|
||||
}
|
||||
}
|
||||
return getBucket().defaultCollection();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Role;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
@@ -40,9 +41,16 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
|
||||
import org.springframework.data.couchbase.core.mapping.Document;
|
||||
import org.springframework.data.couchbase.repository.config.ReactiveRepositoryOperationsMapping;
|
||||
import org.springframework.data.couchbase.repository.config.RepositoryOperationsMapping;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseCallbackTransactionManager;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseTransactionInterceptor;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseTransactionalOperator;
|
||||
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
|
||||
import org.springframework.data.mapping.model.FieldNamingStrategy;
|
||||
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
|
||||
import org.springframework.transaction.TransactionManager;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -123,7 +131,7 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
* @param couchbaseCluster the cluster reference from the SDK.
|
||||
* @return the initialized factory.
|
||||
*/
|
||||
@Bean
|
||||
@Bean(name = BeanNames.COUCHBASE_CLIENT_FACTORY)
|
||||
public CouchbaseClientFactory couchbaseClientFactory(final Cluster couchbaseCluster) {
|
||||
return new SimpleCouchbaseClientFactory(couchbaseCluster, getBucketName(), getScopeName());
|
||||
}
|
||||
@@ -280,9 +288,8 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
|
||||
/**
|
||||
* Creates a {@link CouchbaseMappingContext} equipped with entity classes scanned from the mapping base package.
|
||||
*
|
||||
*/
|
||||
@Bean
|
||||
@Bean(BeanNames.COUCHBASE_MAPPING_CONTEXT)
|
||||
public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customConversions) throws Exception {
|
||||
CouchbaseMappingContext mappingContext = new CouchbaseMappingContext();
|
||||
mappingContext.setInitialEntitySet(getInitialEntitySet());
|
||||
@@ -310,6 +317,44 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager
|
||||
* CallbackPreferrringTransactionmanagers do not play well with test-cases that rely
|
||||
* on @TestTransaction/@BeforeTransaction/@AfterTransaction
|
||||
*
|
||||
* @param clientFactory
|
||||
* @return
|
||||
*/
|
||||
@Bean(BeanNames.COUCHBASE_TRANSACTION_MANAGER)
|
||||
CouchbaseCallbackTransactionManager couchbaseTransactionManager(CouchbaseClientFactory clientFactory) {
|
||||
return new CouchbaseCallbackTransactionManager(clientFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* The default TransactionalOperator.
|
||||
*
|
||||
* @param couchbaseCallbackTransactionManager
|
||||
* @return
|
||||
*/
|
||||
@Bean(BeanNames.COUCHBASE_TRANSACTIONAL_OPERATOR)
|
||||
public CouchbaseTransactionalOperator couchbaseTransactionalOperator(
|
||||
CouchbaseCallbackTransactionManager couchbaseCallbackTransactionManager) {
|
||||
return CouchbaseTransactionalOperator.create(couchbaseCallbackTransactionManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
public TransactionInterceptor transactionInterceptor(TransactionManager couchbaseTransactionManager) {
|
||||
TransactionAttributeSource transactionAttributeSource = new AnnotationTransactionAttributeSource();
|
||||
TransactionInterceptor interceptor = new CouchbaseTransactionInterceptor(couchbaseTransactionManager,
|
||||
transactionAttributeSource);
|
||||
interceptor.setTransactionAttributeSource(transactionAttributeSource);
|
||||
if (couchbaseTransactionManager != null) {
|
||||
interceptor.setTransactionManager(couchbaseTransactionManager);
|
||||
}
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure whether to automatically create indices for domain types by deriving the from the entity or not.
|
||||
*/
|
||||
@@ -375,5 +420,4 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
public QueryScanConsistency getDefaultConsistency() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors
|
||||
* Copyright 2012-2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -59,4 +59,10 @@ public class BeanNames {
|
||||
* The name for the bean that will handle reactive audit trail marking of entities.
|
||||
*/
|
||||
public static final String REACTIVE_COUCHBASE_AUDITING_HANDLER = "reactiveCouchbaseAuditingHandler";
|
||||
|
||||
public static final String COUCHBASE_CLIENT_FACTORY = "couchbaseClientFactory";
|
||||
|
||||
public static final String COUCHBASE_TRANSACTION_MANAGER = "couchbaseTransactionManager";
|
||||
|
||||
public static final String COUCHBASE_TRANSACTIONAL_OPERATOR = "couchbaseTransactionalOperator";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.couchbase.client.core.annotation.Stability;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver;
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
|
||||
import org.springframework.data.couchbase.core.support.TemplateUtils;
|
||||
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.couchbase.client.core.error.CouchbaseException;
|
||||
|
||||
|
||||
/**
|
||||
* Base shared by Reactive and non-Reactive TemplateSupport
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
@Stability.Internal
|
||||
public abstract class AbstractTemplateSupport {
|
||||
|
||||
final ReactiveCouchbaseTemplate template;
|
||||
final CouchbaseConverter converter;
|
||||
final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
|
||||
final TranslationService translationService;
|
||||
ApplicationContext applicationContext;
|
||||
static final Logger LOG = LoggerFactory.getLogger(AbstractTemplateSupport.class);
|
||||
|
||||
public AbstractTemplateSupport(ReactiveCouchbaseTemplate template, CouchbaseConverter converter,
|
||||
TranslationService translationService) {
|
||||
this.template = template;
|
||||
this.converter = converter;
|
||||
this.mappingContext = converter.getMappingContext();
|
||||
this.translationService = translationService;
|
||||
}
|
||||
|
||||
abstract ReactiveCouchbaseTemplate getReactiveTemplate();
|
||||
|
||||
public <T> T decodeEntityBase(String id, String source, Long cas, Class<T> entityClass, String scope,
|
||||
String collection, Object txResultHolder, CouchbaseResourceHolder holder) {
|
||||
|
||||
// this is the entity class defined for the repository. It may not be the class of the document that was read
|
||||
// we will reset it after reading the document
|
||||
//
|
||||
// This will fail for the case where:
|
||||
// 1) The version is defined in the concrete class, but not in the abstract class; and
|
||||
// 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in
|
||||
// the source is null.
|
||||
// We could expose from the MappingCouchbaseConverter determining the persistent entity from the source,
|
||||
// but that is a lot of work to do every time just for this very rare and avoidable case.
|
||||
// TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
|
||||
|
||||
CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass);
|
||||
|
||||
if (persistentEntity == null) { // method could return a Long, Boolean, String etc.
|
||||
// QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left
|
||||
// to unwrap. This results in List<String[]> being unwrapped past String[] to String, so this may also be a
|
||||
// Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told.
|
||||
// if this is a Collection or array, only the first element will be returned.
|
||||
final CouchbaseDocument converted = new CouchbaseDocument(id);
|
||||
Set<Map.Entry<String, Object>> set = ((CouchbaseDocument) translationService.decode(source, converted))
|
||||
.getContent().entrySet();
|
||||
return (T) set.iterator().next().getValue();
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project "
|
||||
+ TemplateUtils.SELECT_ID);
|
||||
}
|
||||
|
||||
final CouchbaseDocument converted = new CouchbaseDocument(id);
|
||||
|
||||
// if possible, set the version property in the source so that if the constructor has a long version argument,
|
||||
// it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure
|
||||
// can be avoid by defining the argument as Long instead of long.
|
||||
// persistentEntity is still the (possibly abstract) class specified in the repository definition
|
||||
// it's possible that the abstract class does not have a version property, and this won't be able to set the version
|
||||
if (persistentEntity.getVersionProperty() != null) {
|
||||
if (cas == null) {
|
||||
throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS
|
||||
+ " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_CAS);
|
||||
}
|
||||
if (cas != 0) {
|
||||
converted.put(persistentEntity.getVersionProperty().getName(), cas);
|
||||
}
|
||||
}
|
||||
|
||||
// if the constructor has an argument that is long version, then construction will fail if the 'version'
|
||||
// is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this.
|
||||
// (Version doesn't come from 'source', it comes from the cas argument to decodeEntity)
|
||||
T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted));
|
||||
final ConvertingPropertyAccessor<T> accessor = getPropertyAccessor(readEntity);
|
||||
|
||||
persistentEntity = couldBePersistentEntity(readEntity.getClass());
|
||||
|
||||
if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) {
|
||||
accessor.setProperty(persistentEntity.getVersionProperty(), cas);
|
||||
}
|
||||
N1qlJoinResolver.handleProperties(persistentEntity, accessor, getReactiveTemplate(), id, scope, collection);
|
||||
|
||||
if (holder != null) {
|
||||
holder.transactionResultHolder(txResultHolder, (T) accessor.getBean());
|
||||
}
|
||||
|
||||
return accessor.getBean();
|
||||
}
|
||||
|
||||
CouchbasePersistentEntity couldBePersistentEntity(Class<?> entityClass) {
|
||||
if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return mappingContext.getPersistentEntity(entityClass);
|
||||
} catch (InaccessibleObjectException t) {
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public <T> T applyResultBase(T entity, CouchbaseDocument converted, Object id, long cas,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder) {
|
||||
ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
|
||||
final CouchbasePersistentEntity<?> persistentEntity = converter.getMappingContext()
|
||||
.getRequiredPersistentEntity(entity.getClass());
|
||||
|
||||
final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty();
|
||||
if (idProperty != null) {
|
||||
accessor.setProperty(idProperty, id);
|
||||
}
|
||||
|
||||
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
|
||||
if (versionProperty != null) {
|
||||
accessor.setProperty(versionProperty, cas);
|
||||
}
|
||||
|
||||
if (holder != null) {
|
||||
holder.transactionResultHolder(txResultHolder, (T) accessor.getBean());
|
||||
}
|
||||
maybeEmitEvent(new AfterSaveEvent(accessor.getBean(), converted));
|
||||
return (T) accessor.getBean();
|
||||
|
||||
}
|
||||
|
||||
public Long getCas(final Object entity) {
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
|
||||
long cas = 0;
|
||||
if (versionProperty != null) {
|
||||
Object casObject = accessor.getProperty(versionProperty);
|
||||
if (casObject instanceof Number) {
|
||||
cas = ((Number) casObject).longValue();
|
||||
}
|
||||
}
|
||||
return cas;
|
||||
}
|
||||
|
||||
public Object getId(final Object entity) {
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty();
|
||||
Object id = null;
|
||||
if (idProperty != null) {
|
||||
id = accessor.getProperty(idProperty);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getJavaNameForEntity(final Class<?> clazz) {
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
|
||||
MappingCouchbaseEntityInformation<?, Object> info = new MappingCouchbaseEntityInformation<>(persistentEntity);
|
||||
return info.getJavaType().getName();
|
||||
}
|
||||
|
||||
<T> ConvertingPropertyAccessor<T> getPropertyAccessor(final T source) {
|
||||
CouchbasePersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(source.getClass());
|
||||
PersistentPropertyAccessor<T> accessor = entity.getPropertyAccessor(source);
|
||||
return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService());
|
||||
}
|
||||
|
||||
public void maybeEmitEvent(CouchbaseMappingEvent<?> event) {
|
||||
if (canPublishEvent()) {
|
||||
try {
|
||||
this.applicationContext.publishEvent(event);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("{} thrown during {}", e, event);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean canPublishEvent() {
|
||||
return this.applicationContext != null;
|
||||
}
|
||||
|
||||
public TranslationService getTranslationService() {
|
||||
return translationService;
|
||||
}
|
||||
}
|
||||
@@ -25,12 +25,32 @@ import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.dao.DataRetrievalFailureException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.dao.InvalidDataAccessResourceUsageException;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;;
|
||||
import org.springframework.dao.OptimisticLockingFailureException;
|
||||
import org.springframework.dao.QueryTimeoutException;
|
||||
import org.springframework.dao.TransientDataAccessResourceException;
|
||||
import org.springframework.dao.support.PersistenceExceptionTranslator;
|
||||
import org.springframework.data.couchbase.transaction.error.UncategorizedTransactionDataAccessException;
|
||||
|
||||
import com.couchbase.client.core.error.*;
|
||||
import com.couchbase.client.core.error.BucketNotFoundException;
|
||||
import com.couchbase.client.core.error.CasMismatchException;
|
||||
import com.couchbase.client.core.error.CollectionNotFoundException;
|
||||
import com.couchbase.client.core.error.ConfigException;
|
||||
import com.couchbase.client.core.error.DecodingFailureException;
|
||||
import com.couchbase.client.core.error.DesignDocumentNotFoundException;
|
||||
import com.couchbase.client.core.error.DocumentExistsException;
|
||||
import com.couchbase.client.core.error.DocumentLockedException;
|
||||
import com.couchbase.client.core.error.DocumentNotFoundException;
|
||||
import com.couchbase.client.core.error.DurabilityAmbiguousException;
|
||||
import com.couchbase.client.core.error.DurabilityImpossibleException;
|
||||
import com.couchbase.client.core.error.DurabilityLevelNotAvailableException;
|
||||
import com.couchbase.client.core.error.EncodingFailureException;
|
||||
import com.couchbase.client.core.error.ReplicaNotConfiguredException;
|
||||
import com.couchbase.client.core.error.RequestCanceledException;
|
||||
import com.couchbase.client.core.error.ScopeNotFoundException;
|
||||
import com.couchbase.client.core.error.ServiceNotAvailableException;
|
||||
import com.couchbase.client.core.error.TemporaryFailureException;
|
||||
import com.couchbase.client.core.error.ValueTooLargeException;
|
||||
import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
|
||||
|
||||
/**
|
||||
* Simple {@link PersistenceExceptionTranslator} for Couchbase.
|
||||
@@ -41,6 +61,8 @@ import com.couchbase.client.core.error.*;
|
||||
*
|
||||
* @author Michael Nitschinger
|
||||
* @author Simon Baslé
|
||||
* @author Michael Reiche
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class CouchbaseExceptionTranslator implements PersistenceExceptionTranslator {
|
||||
|
||||
@@ -71,7 +93,7 @@ public class CouchbaseExceptionTranslator implements PersistenceExceptionTransla
|
||||
return new OptimisticLockingFailureException(ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
if ( ex instanceof ReplicaNotConfiguredException || ex instanceof DurabilityLevelNotAvailableException
|
||||
if (ex instanceof ReplicaNotConfiguredException || ex instanceof DurabilityLevelNotAvailableException
|
||||
|| ex instanceof DurabilityImpossibleException || ex instanceof DurabilityAmbiguousException) {
|
||||
return new DataIntegrityViolationException(ex.getMessage(), ex);
|
||||
}
|
||||
@@ -98,6 +120,13 @@ public class CouchbaseExceptionTranslator implements PersistenceExceptionTransla
|
||||
return new DataRetrievalFailureException(ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
if (ex instanceof TransactionOperationFailedException) {
|
||||
// Replace the TransactionOperationFailedException, since we want the Spring operation to fail with a
|
||||
// Spring error. Internal state has already been set in the AttemptContext so the retry, rollback etc.
|
||||
// will get respected regardless of what gets propagated (or not) from the lambda.
|
||||
return new UncategorizedTransactionDataAccessException((TransactionOperationFailedException) ex);
|
||||
}
|
||||
|
||||
// Unable to translate exception, therefore just throw the original!
|
||||
throw ex;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors
|
||||
* Copyright 2012-2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,11 +18,14 @@ package org.springframework.data.couchbase.core;
|
||||
|
||||
import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.query.Query;
|
||||
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
|
||||
/**
|
||||
* Defines common operations on the Couchbase data source, most commonly implemented by {@link CouchbaseTemplate}.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public interface CouchbaseOperations extends FluentCouchbaseOperations {
|
||||
|
||||
@@ -50,4 +53,9 @@ public interface CouchbaseOperations extends FluentCouchbaseOperations {
|
||||
* Returns the default consistency to use for queries
|
||||
*/
|
||||
QueryScanConsistency getConsistency();
|
||||
|
||||
<T> T save(T entity, String... scopeAndCollection);
|
||||
|
||||
<T> Long count(Query query, Class<T> domainType);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors
|
||||
* Copyright 2012-2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import org.springframework.data.couchbase.core.index.CouchbasePersistentEntityIn
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.couchbase.core.query.Query;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -49,8 +50,8 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex
|
||||
private final CouchbaseTemplateSupport templateSupport;
|
||||
private final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
|
||||
private final ReactiveCouchbaseTemplate reactiveCouchbaseTemplate;
|
||||
private final QueryScanConsistency scanConsistency;
|
||||
private @Nullable CouchbasePersistentEntityIndexCreator indexCreator;
|
||||
private QueryScanConsistency scanConsistency;
|
||||
|
||||
public CouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) {
|
||||
this(clientFactory, converter, new JacksonTranslationService());
|
||||
@@ -69,6 +70,7 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex
|
||||
this.reactiveCouchbaseTemplate = new ReactiveCouchbaseTemplate(clientFactory, converter, translationService,
|
||||
scanConsistency);
|
||||
this.scanConsistency = scanConsistency;
|
||||
|
||||
this.mappingContext = this.converter.getMappingContext();
|
||||
if (mappingContext instanceof CouchbaseMappingContext) {
|
||||
CouchbaseMappingContext cmc = (CouchbaseMappingContext) mappingContext;
|
||||
@@ -78,6 +80,16 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T save(T entity, String... scopeAndCollection) {
|
||||
return reactive().save(entity, scopeAndCollection).block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Long count(Query query, Class<T> domainType) {
|
||||
return findByQuery(domainType).matching(query).count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ExecutableUpsertById<T> upsertById(final Class<T> domainType) {
|
||||
return new ExecutableUpsertByIdOperationSupport(this).upsertById(domainType);
|
||||
@@ -209,5 +221,4 @@ public class CouchbaseTemplate implements CouchbaseOperations, ApplicationContex
|
||||
public TemplateSupport support() {
|
||||
return templateSupport;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,37 +16,19 @@
|
||||
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver;
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.couchbase.core.mapping.event.AfterConvertCallback;
|
||||
import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.BeforeConvertCallback;
|
||||
import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
|
||||
import org.springframework.data.couchbase.core.support.TemplateUtils;
|
||||
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder;
|
||||
import org.springframework.data.mapping.callback.EntityCallbacks;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.couchbase.client.core.error.CouchbaseException;
|
||||
|
||||
/**
|
||||
* Internal encode/decode support for CouchbaseTemplate.
|
||||
@@ -57,23 +39,15 @@ import com.couchbase.client.core.error.CouchbaseException;
|
||||
* @author Carlos Espinaco
|
||||
* @since 3.0
|
||||
*/
|
||||
class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSupport {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CouchbaseTemplateSupport.class);
|
||||
class CouchbaseTemplateSupport extends AbstractTemplateSupport implements ApplicationContextAware, TemplateSupport {
|
||||
|
||||
private final CouchbaseTemplate template;
|
||||
private final CouchbaseConverter converter;
|
||||
private final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
|
||||
private final TranslationService translationService;
|
||||
private EntityCallbacks entityCallbacks;
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
public CouchbaseTemplateSupport(final CouchbaseTemplate template, final CouchbaseConverter converter,
|
||||
final TranslationService translationService) {
|
||||
super(template.reactive(), converter, translationService);
|
||||
this.template = template;
|
||||
this.converter = converter;
|
||||
this.mappingContext = converter.getMappingContext();
|
||||
this.translationService = translationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,141 +62,22 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope, String collection) {
|
||||
|
||||
// this is the entity class defined for the repository. It may not be the class of the document that was read
|
||||
// we will reset it after reading the document
|
||||
//
|
||||
// This will fail for the case where:
|
||||
// 1) The version is defined in the concrete class, but not in the abstract class; and
|
||||
// 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in
|
||||
// the source is null.
|
||||
// We could expose from the MappingCouchbaseConverter determining the persistent entity from the source,
|
||||
// but that is a lot of work to do every time just for this very rare and avoidable case.
|
||||
// TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
|
||||
|
||||
CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass);
|
||||
|
||||
if (persistentEntity == null) { // method could return a Long, Boolean, String etc.
|
||||
// QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left
|
||||
// to unwrap. This results in List<String[]> being unwrapped past String[] to String, so this may also be a
|
||||
// Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told.
|
||||
// if this is a Collection or array, only the first element will be returned.
|
||||
final CouchbaseDocument converted = new CouchbaseDocument(id);
|
||||
Set<Map.Entry<String, Object>> set = ((CouchbaseDocument) translationService.decode(source, converted))
|
||||
.getContent().entrySet();
|
||||
return (T) set.iterator().next().getValue();
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project "
|
||||
+ TemplateUtils.SELECT_ID);
|
||||
}
|
||||
|
||||
final CouchbaseDocument converted = new CouchbaseDocument(id);
|
||||
|
||||
// if possible, set the version property in the source so that if the constructor has a long version argument,
|
||||
// it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure
|
||||
// can be avoid by defining the argument as Long instead of long.
|
||||
// persistentEntity is still the (possibly abstract) class specified in the repository definition
|
||||
// it's possible that the abstract class does not have a version property, and this won't be able to set the version
|
||||
if (persistentEntity.getVersionProperty() != null) {
|
||||
if (cas == null) {
|
||||
throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS
|
||||
+ " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_CAS);
|
||||
}
|
||||
if (cas != 0) {
|
||||
converted.put(persistentEntity.getVersionProperty().getName(), cas);
|
||||
}
|
||||
}
|
||||
|
||||
// if the constructor has an argument that is long version, then construction will fail if the 'version'
|
||||
// is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this.
|
||||
// (Version doesn't come from 'source', it comes from the cas argument to decodeEntity)
|
||||
T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted));
|
||||
final ConvertingPropertyAccessor<T> accessor = getPropertyAccessor(readEntity);
|
||||
|
||||
persistentEntity = couldBePersistentEntity(readEntity.getClass());
|
||||
|
||||
if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) {
|
||||
accessor.setProperty(persistentEntity.getVersionProperty(), cas);
|
||||
}
|
||||
N1qlJoinResolver.handleProperties(persistentEntity, accessor, template.reactive(), id, scope, collection);
|
||||
return accessor.getBean();
|
||||
public <T> T decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope, String collection,
|
||||
Object txHolder, CouchbaseResourceHolder holder) {
|
||||
return decodeEntityBase(id, source, cas, entityClass, scope, collection, txHolder, holder);
|
||||
}
|
||||
|
||||
CouchbasePersistentEntity couldBePersistentEntity(Class<?> entityClass) {
|
||||
if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return mappingContext.getPersistentEntity(entityClass);
|
||||
} catch (InaccessibleObjectException t) {
|
||||
@Override
|
||||
public <T> T applyResult(T entity, CouchbaseDocument converted, Object id, long cas,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder) {
|
||||
return applyResultBase(entity, converted, id, cas, txResultHolder, holder);
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public <T> Integer getTxResultHolder(T source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) {
|
||||
Object returnValue;
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
|
||||
|
||||
if (versionProperty != null) {
|
||||
accessor.setProperty(versionProperty, cas);
|
||||
returnValue = accessor.getBean();
|
||||
} else {
|
||||
returnValue = entity;
|
||||
}
|
||||
maybeEmitEvent(new AfterSaveEvent(returnValue, converted));
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object applyUpdatedId(final Object entity, Object id) {
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty();
|
||||
|
||||
if (idProperty != null) {
|
||||
accessor.setProperty(idProperty, id);
|
||||
return accessor.getBean();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCas(final Object entity) {
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
|
||||
|
||||
long cas = 0;
|
||||
if (versionProperty != null) {
|
||||
Object casObject = accessor.getProperty(versionProperty);
|
||||
if (casObject instanceof Number) {
|
||||
cas = ((Number) casObject).longValue();
|
||||
}
|
||||
}
|
||||
return cas;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJavaNameForEntity(final Class<?> clazz) {
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
|
||||
MappingCouchbaseEntityInformation<?, Object> info = new MappingCouchbaseEntityInformation<>(persistentEntity);
|
||||
return info.getJavaType().getName();
|
||||
}
|
||||
|
||||
private <T> ConvertingPropertyAccessor<T> getPropertyAccessor(final T source) {
|
||||
CouchbasePersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(source.getClass());
|
||||
PersistentPropertyAccessor<T> accessor = entity.getPropertyAccessor(source);
|
||||
return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
@@ -246,24 +101,6 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo
|
||||
this.entityCallbacks = entityCallbacks;
|
||||
}
|
||||
|
||||
public void maybeEmitEvent(CouchbaseMappingEvent<?> event) {
|
||||
if (canPublishEvent()) {
|
||||
try {
|
||||
this.applicationContext.publishEvent(event);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("{} thrown during {}", e, event);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
LOG.info("maybeEmitEvent called, but CouchbaseTemplate not initialized with applicationContext");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean canPublishEvent() {
|
||||
return this.applicationContext != null;
|
||||
}
|
||||
|
||||
protected <T> T maybeCallBeforeConvert(T object, String collection) {
|
||||
if (entityCallbacks != null) {
|
||||
return entityCallbacks.callback(BeforeConvertCallback.class, object, collection);
|
||||
@@ -282,4 +119,8 @@ class CouchbaseTemplateSupport implements ApplicationContextAware, TemplateSuppo
|
||||
return object;
|
||||
}
|
||||
|
||||
@Override
|
||||
ReactiveCouchbaseTemplate getReactiveTemplate() {
|
||||
return template.reactive();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.couchbase.client.java.kv.ReplicateTo;
|
||||
* Remove Operations on KV service.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Michael Reiche
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface ExecutableRemoveByIdOperation {
|
||||
@@ -61,6 +62,14 @@ public interface ExecutableRemoveByIdOperation {
|
||||
@Override
|
||||
RemoveResult one(String id);
|
||||
|
||||
/**
|
||||
* Remove one document based on the entity. Transactions need the entity for the cas.
|
||||
*
|
||||
* @param entity the document ID.
|
||||
* @return result of the remove
|
||||
*/
|
||||
RemoveResult oneEntity(Object entity);
|
||||
|
||||
/**
|
||||
* Remove the documents in the collection.
|
||||
*
|
||||
@@ -70,6 +79,14 @@ public interface ExecutableRemoveByIdOperation {
|
||||
@Override
|
||||
List<RemoveResult> all(Collection<String> ids);
|
||||
|
||||
/**
|
||||
* Remove documents based on the entities. Transactions need the entity for the cas.
|
||||
*
|
||||
* @param entities to remove.
|
||||
* @return result of the remove
|
||||
*/
|
||||
List<RemoveResult> allEntities(Collection<Object> entities);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,11 @@ import com.couchbase.client.java.kv.PersistTo;
|
||||
import com.couchbase.client.java.kv.RemoveOptions;
|
||||
import com.couchbase.client.java.kv.ReplicateTo;
|
||||
|
||||
/**
|
||||
* {@link ExecutableRemoveByIdOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByIdOperation {
|
||||
|
||||
private final CouchbaseTemplate template;
|
||||
@@ -45,8 +50,8 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI
|
||||
public ExecutableRemoveById removeById(Class<?> domainType) {
|
||||
|
||||
return new ExecutableRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType),
|
||||
OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE,
|
||||
DurabilityLevel.NONE, null);
|
||||
OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE,
|
||||
null);
|
||||
}
|
||||
|
||||
static class ExecutableRemoveByIdSupport implements ExecutableRemoveById {
|
||||
@@ -83,15 +88,25 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI
|
||||
return reactiveRemoveByIdSupport.one(id).block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoveResult oneEntity(final Object entity) {
|
||||
return reactiveRemoveByIdSupport.oneEntity(entity).block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RemoveResult> all(final Collection<String> ids) {
|
||||
return reactiveRemoveByIdSupport.all(ids).collectList().block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RemoveResult> allEntities(final Collection<Object> entities) {
|
||||
return reactiveRemoveByIdSupport.allEntities(entities).collectList().block();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoveByIdWithOptions inCollection(final String collection) {
|
||||
return new ExecutableRemoveByIdSupport(template, domainType, scope, collection != null ? collection : this.collection, options, persistTo, replicateTo,
|
||||
durabilityLevel, cas);
|
||||
return new ExecutableRemoveByIdSupport(template, domainType, scope,
|
||||
collection != null ? collection : this.collection, options, persistTo, replicateTo, durabilityLevel, cas);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -118,8 +133,8 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI
|
||||
|
||||
@Override
|
||||
public RemoveByIdInCollection inScope(final String scope) {
|
||||
return new ExecutableRemoveByIdSupport(template, domainType, scope != null ? scope : this.scope, collection, options, persistTo, replicateTo,
|
||||
durabilityLevel, cas);
|
||||
return new ExecutableRemoveByIdSupport(template, domainType, scope != null ? scope : this.scope, collection,
|
||||
options, persistTo, replicateTo, durabilityLevel, cas);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -127,6 +142,7 @@ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByI
|
||||
return new ExecutableRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo,
|
||||
durabilityLevel, cas);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,13 +17,15 @@ package org.springframework.data.couchbase.core;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
|
||||
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder;
|
||||
|
||||
/**
|
||||
* Wrapper of {@link TemplateSupport} methods to adapt them to {@link ReactiveTemplateSupport}.
|
||||
*
|
||||
* @author Carlos Espinaco
|
||||
* @author Michael Reiche
|
||||
* @since 4.2
|
||||
*/
|
||||
public class NonReactiveSupportWrapper implements ReactiveTemplateSupport {
|
||||
@@ -40,33 +42,35 @@ public class NonReactiveSupportWrapper implements ReactiveTemplateSupport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope,
|
||||
String collection) {
|
||||
return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection));
|
||||
public <T> Mono<T> decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope, String collection,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder) {
|
||||
return Mono.fromSupplier(() -> support.decodeEntity(id, source, cas, entityClass, scope, collection, txResultHolder, holder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> applyUpdatedCas(Object entity, CouchbaseDocument converted, long cas) {
|
||||
return Mono.fromSupplier(() -> support.applyUpdatedCas(entity, converted, cas));
|
||||
public <T> Mono<T> applyResult(T entity, CouchbaseDocument converted, Object id, Long cas,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder) {
|
||||
return Mono.fromSupplier(() -> support.applyResult(entity, converted, id, cas, txResultHolder, holder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> applyUpdatedId(Object entity, Object id) {
|
||||
return Mono.fromSupplier(() -> support.applyUpdatedId(entity, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getCas(Object entity) {
|
||||
return support.getCas(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getId(Object entity) {
|
||||
return support.getId(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJavaNameForEntity(Class<?> clazz) {
|
||||
return support.getJavaNameForEntity(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeEmitEvent(CouchbaseMappingEvent<?> event) {
|
||||
support.maybeEmitEvent(event);
|
||||
public TranslationService getTranslationService() {
|
||||
return support.getTranslationService();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2021 the original author or authors
|
||||
* Copyright 2012-2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,8 +15,11 @@
|
||||
*/
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.query.Query;
|
||||
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
|
||||
@@ -49,6 +52,10 @@ public interface ReactiveCouchbaseOperations extends ReactiveFluentCouchbaseOper
|
||||
*/
|
||||
CouchbaseClientFactory getCouchbaseClientFactory();
|
||||
|
||||
<T> Mono<T> save(T entity, String... scopeAndCollection);
|
||||
|
||||
<T> Mono<Long> count(Query query, Class<T> personClass);
|
||||
|
||||
/**
|
||||
* @return the default consistency to use for queries
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
@@ -25,7 +27,12 @@ import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.couchbase.core.query.Query;
|
||||
import org.springframework.data.couchbase.core.support.PseudoArgs;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import com.couchbase.client.java.Collection;
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
@@ -45,19 +52,19 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A
|
||||
private final PersistenceExceptionTranslator exceptionTranslator;
|
||||
private final ReactiveCouchbaseTemplateSupport templateSupport;
|
||||
private ThreadLocal<PseudoArgs<?>> threadLocalArgs = new ThreadLocal<>();
|
||||
private QueryScanConsistency scanConsistency;
|
||||
private final QueryScanConsistency scanConsistency;
|
||||
|
||||
public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter) {
|
||||
this(clientFactory, converter, new JacksonTranslationService());
|
||||
this(clientFactory, converter, new JacksonTranslationService(), null);
|
||||
}
|
||||
|
||||
public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter,
|
||||
final TranslationService translationService) {
|
||||
final TranslationService translationService) {
|
||||
this(clientFactory, converter, translationService, null);
|
||||
}
|
||||
|
||||
public ReactiveCouchbaseTemplate(final CouchbaseClientFactory clientFactory, final CouchbaseConverter converter,
|
||||
final TranslationService translationService, QueryScanConsistency scanConsistency) {
|
||||
final TranslationService translationService, final QueryScanConsistency scanConsistency) {
|
||||
this.clientFactory = clientFactory;
|
||||
this.converter = converter;
|
||||
this.exceptionTranslator = clientFactory.getExceptionTranslator();
|
||||
@@ -65,6 +72,47 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A
|
||||
this.scanConsistency = scanConsistency;
|
||||
}
|
||||
|
||||
public <T> Mono<T> save(T entity) {
|
||||
return save(entity, null, null);
|
||||
}
|
||||
|
||||
public <T> Mono<T> save(T entity, String... scopeAndCollection) {
|
||||
Assert.notNull(entity, "Entity must not be null!");
|
||||
String scope = scopeAndCollection.length > 0 ? scopeAndCollection[0] : null;
|
||||
String collection = scopeAndCollection.length > 1 ? scopeAndCollection[1] : null;
|
||||
Mono<T> result;
|
||||
final CouchbasePersistentEntity<?> mapperEntity = getConverter().getMappingContext()
|
||||
.getPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty();
|
||||
final boolean versionPresent = versionProperty != null;
|
||||
final Long version = versionProperty == null || versionProperty.getField() == null ? null
|
||||
: (Long) ReflectionUtils.getField(versionProperty.getField(), entity);
|
||||
final boolean existingDocument = version != null && version > 0;
|
||||
|
||||
Class clazz = entity.getClass();
|
||||
|
||||
if (!versionPresent) { // the entity doesn't have a version property
|
||||
// No version field - no cas
|
||||
// If in a transaction, insert is the only thing that will work
|
||||
if (TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent()) {
|
||||
result = (Mono<T>) insertById(clazz).inScope(scope).inCollection(collection).one(entity);
|
||||
} else { // if not in a tx, then upsert will work
|
||||
result = (Mono<T>) upsertById(clazz).inScope(scope).inCollection(collection).one(entity);
|
||||
}
|
||||
} else if (existingDocument) { // there is a version property, and it is non-zero
|
||||
// Updating existing document with cas
|
||||
result = (Mono<T>) replaceById(clazz).inScope(scope).inCollection(collection).one(entity);
|
||||
} else { // there is a version property, but it's zero or not set.
|
||||
// Creating new document
|
||||
result = (Mono<T>) insertById(clazz).inScope(scope).inCollection(collection).one(entity);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public <T> Mono<Long> count(Query query, Class<T> domainType) {
|
||||
return findByQuery(domainType).matching(query).all().count();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ReactiveFindById<T> findById(Class<T> domainType) {
|
||||
return new ReactiveFindByIdOperationSupport(this).findById(domainType);
|
||||
@@ -165,8 +213,9 @@ public class ReactiveCouchbaseTemplate implements ReactiveCouchbaseOperations, A
|
||||
*
|
||||
* @param ex the exception to translate
|
||||
*/
|
||||
protected RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) {
|
||||
RuntimeException resolved = exceptionTranslator.translateExceptionIfPossible(ex);
|
||||
RuntimeException potentiallyConvertRuntimeException(final RuntimeException ex) {
|
||||
RuntimeException resolved = exceptionTranslator != null ? exceptionTranslator.translateExceptionIfPossible(ex)
|
||||
: null;
|
||||
return resolved == null ? ex : resolved;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,63 +16,40 @@
|
||||
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import com.couchbase.client.core.error.CouchbaseException;
|
||||
import org.springframework.data.couchbase.core.support.TemplateUtils;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.data.couchbase.core.convert.CouchbaseConverter;
|
||||
import org.springframework.data.couchbase.core.convert.join.N1qlJoinResolver;
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.couchbase.core.mapping.event.AfterSaveEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.BeforeConvertEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.BeforeSaveEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
|
||||
import org.springframework.data.couchbase.core.mapping.event.ReactiveAfterConvertCallback;
|
||||
import org.springframework.data.couchbase.core.mapping.event.ReactiveBeforeConvertCallback;
|
||||
import org.springframework.data.couchbase.repository.support.MappingCouchbaseEntityInformation;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder;
|
||||
import org.springframework.data.mapping.callback.EntityCallbacks;
|
||||
import org.springframework.data.mapping.callback.ReactiveEntityCallbacks;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Internal encode/decode support for {@link ReactiveCouchbaseTemplate}.
|
||||
*
|
||||
* @author Carlos Espinaco
|
||||
* @author Michael Reiche
|
||||
* @since 4.2
|
||||
*/
|
||||
class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, ReactiveTemplateSupport {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReactiveCouchbaseTemplateSupport.class);
|
||||
class ReactiveCouchbaseTemplateSupport extends AbstractTemplateSupport
|
||||
implements ApplicationContextAware, ReactiveTemplateSupport {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
private final CouchbaseConverter converter;
|
||||
private final MappingContext<? extends CouchbasePersistentEntity<?>, CouchbasePersistentProperty> mappingContext;
|
||||
private final TranslationService translationService;
|
||||
private ReactiveEntityCallbacks reactiveEntityCallbacks;
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
public ReactiveCouchbaseTemplateSupport(final ReactiveCouchbaseTemplate template, final CouchbaseConverter converter,
|
||||
final TranslationService translationService) {
|
||||
super(template, converter, translationService);
|
||||
this.template = template;
|
||||
this.converter = converter;
|
||||
this.mappingContext = converter.getMappingContext();
|
||||
this.translationService = translationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,147 +63,22 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React
|
||||
.doOnNext(converted -> maybeEmitEvent(new BeforeSaveEvent<>(entityToEncode, converted)));
|
||||
}
|
||||
|
||||
@Override
|
||||
ReactiveCouchbaseTemplate getReactiveTemplate() {
|
||||
return template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope,
|
||||
String collection) {
|
||||
return Mono.fromSupplier(() -> {
|
||||
// this is the entity class defined for the repository. It may not be the class of the document that was read
|
||||
// we will reset it after reading the document
|
||||
//
|
||||
// This will fail for the case where:
|
||||
// 1) The version is defined in the concrete class, but not in the abstract class; and
|
||||
// 2) The constructor takes a "long version" argument resulting in an exception would be thrown if version in
|
||||
// the source is null.
|
||||
// We could expose from the MappingCouchbaseConverter determining the persistent entity from the source,
|
||||
// but that is a lot of work to do every time just for this very rare and avoidable case.
|
||||
// TypeInformation<? extends R> typeToUse = typeMapper.readType(source, type);
|
||||
|
||||
CouchbasePersistentEntity persistentEntity = couldBePersistentEntity(entityClass);
|
||||
|
||||
if (persistentEntity == null) { // method could return a Long, Boolean, String etc.
|
||||
// QueryExecutionConverters.unwrapWrapperTypes will recursively unwrap until there is nothing left
|
||||
// to unwrap. This results in List<String[]> being unwrapped past String[] to String, so this may also be a
|
||||
// Collection (or Array) of entityClass. We have no way of knowing - so just assume it is what we are told.
|
||||
// if this is a Collection or array, only the first element will be returned.
|
||||
final CouchbaseDocument converted = new CouchbaseDocument(id);
|
||||
Set<Map.Entry<String, Object>> set = ((CouchbaseDocument) translationService.decode(source, converted))
|
||||
.getContent().entrySet();
|
||||
return (T) set.iterator().next().getValue();
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
throw new CouchbaseException(TemplateUtils.SELECT_ID + " was null. Either use #{#n1ql.selectEntity} or project "
|
||||
+ TemplateUtils.SELECT_ID);
|
||||
}
|
||||
|
||||
final CouchbaseDocument converted = new CouchbaseDocument(id);
|
||||
|
||||
// if possible, set the version property in the source so that if the constructor has a long version argument,
|
||||
// it will have a value and not fail (as null is not a valid argument for a long argument). This possible failure
|
||||
// can be avoid by defining the argument as Long instead of long.
|
||||
// persistentEntity is still the (possibly abstract) class specified in the repository definition
|
||||
// it's possible that the abstract class does not have a version property, and this won't be able to set the version
|
||||
if (persistentEntity.getVersionProperty() != null) {
|
||||
if (cas == null) {
|
||||
throw new CouchbaseException("version/cas in the entity but " + TemplateUtils.SELECT_CAS
|
||||
+ " was not in result. Either use #{#n1ql.selectEntity} or project " + TemplateUtils.SELECT_CAS);
|
||||
}
|
||||
if (cas != 0) {
|
||||
converted.put(persistentEntity.getVersionProperty().getName(), cas);
|
||||
}
|
||||
}
|
||||
|
||||
// if the constructor has an argument that is long version, then construction will fail if the 'version'
|
||||
// is not available as 'null' is not a legal value for a long. Changing the arg to "Long version" would solve this.
|
||||
// (Version doesn't come from 'source', it comes from the cas argument to decodeEntity)
|
||||
T readEntity = converter.read(entityClass, (CouchbaseDocument) translationService.decode(source, converted));
|
||||
final ConvertingPropertyAccessor<T> accessor = getPropertyAccessor(readEntity);
|
||||
|
||||
persistentEntity = couldBePersistentEntity(readEntity.getClass());
|
||||
|
||||
if (cas != null && cas != 0 && persistentEntity.getVersionProperty() != null) {
|
||||
accessor.setProperty(persistentEntity.getVersionProperty(), cas);
|
||||
}
|
||||
N1qlJoinResolver.handleProperties(persistentEntity, accessor, template, id, scope, collection);
|
||||
return accessor.getBean();
|
||||
});
|
||||
}
|
||||
|
||||
CouchbasePersistentEntity couldBePersistentEntity(Class<?> entityClass) {
|
||||
if (ClassUtils.isPrimitiveOrWrapper(entityClass) || entityClass == String.class) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return mappingContext.getPersistentEntity(entityClass);
|
||||
} catch (InaccessibleObjectException t) {
|
||||
|
||||
}
|
||||
return null;
|
||||
String collection, Object txResultHolder, CouchbaseResourceHolder holder) {
|
||||
return Mono
|
||||
.fromSupplier(() -> decodeEntityBase(id, source, cas, entityClass, scope, collection, txResultHolder, holder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> applyUpdatedCas(final Object entity, CouchbaseDocument converted, final long cas) {
|
||||
return Mono.fromSupplier(() -> {
|
||||
Object returnValue;
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext
|
||||
.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
|
||||
|
||||
if (versionProperty != null) {
|
||||
accessor.setProperty(versionProperty, cas);
|
||||
returnValue = accessor.getBean();
|
||||
} else {
|
||||
returnValue = entity;
|
||||
}
|
||||
maybeEmitEvent(new AfterSaveEvent(returnValue, converted));
|
||||
return returnValue;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> applyUpdatedId(final Object entity, Object id) {
|
||||
return Mono.fromSupplier(() -> {
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext
|
||||
.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty idProperty = persistentEntity.getIdProperty();
|
||||
|
||||
if (idProperty != null) {
|
||||
accessor.setProperty(idProperty, id);
|
||||
return accessor.getBean();
|
||||
}
|
||||
return entity;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getCas(final Object entity) {
|
||||
final ConvertingPropertyAccessor<Object> accessor = getPropertyAccessor(entity);
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = persistentEntity.getVersionProperty();
|
||||
|
||||
long cas = 0;
|
||||
if (versionProperty != null) {
|
||||
Object casObject = accessor.getProperty(versionProperty);
|
||||
if (casObject instanceof Number) {
|
||||
cas = ((Number) casObject).longValue();
|
||||
}
|
||||
}
|
||||
return cas;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getJavaNameForEntity(final Class<?> clazz) {
|
||||
final CouchbasePersistentEntity<?> persistentEntity = mappingContext.getRequiredPersistentEntity(clazz);
|
||||
MappingCouchbaseEntityInformation<?, Object> info = new MappingCouchbaseEntityInformation<>(persistentEntity);
|
||||
return info.getJavaType().getName();
|
||||
}
|
||||
|
||||
private <T> ConvertingPropertyAccessor<T> getPropertyAccessor(final T source) {
|
||||
CouchbasePersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(source.getClass());
|
||||
PersistentPropertyAccessor<T> accessor = entity.getPropertyAccessor(source);
|
||||
return new ConvertingPropertyAccessor<>(accessor, converter.getConversionService());
|
||||
public <T> Mono<T> applyResult(T entity, CouchbaseDocument converted, Object id, Long cas,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder) {
|
||||
return Mono.fromSupplier(() -> applyResultBase(entity, converted, id, cas, txResultHolder, holder));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,24 +104,6 @@ class ReactiveCouchbaseTemplateSupport implements ApplicationContextAware, React
|
||||
this.reactiveEntityCallbacks = reactiveEntityCallbacks;
|
||||
}
|
||||
|
||||
public void maybeEmitEvent(CouchbaseMappingEvent<?> event) {
|
||||
if (canPublishEvent()) {
|
||||
try {
|
||||
this.applicationContext.publishEvent(event);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("{} thrown during {}", e, event);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
LOG.info("maybeEmitEvent called, but ReactiveCouchbaseTemplate not initialized with applicationContext");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean canPublishEvent() {
|
||||
return this.applicationContext != null;
|
||||
}
|
||||
|
||||
protected <T> Mono<T> maybeCallBeforeConvert(T object, String collection) {
|
||||
if (reactiveEntityCallbacks != null) {
|
||||
return reactiveEntityCallbacks.callback(ReactiveBeforeConvertCallback.class, object, collection);
|
||||
|
||||
@@ -32,6 +32,11 @@ import org.springframework.util.Assert;
|
||||
import com.couchbase.client.java.kv.ExistsOptions;
|
||||
import com.couchbase.client.java.kv.ExistsResult;
|
||||
|
||||
/**
|
||||
* ReactiveExistsById Support
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOperation {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
@@ -73,8 +78,10 @@ public class ReactiveExistsByIdOperationSupport implements ReactiveExistsByIdOpe
|
||||
@Override
|
||||
public Mono<Boolean> one(final String id) {
|
||||
PseudoArgs<ExistsOptions> pArgs = new PseudoArgs<>(template, scope, collection, options, domainType);
|
||||
LOG.trace("existsById key={} {}", id, pArgs);
|
||||
return Mono.just(id)
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("existsById key={} {}", id, pArgs);
|
||||
}
|
||||
return TransactionalSupport.verifyNotInTransaction("existsById").then(Mono.just(id))
|
||||
.flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive().exists(id, buildOptions(pArgs.getOptions()))
|
||||
.map(ExistsResult::exists))
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.data.couchbase.core;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.couchbase.core.query.AnalyticsQuery;
|
||||
import org.springframework.data.couchbase.core.query.OptionsBuilder;
|
||||
import org.springframework.data.couchbase.core.support.TemplateUtils;
|
||||
@@ -33,6 +35,8 @@ public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAn
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReactiveFindByAnalyticsOperationSupport.class);
|
||||
|
||||
public ReactiveFindByAnalyticsOperationSupport(final ReactiveCouchbaseTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
@@ -110,8 +114,11 @@ public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAn
|
||||
public Flux<T> all() {
|
||||
return Flux.defer(() -> {
|
||||
String statement = assembleEntityQuery(false);
|
||||
return template.getCouchbaseClientFactory().getCluster().reactive()
|
||||
.analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("findByAnalytics statement: {}", statement);
|
||||
}
|
||||
return TransactionalSupport.verifyNotInTransaction("findByAnalytics").then(template.getCouchbaseClientFactory()
|
||||
.getCluster().reactive().analyticsQuery(statement, buildAnalyticsOptions())).onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
@@ -132,7 +139,7 @@ public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAn
|
||||
}
|
||||
row.removeKey(TemplateUtils.SELECT_ID);
|
||||
row.removeKey(TemplateUtils.SELECT_CAS);
|
||||
return support.decodeEntity(id, row.toString(), cas, returnType, null, null);
|
||||
return support.decodeEntity(id, row.toString(), cas, returnType, null, null, null, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -141,8 +148,11 @@ public class ReactiveFindByAnalyticsOperationSupport implements ReactiveFindByAn
|
||||
public Mono<Long> count() {
|
||||
return Mono.defer(() -> {
|
||||
String statement = assembleEntityQuery(true);
|
||||
return template.getCouchbaseClientFactory().getCluster().reactive()
|
||||
.analyticsQuery(statement, buildAnalyticsOptions()).onErrorMap(throwable -> {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("findByAnalytics statement: {}", statement);
|
||||
}
|
||||
return TransactionalSupport.verifyNotInTransaction("findByAnalytics").then(template.getCouchbaseClientFactory()
|
||||
.getCluster().reactive().analyticsQuery(statement, buildAnalyticsOptions())).onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
|
||||
@@ -16,10 +16,12 @@
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import static com.couchbase.client.java.kv.GetAndTouchOptions.getAndTouchOptions;
|
||||
import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -39,6 +41,11 @@ import com.couchbase.client.java.codec.RawJsonTranscoder;
|
||||
import com.couchbase.client.java.kv.GetAndTouchOptions;
|
||||
import com.couchbase.client.java.kv.GetOptions;
|
||||
|
||||
/**
|
||||
* {@link ReactiveFindByIdOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperation {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
@@ -82,31 +89,44 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati
|
||||
|
||||
CommonOptions<?> gOptions = initGetOptions();
|
||||
PseudoArgs<?> pArgs = new PseudoArgs(template, scope, collection, gOptions, domainType);
|
||||
LOG.trace("findById key={} {}", id, pArgs);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("findById key={} {}", id, pArgs);
|
||||
}
|
||||
ReactiveCollection rc = template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive();
|
||||
|
||||
return Mono.just(id).flatMap(docId -> {
|
||||
ReactiveCollection reactive = template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive();
|
||||
if (pArgs.getOptions() instanceof GetAndTouchOptions) {
|
||||
return reactive.getAndTouch(docId, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions());
|
||||
Mono<T> reactiveEntity = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> {
|
||||
if (!ctxOpt.isPresent()) {
|
||||
if (pArgs.getOptions() instanceof GetAndTouchOptions) {
|
||||
return rc.getAndTouch(id, expiryToUse(), (GetAndTouchOptions) pArgs.getOptions())
|
||||
.flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType,
|
||||
pArgs.getScope(), pArgs.getCollection(), null, null));
|
||||
} else {
|
||||
return rc.get(id, (GetOptions) pArgs.getOptions())
|
||||
.flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType,
|
||||
pArgs.getScope(), pArgs.getCollection(), null, null));
|
||||
}
|
||||
} else {
|
||||
return reactive.get(docId, (GetOptions) pArgs.getOptions());
|
||||
return ctxOpt.get().getCore().get(makeCollectionIdentifier(rc.async()), id)
|
||||
.flatMap(result -> support.decodeEntity(id, new String(result.contentAsBytes(), StandardCharsets.UTF_8),
|
||||
result.cas(), domainType, pArgs.getScope(), pArgs.getCollection(),
|
||||
null, null));
|
||||
}
|
||||
}).flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), domainType,
|
||||
pArgs.getScope(), pArgs.getCollection())).onErrorResume(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
if (throwable instanceof DocumentNotFoundException) {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
return Mono.error(throwable);
|
||||
}).onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return reactiveEntity.onErrorResume(throwable -> {
|
||||
if (throwable instanceof DocumentNotFoundException) {
|
||||
return Mono.empty();
|
||||
}
|
||||
return Mono.error(throwable);
|
||||
}).onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -115,7 +135,7 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati
|
||||
}
|
||||
|
||||
@Override
|
||||
public TerminatingFindById<T> withOptions(final GetOptions options) {
|
||||
public FindByIdInScope<T> withOptions(final GetOptions options) {
|
||||
Assert.notNull(options, "Options must not be null.");
|
||||
return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, fields, expiry, support);
|
||||
}
|
||||
@@ -133,7 +153,7 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati
|
||||
}
|
||||
|
||||
@Override
|
||||
public FindByIdInScope<T> project(String... fields) {
|
||||
public FindByIdInCollection<T> project(String... fields) {
|
||||
Assert.notNull(fields, "Fields must not be null");
|
||||
return new ReactiveFindByIdSupport<>(template, domainType, scope, collection, options, Arrays.asList(fields),
|
||||
expiry, support);
|
||||
@@ -176,6 +196,7 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati
|
||||
}
|
||||
return expiryToUse;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,15 +20,20 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
import org.springframework.data.couchbase.core.query.OptionsBuilder;
|
||||
import org.springframework.data.couchbase.core.query.Query;
|
||||
import org.springframework.data.couchbase.core.support.PseudoArgs;
|
||||
import org.springframework.data.couchbase.core.support.TemplateUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.couchbase.client.java.ReactiveScope;
|
||||
import com.couchbase.client.java.query.QueryOptions;
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
import com.couchbase.client.java.query.ReactiveQueryResult;
|
||||
import com.couchbase.client.java.transactions.AttemptContextReactiveAccessor;
|
||||
import com.couchbase.client.java.transactions.TransactionQueryOptions;
|
||||
import com.couchbase.client.java.transactions.TransactionQueryResult;
|
||||
|
||||
/**
|
||||
* {@link ReactiveFindByQueryOperation} implementations for Couchbase.
|
||||
@@ -172,40 +177,51 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
|
||||
public Flux<T> all() {
|
||||
PseudoArgs<QueryOptions> pArgs = new PseudoArgs(template, scope, collection, options, domainType);
|
||||
String statement = assembleEntityQuery(false, distinctFields, pArgs.getScope(), pArgs.getCollection());
|
||||
LOG.trace("findByQuery {} statement: {}", pArgs, statement);
|
||||
Mono<ReactiveQueryResult> allResult = pArgs.getScope() == null
|
||||
? template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
|
||||
buildOptions(pArgs.getOptions()))
|
||||
: template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement,
|
||||
buildOptions(pArgs.getOptions()));
|
||||
return Flux.defer(() -> allResult.onErrorMap(throwable -> {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("findByQuery {} statement: {}", pArgs, statement);
|
||||
}
|
||||
CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory();
|
||||
ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive();
|
||||
|
||||
Mono<Object> allResult = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> {
|
||||
if (!s.isPresent()) {
|
||||
QueryOptions opts = buildOptions(pArgs.getOptions());
|
||||
return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts)
|
||||
: rs.query(statement, opts);
|
||||
} else {
|
||||
TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions());
|
||||
return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(),
|
||||
clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts);
|
||||
}
|
||||
});
|
||||
|
||||
return allResult.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
}).flatMapMany(ReactiveQueryResult::rowsAsObject).flatMap(row -> {
|
||||
String id = null;
|
||||
Long cas = null;
|
||||
if (query.isDistinct() || distinctFields != null) {
|
||||
id = "";
|
||||
cas = Long.valueOf(0);
|
||||
} else {
|
||||
id = row.getString(TemplateUtils.SELECT_ID);
|
||||
if (id == null) {
|
||||
id = row.getString(TemplateUtils.SELECT_ID_3x);
|
||||
row.removeKey(TemplateUtils.SELECT_ID_3x);
|
||||
}
|
||||
cas = row.getLong(TemplateUtils.SELECT_CAS);
|
||||
if (cas == null) {
|
||||
cas = row.getLong(TemplateUtils.SELECT_CAS_3x);
|
||||
row.removeKey(TemplateUtils.SELECT_CAS_3x);
|
||||
}
|
||||
row.removeKey(TemplateUtils.SELECT_ID);
|
||||
row.removeKey(TemplateUtils.SELECT_CAS);
|
||||
}
|
||||
return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection());
|
||||
}));
|
||||
}).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject()
|
||||
: Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject())).flatMap(row -> {
|
||||
String id = "";
|
||||
Long cas = Long.valueOf(0);
|
||||
if (!query.isDistinct() && distinctFields == null) {
|
||||
id = row.getString(TemplateUtils.SELECT_ID);
|
||||
if (id == null) {
|
||||
id = row.getString(TemplateUtils.SELECT_ID_3x);
|
||||
row.removeKey(TemplateUtils.SELECT_ID_3x);
|
||||
}
|
||||
cas = row.getLong(TemplateUtils.SELECT_CAS);
|
||||
if (cas == null) {
|
||||
cas = row.getLong(TemplateUtils.SELECT_CAS_3x);
|
||||
row.removeKey(TemplateUtils.SELECT_CAS_3x);
|
||||
}
|
||||
row.removeKey(TemplateUtils.SELECT_ID);
|
||||
row.removeKey(TemplateUtils.SELECT_CAS);
|
||||
}
|
||||
return support.decodeEntity(id, row.toString(), cas, returnType, pArgs.getScope(), pArgs.getCollection(),
|
||||
null, null);
|
||||
});
|
||||
}
|
||||
|
||||
public QueryOptions buildOptions(QueryOptions options) {
|
||||
@@ -213,24 +229,43 @@ public class ReactiveFindByQueryOperationSupport implements ReactiveFindByQueryO
|
||||
return query.buildQueryOptions(options, qsc);
|
||||
}
|
||||
|
||||
private TransactionQueryOptions buildTransactionOptions(QueryOptions options) {
|
||||
TransactionQueryOptions opts = OptionsBuilder.buildTransactionQueryOptions(buildOptions(options));
|
||||
return opts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Long> count() {
|
||||
PseudoArgs<QueryOptions> pArgs = new PseudoArgs(template, scope, collection, options, domainType);
|
||||
String statement = assembleEntityQuery(true, distinctFields, pArgs.getScope(), pArgs.getCollection());
|
||||
LOG.trace("findByQuery {} statement: {}", pArgs, statement);
|
||||
Mono<ReactiveQueryResult> countResult = pArgs.getScope() == null
|
||||
? template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
|
||||
buildOptions(pArgs.getOptions()))
|
||||
: template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement,
|
||||
buildOptions(pArgs.getOptions()));
|
||||
return Mono.defer(() -> countResult.onErrorMap(throwable -> {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("findByQuery {} statement: {}", pArgs, statement);
|
||||
}
|
||||
|
||||
CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory();
|
||||
ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive();
|
||||
|
||||
Mono<Object> allResult = TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> {
|
||||
if (!s.isPresent()) {
|
||||
QueryOptions opts = buildOptions(pArgs.getOptions());
|
||||
return pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts)
|
||||
: rs.query(statement, opts);
|
||||
} else {
|
||||
TransactionQueryOptions opts = buildTransactionOptions(pArgs.getOptions());
|
||||
return (AttemptContextReactiveAccessor.createReactiveTransactionAttemptContext(s.get().getCore(),
|
||||
clientFactory.getCluster().environment().jsonSerializer())).query(statement, opts);
|
||||
}
|
||||
});
|
||||
|
||||
return allResult.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
}).flatMapMany(ReactiveQueryResult::rowsAsObject).map(row -> row.getLong(row.getNames().iterator().next()))
|
||||
.next());
|
||||
}).flatMapMany(o -> o instanceof ReactiveQueryResult ? ((ReactiveQueryResult) o).rowsAsObject()
|
||||
: Flux.fromIterable(((TransactionQueryResult) o).rowsAsObject()))
|
||||
.map(row -> row.getLong(row.getNames().iterator().next())).next();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,6 +31,11 @@ import org.springframework.util.Assert;
|
||||
import com.couchbase.client.java.codec.RawJsonTranscoder;
|
||||
import com.couchbase.client.java.kv.GetAnyReplicaOptions;
|
||||
|
||||
/**
|
||||
* {@link ReactiveFindFromReplicasByIdOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFindFromReplicasByIdOperation {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
@@ -75,12 +80,14 @@ public class ReactiveFindFromReplicasByIdOperationSupport implements ReactiveFin
|
||||
garOptions.transcoder(RawJsonTranscoder.INSTANCE);
|
||||
}
|
||||
PseudoArgs<GetAnyReplicaOptions> pArgs = new PseudoArgs<>(template, scope, collection, garOptions, domainType);
|
||||
LOG.trace("getAnyReplica key={} {}", id, pArgs);
|
||||
return Mono.just(id)
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("getAnyReplica key={} {}", id, pArgs);
|
||||
}
|
||||
return TransactionalSupport.verifyNotInTransaction("findFromReplicasById").then(Mono.just(id))
|
||||
.flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive().getAnyReplica(docId, pArgs.getOptions()))
|
||||
.flatMap(result -> support.decodeEntity(id, result.contentAs(String.class), result.cas(), returnType,
|
||||
pArgs.getScope(), pArgs.getCollection()))
|
||||
pArgs.getScope(), pArgs.getCollection(), null, null))
|
||||
.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -33,6 +35,11 @@ import com.couchbase.client.java.kv.InsertOptions;
|
||||
import com.couchbase.client.java.kv.PersistTo;
|
||||
import com.couchbase.client.java.kv.ReplicateTo;
|
||||
|
||||
/**
|
||||
* {@link ReactiveInsertByIdOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
@@ -81,20 +88,48 @@ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOpe
|
||||
@Override
|
||||
public Mono<T> one(T object) {
|
||||
PseudoArgs<InsertOptions> pArgs = new PseudoArgs(template, scope, collection, options, domainType);
|
||||
LOG.trace("insertById object={} {}", object, pArgs);
|
||||
return Mono.just(object).flatMap(support::encodeEntity)
|
||||
.flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive()
|
||||
.insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted))
|
||||
.flatMap(result -> support.applyUpdatedId(object, converted.getId())
|
||||
.flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas()))))
|
||||
.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
});
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("insertById object={} {}", object, pArgs);
|
||||
}
|
||||
return Mono
|
||||
.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()))
|
||||
.flatMap(collection -> support.encodeEntity(object)
|
||||
.flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> {
|
||||
if (!ctxOpt.isPresent()) {
|
||||
return collection.reactive()
|
||||
.insert(converted.getId(), converted.export(), buildOptions(pArgs.getOptions(), converted))
|
||||
.flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(),
|
||||
null, null));
|
||||
} else {
|
||||
rejectInvalidTransactionalOptions();
|
||||
return ctxOpt.get().getCore()
|
||||
.insert(makeCollectionIdentifier(collection.async()), converted.getId(),
|
||||
template.getCouchbaseClientFactory().getCluster().environment().transcoder()
|
||||
.encode(converted.export()).encoded())
|
||||
.flatMap(result -> this.support.applyResult(object, converted, converted.getId(), result.cas(),
|
||||
null, null));
|
||||
}
|
||||
})).onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void rejectInvalidTransactionalOptions() {
|
||||
if ((this.persistTo != null && this.persistTo != PersistTo.NONE)
|
||||
|| (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) {
|
||||
throw new IllegalArgumentException(
|
||||
"withDurability PersistTo and ReplicateTo overload is not supported in a transaction");
|
||||
}
|
||||
if (this.expiry != null) {
|
||||
throw new IllegalArgumentException("withExpiry is not supported in a transaction");
|
||||
}
|
||||
if (this.options != null) {
|
||||
throw new IllegalArgumentException("withOptions is not supported in a transaction");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -147,6 +182,7 @@ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOpe
|
||||
return new ReactiveInsertByIdSupport<>(template, domainType, scope, collection, options, persistTo, replicateTo,
|
||||
durabilityLevel, expiry, support);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.couchbase.client.java.kv.ReplicateTo;
|
||||
* Remove Operations on KV service.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @author Michael Reiche
|
||||
* @since 2.0
|
||||
*/
|
||||
public interface ReactiveRemoveByIdOperation {
|
||||
@@ -63,6 +64,14 @@ public interface ReactiveRemoveByIdOperation {
|
||||
@Override
|
||||
Mono<RemoveResult> one(String id);
|
||||
|
||||
/**
|
||||
* Remove one document. Requires whole entity for transaction to have the cas.
|
||||
*
|
||||
* @param entity the entity
|
||||
* @return result of the remove
|
||||
*/
|
||||
Mono<RemoveResult> oneEntity(Object entity);
|
||||
|
||||
/**
|
||||
* Remove the documents in the collection.
|
||||
*
|
||||
@@ -72,6 +81,14 @@ public interface ReactiveRemoveByIdOperation {
|
||||
@Override
|
||||
Flux<RemoveResult> all(Collection<String> ids);
|
||||
|
||||
/**
|
||||
* Remove the documents in the collection. Requires whole entity for transaction to have the cas.
|
||||
*
|
||||
* @param ids the document IDs.
|
||||
* @return result of the removes.
|
||||
*/
|
||||
Flux<RemoveResult> allEntities(Collection<Object> ids);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -22,15 +24,24 @@ import java.util.Collection;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
import org.springframework.data.couchbase.core.query.OptionsBuilder;
|
||||
import org.springframework.data.couchbase.core.support.PseudoArgs;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.couchbase.client.core.msg.kv.DurabilityLevel;
|
||||
import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
|
||||
import com.couchbase.client.core.transaction.CoreTransactionGetResult;
|
||||
import com.couchbase.client.java.ReactiveCollection;
|
||||
import com.couchbase.client.java.kv.PersistTo;
|
||||
import com.couchbase.client.java.kv.RemoveOptions;
|
||||
import com.couchbase.client.java.kv.ReplicateTo;
|
||||
|
||||
/**
|
||||
* {@link ReactiveRemoveByIdOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
@@ -82,18 +93,57 @@ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOpe
|
||||
@Override
|
||||
public Mono<RemoveResult> one(final String id) {
|
||||
PseudoArgs<RemoveOptions> pArgs = new PseudoArgs<>(template, scope, collection, options, domainType);
|
||||
LOG.trace("removeById key={} {}", id, pArgs);
|
||||
return Mono.just(id)
|
||||
.flatMap(docId -> template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive().remove(id, buildRemoveOptions(pArgs.getOptions()))
|
||||
.map(r -> RemoveResult.from(docId, r)))
|
||||
.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("removeById key={} {}", id, pArgs);
|
||||
}
|
||||
CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory();
|
||||
ReactiveCollection rc = clientFactory.withScope(pArgs.getScope()).getCollection(pArgs.getCollection()).reactive();
|
||||
|
||||
return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(s -> {
|
||||
if (!s.isPresent()) {
|
||||
return rc.remove(id, buildRemoveOptions(pArgs.getOptions())).map(r -> RemoveResult.from(id, r));
|
||||
} else {
|
||||
rejectInvalidTransactionalOptions();
|
||||
|
||||
if (cas == null || cas == 0) {
|
||||
throw new IllegalArgumentException("cas must be supplied for tx remove");
|
||||
}
|
||||
CoreTransactionAttemptContext ctx = s.get().getCore();
|
||||
Mono<CoreTransactionGetResult> gr = ctx.get(makeCollectionIdentifier(rc.async()), id);
|
||||
|
||||
return gr.flatMap(getResult -> {
|
||||
if (getResult.cas() != cas) {
|
||||
return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(ctx, getResult.cas(), cas));
|
||||
}
|
||||
return ctx.remove(getResult).map(r -> new RemoveResult(id, 0, null));
|
||||
});
|
||||
|
||||
}
|
||||
}).onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void rejectInvalidTransactionalOptions() {
|
||||
if ((this.persistTo != null && this.persistTo != PersistTo.NONE)
|
||||
|| (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) {
|
||||
throw new IllegalArgumentException(
|
||||
"withDurability PersistTo and ReplicateTo overload is not supported in a transaction");
|
||||
}
|
||||
if (this.options != null) {
|
||||
throw new IllegalArgumentException("withOptions is not supported in a transaction");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<RemoveResult> oneEntity(Object entity) {
|
||||
ReactiveRemoveByIdSupport op = new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options,
|
||||
persistTo, replicateTo, durabilityLevel, template.support().getCas(entity));
|
||||
return op.one(template.support().getId(entity).toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,6 +151,11 @@ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOpe
|
||||
return Flux.fromIterable(ids).flatMap(this::one);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<RemoveResult> allEntities(Collection<Object> entities) {
|
||||
return Flux.fromIterable(entities).flatMap(this::oneEntity);
|
||||
}
|
||||
|
||||
private RemoveOptions buildRemoveOptions(RemoveOptions options) {
|
||||
return OptionsBuilder.buildRemoveOptions(options, persistTo, replicateTo, durabilityLevel, cas);
|
||||
}
|
||||
@@ -144,6 +199,7 @@ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOpe
|
||||
return new ReactiveRemoveByIdSupport(template, domainType, scope, collection, options, persistTo, replicateTo,
|
||||
durabilityLevel, cas);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,22 +16,31 @@
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
import org.springframework.data.couchbase.core.query.OptionsBuilder;
|
||||
import org.springframework.data.couchbase.core.query.Query;
|
||||
import org.springframework.data.couchbase.core.support.PseudoArgs;
|
||||
import org.springframework.data.couchbase.core.support.TemplateUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.couchbase.client.java.ReactiveScope;
|
||||
import com.couchbase.client.java.json.JsonObject;
|
||||
import com.couchbase.client.java.query.QueryOptions;
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
import com.couchbase.client.java.query.ReactiveQueryResult;
|
||||
import com.couchbase.client.java.transactions.TransactionQueryOptions;
|
||||
|
||||
/**
|
||||
* {@link ReactiveRemoveByQueryOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQueryOperation {
|
||||
|
||||
private static final Query ALL_QUERY = new Query();
|
||||
@@ -74,21 +83,34 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu
|
||||
public Flux<RemoveResult> all() {
|
||||
PseudoArgs<QueryOptions> pArgs = new PseudoArgs<>(template, scope, collection, options, domainType);
|
||||
String statement = assembleDeleteQuery(pArgs.getScope(), pArgs.getCollection());
|
||||
LOG.trace("removeByQuery {} statement: {}", pArgs, statement);
|
||||
Mono<ReactiveQueryResult> allResult = pArgs.getScope() == null
|
||||
? template.getCouchbaseClientFactory().getCluster().reactive().query(statement,
|
||||
buildQueryOptions(pArgs.getOptions()))
|
||||
: template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getScope().reactive().query(statement,
|
||||
buildQueryOptions(pArgs.getOptions()));
|
||||
return Flux.defer(() -> allResult.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("removeByQuery {} statement: {}", pArgs, statement);
|
||||
}
|
||||
CouchbaseClientFactory clientFactory = template.getCouchbaseClientFactory();
|
||||
ReactiveScope rs = clientFactory.withScope(pArgs.getScope()).getScope().reactive();
|
||||
|
||||
return TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMapMany(transactionContext -> {
|
||||
|
||||
if (!transactionContext.isPresent()) {
|
||||
QueryOptions opts = buildQueryOptions(pArgs.getOptions());
|
||||
return (pArgs.getScope() == null ? clientFactory.getCluster().reactive().query(statement, opts)
|
||||
: rs.query(statement, opts)).flatMapMany(ReactiveQueryResult::rowsAsObject)
|
||||
.map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID),
|
||||
row.getLong(TemplateUtils.SELECT_CAS), Optional.empty()));
|
||||
} else {
|
||||
return throwable;
|
||||
TransactionQueryOptions opts = OptionsBuilder
|
||||
.buildTransactionQueryOptions(buildQueryOptions(pArgs.getOptions()));
|
||||
ObjectNode convertedOptions = com.couchbase.client.java.transactions.internal.OptionsUtil
|
||||
.createTransactionOptions(pArgs.getScope() == null ? null : rs, statement, opts);
|
||||
return transactionContext.get().getCore()
|
||||
.queryBlocking(statement, template.getBucketName(), pArgs.getScope(), convertedOptions, false)
|
||||
.flatMapIterable(result -> result.rows).map(row -> {
|
||||
JsonObject json = JsonObject.fromJson(row.data());
|
||||
return new RemoveResult(json.getString(TemplateUtils.SELECT_ID), json.getLong(TemplateUtils.SELECT_CAS),
|
||||
Optional.empty());
|
||||
});
|
||||
}
|
||||
}).flatMapMany(ReactiveQueryResult::rowsAsObject)
|
||||
.map(row -> new RemoveResult(row.getString(TemplateUtils.SELECT_ID), row.getLong(TemplateUtils.SELECT_CAS),
|
||||
Optional.empty())));
|
||||
});
|
||||
}
|
||||
|
||||
private QueryOptions buildQueryOptions(QueryOptions options) {
|
||||
@@ -138,6 +160,7 @@ public class ReactiveRemoveByQueryOperationSupport implements ReactiveRemoveByQu
|
||||
return new ReactiveRemoveByQuerySupport<>(template, domainType, query, scanConsistency,
|
||||
scope != null ? scope : this.scope, collection, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import static com.couchbase.client.java.transactions.internal.ConverterUtil.makeCollectionIdentifier;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -28,11 +30,20 @@ import org.springframework.data.couchbase.core.query.OptionsBuilder;
|
||||
import org.springframework.data.couchbase.core.support.PseudoArgs;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.couchbase.client.core.io.CollectionIdentifier;
|
||||
import com.couchbase.client.core.msg.kv.DurabilityLevel;
|
||||
import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
|
||||
import com.couchbase.client.core.transaction.CoreTransactionGetResult;
|
||||
import com.couchbase.client.core.transaction.util.DebugUtil;
|
||||
import com.couchbase.client.java.kv.PersistTo;
|
||||
import com.couchbase.client.java.kv.ReplaceOptions;
|
||||
import com.couchbase.client.java.kv.ReplicateTo;
|
||||
|
||||
/**
|
||||
* {@link ReactiveReplaceByIdOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
@@ -81,20 +92,63 @@ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdO
|
||||
@Override
|
||||
public Mono<T> one(T object) {
|
||||
PseudoArgs<ReplaceOptions> pArgs = new PseudoArgs<>(template, scope, collection, options, domainType);
|
||||
LOG.trace("replaceById object={} {}", object, pArgs);
|
||||
return Mono.just(object).flatMap(support::encodeEntity)
|
||||
.flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive()
|
||||
.replace(converted.getId(), converted.export(),
|
||||
buildReplaceOptions(pArgs.getOptions(), object, converted))
|
||||
.flatMap(result -> support.applyUpdatedCas(object, converted, result.cas())))
|
||||
.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
});
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("replaceById object={} {}", object, pArgs);
|
||||
}
|
||||
return Mono
|
||||
.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope()).getCollection(pArgs.getCollection()))
|
||||
.flatMap(collection -> support.encodeEntity(object)
|
||||
.flatMap(converted -> TransactionalSupport.checkForTransactionInThreadLocalStorage().flatMap(ctxOpt -> {
|
||||
if (!ctxOpt.isPresent()) {
|
||||
return collection.reactive()
|
||||
.replace(converted.getId(), converted.export(),
|
||||
buildReplaceOptions(pArgs.getOptions(), object, converted))
|
||||
.flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null,
|
||||
null));
|
||||
} else {
|
||||
rejectInvalidTransactionalOptions();
|
||||
|
||||
Long cas = support.getCas(object);
|
||||
if (cas == null || cas == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"cas must be supplied in object for tx replace. object=" + object);
|
||||
}
|
||||
|
||||
CollectionIdentifier collId = makeCollectionIdentifier(collection.async());
|
||||
CoreTransactionAttemptContext ctx = ctxOpt.get().getCore();
|
||||
ctx.logger().info(ctx.attemptId(), "refetching %s for Spring replace",
|
||||
DebugUtil.docId(collId, converted.getId()));
|
||||
Mono<CoreTransactionGetResult> gr = ctx.get(collId, converted.getId());
|
||||
|
||||
return gr.flatMap(getResult -> {
|
||||
if (getResult.cas() != cas) {
|
||||
return Mono.error(TransactionalSupport.retryTransactionOnCasMismatch(ctx, getResult.cas(), cas));
|
||||
}
|
||||
return ctx.replace(getResult, template.getCouchbaseClientFactory().getCluster().environment()
|
||||
.transcoder().encode(converted.export()).encoded());
|
||||
}).flatMap(result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, null));
|
||||
}
|
||||
})).onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void rejectInvalidTransactionalOptions() {
|
||||
if ((this.persistTo != null && this.persistTo != PersistTo.NONE)
|
||||
|| (this.replicateTo != null && this.replicateTo != ReplicateTo.NONE)) {
|
||||
throw new IllegalArgumentException(
|
||||
"withDurability PersistTo and ReplicateTo overload is not supported in a transaction");
|
||||
}
|
||||
if (this.expiry != null) {
|
||||
throw new IllegalArgumentException("withExpiry is not supported in a transaction");
|
||||
}
|
||||
if (this.options != null) {
|
||||
throw new IllegalArgumentException("withOptions is not supported in a transaction");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,25 +17,30 @@ package org.springframework.data.couchbase.core;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
|
||||
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder;
|
||||
|
||||
/**
|
||||
* ReactiveTemplateSupport
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public interface ReactiveTemplateSupport {
|
||||
|
||||
Mono<CouchbaseDocument> encodeEntity(Object entityToEncode);
|
||||
|
||||
<T> Mono<T> decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope, String collection);
|
||||
<T> Mono<T> decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope, String collection,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder);
|
||||
|
||||
<T> Mono<T> applyUpdatedCas(T entity, CouchbaseDocument converted, long cas);
|
||||
|
||||
<T> Mono<T> applyUpdatedId(T entity, Object id);
|
||||
<T> Mono<T> applyResult(T entity, CouchbaseDocument converted, Object id, Long cas,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder);
|
||||
|
||||
Long getCas(Object entity);
|
||||
|
||||
Object getId(Object entity);
|
||||
|
||||
String getJavaNameForEntity(Class<?> clazz);
|
||||
|
||||
void maybeEmitEvent(CouchbaseMappingEvent<?> event);
|
||||
TranslationService getTranslationService();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ import com.couchbase.client.java.kv.PersistTo;
|
||||
import com.couchbase.client.java.kv.ReplicateTo;
|
||||
import com.couchbase.client.java.kv.UpsertOptions;
|
||||
|
||||
/**
|
||||
* {@link ReactiveUpsertByIdOperation} implementations for Couchbase.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOperation {
|
||||
|
||||
private final ReactiveCouchbaseTemplate template;
|
||||
@@ -81,20 +86,27 @@ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOpe
|
||||
@Override
|
||||
public Mono<T> one(T object) {
|
||||
PseudoArgs<UpsertOptions> pArgs = new PseudoArgs(template, scope, collection, options, domainType);
|
||||
LOG.trace("upsertById object={} {}", object, pArgs);
|
||||
return Mono.just(object).flatMap(support::encodeEntity)
|
||||
.flatMap(converted -> template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()).reactive()
|
||||
.upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted))
|
||||
.flatMap(result -> support.applyUpdatedId(object, converted.getId())
|
||||
.flatMap(updatedObject -> support.applyUpdatedCas(updatedObject, converted, result.cas()))))
|
||||
.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("upsertById object={} {}", object, pArgs);
|
||||
}
|
||||
Mono<T> reactiveEntity = TransactionalSupport.verifyNotInTransaction("upsertById")
|
||||
.then(support.encodeEntity(object)).flatMap(converted -> {
|
||||
return Mono
|
||||
.just(template.getCouchbaseClientFactory().withScope(pArgs.getScope())
|
||||
.getCollection(pArgs.getCollection()))
|
||||
.flatMap(collection -> collection.reactive()
|
||||
.upsert(converted.getId(), converted.export(), buildUpsertOptions(pArgs.getOptions(), converted))
|
||||
.flatMap(
|
||||
result -> support.applyResult(object, converted, converted.getId(), result.cas(), null, null)));
|
||||
});
|
||||
|
||||
return reactiveEntity.onErrorMap(throwable -> {
|
||||
if (throwable instanceof RuntimeException) {
|
||||
return template.potentiallyConvertRuntimeException((RuntimeException) throwable);
|
||||
} else {
|
||||
return throwable;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,26 +15,30 @@
|
||||
*/
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
|
||||
import org.springframework.data.couchbase.core.mapping.event.CouchbaseMappingEvent;
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public interface TemplateSupport {
|
||||
|
||||
CouchbaseDocument encodeEntity(Object entityToEncode);
|
||||
|
||||
<T> T decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope, String collection);
|
||||
<T> T decodeEntity(String id, String source, Long cas, Class<T> entityClass, String scope, String collection,
|
||||
Object txResultHolder, CouchbaseResourceHolder holder);
|
||||
|
||||
<T> T applyUpdatedCas(T entity, CouchbaseDocument converted, long cas);
|
||||
<T> T applyResult(T entity, CouchbaseDocument converted, Object id, long cas, Object txResultHolder,
|
||||
CouchbaseResourceHolder holder);
|
||||
|
||||
<T> T applyUpdatedId(T entity, Object id);
|
||||
Long getCas(Object entity);
|
||||
|
||||
long getCas(Object entity);
|
||||
Object getId(Object entity);
|
||||
|
||||
String getJavaNameForEntity(Class<?> clazz);
|
||||
|
||||
void maybeEmitEvent(CouchbaseMappingEvent<?> event);
|
||||
<T> Integer getTxResultHolder(T source);
|
||||
|
||||
TranslationService getTranslationService();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.core;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.couchbase.transaction.CouchbaseResourceHolder;
|
||||
|
||||
import com.couchbase.client.core.annotation.Stability;
|
||||
import com.couchbase.client.core.error.CasMismatchException;
|
||||
import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
|
||||
import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
|
||||
import com.couchbase.client.core.transaction.threadlocal.TransactionMarkerOwner;
|
||||
|
||||
/**
|
||||
* Utility methods to support transactions.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
@Stability.Internal
|
||||
public class TransactionalSupport {
|
||||
|
||||
/**
|
||||
* Returns non-empty iff in a transaction. It determines this from thread-local storage and/or reactive context.
|
||||
* <p>
|
||||
* The user could be doing a reactive operation (with .block()) inside a blocking transaction (like @Transactional).
|
||||
* Or a blocking operation inside a ReactiveTransactionsWrapper transaction (which would be a bad idea). So, need to
|
||||
* check both thread-local storage and reactive context.
|
||||
*/
|
||||
public static Mono<Optional<CouchbaseResourceHolder>> checkForTransactionInThreadLocalStorage() {
|
||||
return TransactionMarkerOwner.get().flatMap(markerOpt -> {
|
||||
Optional<CouchbaseResourceHolder> out = markerOpt
|
||||
.flatMap(marker -> Optional.of(new CouchbaseResourceHolder(marker.context())));
|
||||
return Mono.just(out);
|
||||
});
|
||||
}
|
||||
|
||||
public static Mono<Void> verifyNotInTransaction(String methodName) {
|
||||
return checkForTransactionInThreadLocalStorage().flatMap(s -> {
|
||||
if (s.isPresent()) {
|
||||
return Mono.error(new IllegalArgumentException(methodName + "can not be used inside a transaction"));
|
||||
} else {
|
||||
return Mono.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static RuntimeException retryTransactionOnCasMismatch(CoreTransactionAttemptContext ctx, long cas1,
|
||||
long cas2) {
|
||||
try {
|
||||
ctx.logger().info(ctx.attemptId(), "Spring CAS mismatch %s != %s, retrying transaction", cas1, cas2);
|
||||
TransactionOperationFailedException err = TransactionOperationFailedException.Builder.createError()
|
||||
.retryTransaction().cause(new CasMismatchException(null)).build();
|
||||
return ctx.operationFailed(err);
|
||||
} catch (Throwable err) {
|
||||
return new RuntimeException(err);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -49,7 +50,13 @@ import com.couchbase.client.java.kv.ReplicateTo;
|
||||
import com.couchbase.client.java.kv.UpsertOptions;
|
||||
import com.couchbase.client.java.query.QueryOptions;
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
import com.couchbase.client.java.transactions.TransactionQueryOptions;
|
||||
|
||||
/**
|
||||
* Methods for building Options objects for Couchbae APIs.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class OptionsBuilder {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OptionsBuilder.class);
|
||||
@@ -70,8 +77,8 @@ public class OptionsBuilder {
|
||||
QueryScanConsistency metaQueryScanConsistency = meta.get(SCAN_CONSISTENCY) != null
|
||||
? ((ScanConsistency) meta.get(SCAN_CONSISTENCY)).query()
|
||||
: null;
|
||||
QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, getScanConsistency(optsJson),
|
||||
scanConsistency, metaQueryScanConsistency);
|
||||
QueryScanConsistency qsc = fromFirst(QueryScanConsistency.NOT_BOUNDED, query.getScanConsistency(),
|
||||
getScanConsistency(optsJson), scanConsistency, metaQueryScanConsistency);
|
||||
Duration timeout = fromFirst(Duration.ofSeconds(0), getTimeout(optsBuilt), meta.get(TIMEOUT));
|
||||
RetryStrategy retryStrategy = fromFirst(null, getRetryStrategy(optsBuilt), meta.get(RETRY_STRATEGY));
|
||||
|
||||
@@ -84,12 +91,32 @@ public class OptionsBuilder {
|
||||
if (retryStrategy != null) {
|
||||
options.retryStrategy(retryStrategy);
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("query options: {}", getQueryOpts(options.build()));
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("query options: {}", getQueryOpts(options.build()));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
public static TransactionQueryOptions buildTransactionQueryOptions(QueryOptions options) {
|
||||
QueryOptions.Built built = options.build();
|
||||
TransactionQueryOptions txOptions = TransactionQueryOptions.queryOptions();
|
||||
|
||||
JsonObject optsJson = getQueryOpts(built);
|
||||
|
||||
if (optsJson.containsKey("use_fts")) {
|
||||
throw new IllegalArgumentException("QueryOptions.flexIndex is not supported in a transaction");
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Object> entry : optsJson.toMap().entrySet()) {
|
||||
txOptions.raw(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("query options: {}", optsJson);
|
||||
}
|
||||
return txOptions;
|
||||
}
|
||||
|
||||
public static ExistsOptions buildExistsOptions(ExistsOptions options) {
|
||||
options = options != null ? options : ExistsOptions.existsOptions();
|
||||
return options;
|
||||
@@ -108,8 +135,8 @@ public class OptionsBuilder {
|
||||
} else if (doc.getExpiration() != 0) {
|
||||
options.expiry(Duration.ofSeconds(doc.getExpiration()));
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("insert options: {}" + toString(options));
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("insert options: {}" + toString(options));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -127,8 +154,8 @@ public class OptionsBuilder {
|
||||
} else if (doc.getExpiration() != 0) {
|
||||
options.expiry(Duration.ofSeconds(doc.getExpiration()));
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("upsert options: {}" + toString(options));
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("upsert options: {}" + toString(options));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -149,8 +176,8 @@ public class OptionsBuilder {
|
||||
if (cas != null) {
|
||||
options.cas(cas);
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("replace options: {}" + toString(options));
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("replace options: {}" + toString(options));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -176,14 +203,14 @@ public class OptionsBuilder {
|
||||
if (cas != null) {
|
||||
options.cas(cas);
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("remove options: {}", toString(options));
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("remove options: {}", toString(options));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* scope annotation could be a
|
||||
* scope annotation
|
||||
*
|
||||
* @param domainType
|
||||
* @return
|
||||
@@ -199,6 +226,12 @@ public class OptionsBuilder {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* collection annotation
|
||||
*
|
||||
* @param domainType
|
||||
* @return
|
||||
*/
|
||||
public static String getCollectionFrom(Class<?> domainType) {
|
||||
if (domainType == null) {
|
||||
return null;
|
||||
@@ -423,4 +456,5 @@ public class OptionsBuilder {
|
||||
AnnotatedElement[] elements) {
|
||||
return annotationString(annotation, "value", defaultValue, elements);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,13 +19,13 @@ import static org.springframework.data.couchbase.core.query.OptionsBuilder.fromF
|
||||
|
||||
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
|
||||
|
||||
import com.couchbase.client.core.error.CouchbaseException;
|
||||
import com.couchbase.client.core.io.CollectionIdentifier;
|
||||
|
||||
/**
|
||||
* determine the arguments to be used in the operation from various sources
|
||||
* Determine the arguments to be used in the operation from various sources
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*
|
||||
* @param <OPTS>
|
||||
*/
|
||||
public class PseudoArgs<OPTS> {
|
||||
@@ -96,7 +96,7 @@ public class PseudoArgs<OPTS> {
|
||||
// if a collection was specified but no scope, use the scope from the clientFactory
|
||||
|
||||
if (collectionForQuery != null && scopeForQuery == null) {
|
||||
scopeForQuery = template.getCouchbaseClientFactory().getScope().name();
|
||||
scopeForQuery = template.getScopeName();
|
||||
}
|
||||
|
||||
// specifying scope and collection = _default is not necessary and will fail if server doesn't have collections
|
||||
@@ -110,6 +110,10 @@ public class PseudoArgs<OPTS> {
|
||||
|
||||
this.scopeName = scopeForQuery;
|
||||
this.collectionName = collectionForQuery;
|
||||
if (scopeForQuery != null && collectionForQuery == null) {
|
||||
throw new CouchbaseException(
|
||||
new IllegalArgumentException("if scope is not default or null, then collection must be specified"));
|
||||
}
|
||||
this.options = optionsForQuery;
|
||||
|
||||
}
|
||||
@@ -139,4 +143,5 @@ public class PseudoArgs<OPTS> {
|
||||
public String toString() {
|
||||
return "scope: " + getScope() + " collection: " + getCollection() + " options: " + getOptions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,14 @@ import org.springframework.data.couchbase.repository.query.CouchbaseEntityInform
|
||||
import com.couchbase.client.core.io.CollectionIdentifier;
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
|
||||
/**
|
||||
* Common base for SimpleCouchbaseRepository and SimpleReactiveCouchbaseRepository
|
||||
*
|
||||
* @param <T>
|
||||
* @param <ID>
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class CouchbaseRepositoryBase<T, ID> {
|
||||
|
||||
/**
|
||||
@@ -60,7 +68,7 @@ public class CouchbaseRepositoryBase<T, ID> {
|
||||
}
|
||||
|
||||
<S extends T> String getId(S entity) {
|
||||
return getEntityInformation().getId(entity);
|
||||
return String.valueOf(getEntityInformation().getId(entity));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021 the original author or authors.
|
||||
* Copyright 2021-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,7 +44,7 @@ public class DynamicInvocationHandler<T> implements InvocationHandler {
|
||||
final ReactiveCouchbaseTemplate reactiveTemplate;
|
||||
CommonOptions<?> options;
|
||||
String collection;
|
||||
String scope;;
|
||||
String scope;
|
||||
|
||||
public DynamicInvocationHandler(T target, CommonOptions<?> options, String collection, String scope) {
|
||||
this.target = target;
|
||||
@@ -52,10 +52,12 @@ public class DynamicInvocationHandler<T> implements InvocationHandler {
|
||||
reactiveTemplate = ((CouchbaseTemplate) ((CouchbaseRepository) target).getOperations()).reactive();
|
||||
this.entityInformation = ((CouchbaseRepository<?, String>) target).getEntityInformation();
|
||||
} else if (target instanceof ReactiveCouchbaseRepository) {
|
||||
reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) target).getOperations();
|
||||
this.entityInformation = ((ReactiveCouchbaseRepository<?, String>) target).getEntityInformation();
|
||||
reactiveTemplate = (ReactiveCouchbaseTemplate) ((ReactiveCouchbaseRepository) this.target).getOperations();
|
||||
this.entityInformation = ((ReactiveCouchbaseRepository<?, String>) this.target).getEntityInformation();
|
||||
} else {
|
||||
throw new RuntimeException("Unknown target type: " + target.getClass());
|
||||
throw new RuntimeException("Unknown target type: " + target.getClass()
|
||||
+ " CouchbaseRepository.class.isAssignable:" + CouchbaseRepository.class.isAssignableFrom(target.getClass())
|
||||
+ " " + dumpInterfaces(target.getClass(), " "));
|
||||
}
|
||||
this.options = options;
|
||||
this.collection = collection;
|
||||
@@ -63,6 +65,18 @@ public class DynamicInvocationHandler<T> implements InvocationHandler {
|
||||
this.repositoryClass = target.getClass();
|
||||
}
|
||||
|
||||
String dumpInterfaces(Class clazz, String tab) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(tab + "{");
|
||||
for (Class c : clazz.getInterfaces()) {
|
||||
sb.append(tab + " " + c.getSimpleName());
|
||||
if (c.getInterfaces().length > 0)
|
||||
sb.append(dumpInterfaces(c, tab + " "));
|
||||
}
|
||||
sb.append(tab + "}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.util.StreamUtils;
|
||||
import org.springframework.data.util.Streamable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Repository base implementation for Couchbase.
|
||||
@@ -71,28 +71,7 @@ public class SimpleCouchbaseRepository<T, ID> extends CouchbaseRepositoryBase<T,
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <S extends T> S save(S entity) {
|
||||
Assert.notNull(entity, "Entity must not be null!");
|
||||
S result;
|
||||
|
||||
final CouchbasePersistentEntity<?> mapperEntity = operations.getConverter().getMappingContext()
|
||||
.getPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty();
|
||||
final boolean versionPresent = versionProperty != null;
|
||||
final Long version = versionProperty == null || versionProperty.getField() == null ? null
|
||||
: (Long) ReflectionUtils.getField(versionProperty.getField(), entity);
|
||||
final boolean existingDocument = version != null && version > 0;
|
||||
|
||||
if (!versionPresent) { // the entity doesn't have a version property
|
||||
// No version field - no cas
|
||||
result = (S) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity);
|
||||
} else if (existingDocument) { // there is a version property, and it is non-zero
|
||||
// Updating existing document with cas
|
||||
result = (S) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity);
|
||||
} else { // there is a version property, but it's zero or not set.
|
||||
// Creating new document
|
||||
result = (S) operations.insertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity);
|
||||
}
|
||||
return result;
|
||||
return operations.save(entity, getScope(), getCollection());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -97,27 +97,7 @@ public class SimpleReactiveCouchbaseRepository<T, ID> extends CouchbaseRepositor
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <S extends T> Mono<S> save(S entity, String scope, String collection) {
|
||||
Assert.notNull(entity, "Entity must not be null!");
|
||||
Mono<S> result;
|
||||
final CouchbasePersistentEntity<?> mapperEntity = operations.getConverter().getMappingContext()
|
||||
.getPersistentEntity(entity.getClass());
|
||||
final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty();
|
||||
final boolean versionPresent = versionProperty != null;
|
||||
final Long version = versionProperty == null || versionProperty.getField() == null ? null
|
||||
: (Long) ReflectionUtils.getField(versionProperty.getField(), entity);
|
||||
final boolean existingDocument = version != null && version > 0;
|
||||
|
||||
if (!versionPresent) { // the entity doesn't have a version property
|
||||
// No version field - no cas
|
||||
result = (Mono<S>) operations.upsertById(getJavaType()).inScope(scope).inCollection(collection).one(entity);
|
||||
} else if (existingDocument) { // there is a version property, and it is non-zero
|
||||
// Updating existing document with cas
|
||||
result = (Mono<S>) operations.replaceById(getJavaType()).inScope(scope).inCollection(collection).one(entity);
|
||||
} else { // there is a version property, but it's zero or not set.
|
||||
// Creating new document
|
||||
result = (Mono<S>) operations.insertById(getJavaType()).inScope(scope).inCollection(collection).one(entity);
|
||||
}
|
||||
return result;
|
||||
return operations.save(entity, scope, collection);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,7 +182,7 @@ public class SimpleReactiveCouchbaseRepository<T, ID> extends CouchbaseRepositor
|
||||
|
||||
private Mono<Void> delete(T entity, String scope, String collection) {
|
||||
Assert.notNull(entity, "Entity must not be null!");
|
||||
return operations.removeById(getJavaType()).inScope(scope).inCollection(collection).one(getId(entity)).then();
|
||||
return operations.removeById(getJavaType()).inScope(scope).inCollection(collection).oneEntity(entity).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,7 +194,7 @@ public class SimpleReactiveCouchbaseRepository<T, ID> extends CouchbaseRepositor
|
||||
@Override
|
||||
public Mono<Void> deleteAll(Iterable<? extends T> entities) {
|
||||
return operations.removeById(getJavaType()).inScope(getScope()).inCollection(getCollection())
|
||||
.all(Streamable.of(entities).map(this::getId).toList()).then();
|
||||
.allEntities((java.util.Collection<Object>) (Streamable.of(entities).toList())).then();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.couchbase.CouchbaseClientFactory;
|
||||
import org.springframework.data.couchbase.core.TransactionalSupport;
|
||||
import org.springframework.data.couchbase.transaction.error.TransactionRollbackRequestedException;
|
||||
import org.springframework.data.couchbase.transaction.error.TransactionSystemAmbiguousException;
|
||||
import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.IllegalTransactionStateException;
|
||||
import org.springframework.transaction.ReactiveTransaction;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionException;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionCallback;
|
||||
|
||||
import com.couchbase.client.core.annotation.Stability;
|
||||
import com.couchbase.client.java.transactions.TransactionResult;
|
||||
import com.couchbase.client.java.transactions.config.TransactionOptions;
|
||||
import com.couchbase.client.java.transactions.error.TransactionCommitAmbiguousException;
|
||||
import com.couchbase.client.java.transactions.error.TransactionFailedException;
|
||||
|
||||
/**
|
||||
* The Couchbase transaction manager, providing support for @Transactional methods.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class CouchbaseCallbackTransactionManager implements CallbackPreferringPlatformTransactionManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CouchbaseCallbackTransactionManager.class);
|
||||
|
||||
private final CouchbaseClientFactory couchbaseClientFactory;
|
||||
private @Nullable TransactionOptions options;
|
||||
|
||||
public CouchbaseCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory) {
|
||||
this(couchbaseClientFactory, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This override is for users manually creating a CouchbaseCallbackTransactionManager, and allows the
|
||||
* TransactionOptions to be overridden.
|
||||
*/
|
||||
public CouchbaseCallbackTransactionManager(CouchbaseClientFactory couchbaseClientFactory,
|
||||
@Nullable TransactionOptions options) {
|
||||
this.couchbaseClientFactory = couchbaseClientFactory;
|
||||
this.options = options != null ? options : TransactionOptions.transactionOptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T execute(TransactionDefinition definition, TransactionCallback<T> callback) throws TransactionException {
|
||||
boolean createNewTransaction = handlePropagation(definition);
|
||||
|
||||
setOptionsFromDefinition(definition);
|
||||
|
||||
if (createNewTransaction) {
|
||||
return executeNewTransaction(callback);
|
||||
} else {
|
||||
return callback.doInTransaction(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Stability.Internal
|
||||
<T> Flux<T> executeReactive(TransactionDefinition definition,
|
||||
org.springframework.transaction.reactive.TransactionCallback<T> callback) {
|
||||
return Flux.defer(() -> {
|
||||
boolean createNewTransaction = handlePropagation(definition);
|
||||
|
||||
setOptionsFromDefinition(definition);
|
||||
|
||||
if (createNewTransaction) {
|
||||
return executeNewReactiveTransaction(callback);
|
||||
} else {
|
||||
return Mono.error(new UnsupportedOperationException("Unsupported operation"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <T> T executeNewTransaction(TransactionCallback<T> callback) {
|
||||
final AtomicReference<T> execResult = new AtomicReference<>();
|
||||
|
||||
// Each of these transactions will block one thread on the underlying SDK's transactions scheduler. This
|
||||
// scheduler is effectively unlimited, but this can still potentially lead to high thread usage by the application.
|
||||
// If this is an issue then users need to instead use the standard Couchbase reactive transactions SDK.
|
||||
try {
|
||||
TransactionResult ignored = couchbaseClientFactory.getCluster().transactions().run(ctx -> {
|
||||
CouchbaseTransactionStatus status = new CouchbaseTransactionStatus(ctx, true, false, false, true, null);
|
||||
|
||||
T res = callback.doInTransaction(status);
|
||||
if (res instanceof Mono || res instanceof Flux) {
|
||||
throw new UnsupportedOperationException(
|
||||
"Return type is Mono or Flux, indicating a reactive transaction is being performed in a blocking way. A potential cause is the CouchbaseTransactionInterceptor is not in use.");
|
||||
}
|
||||
execResult.set(res);
|
||||
|
||||
if (status.isRollbackOnly()) {
|
||||
throw new TransactionRollbackRequestedException("TransactionStatus.isRollbackOnly() is set");
|
||||
}
|
||||
}, this.options);
|
||||
|
||||
return execResult.get();
|
||||
} catch (RuntimeException ex) {
|
||||
throw convert(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static RuntimeException convert(RuntimeException ex) {
|
||||
if (ex instanceof TransactionCommitAmbiguousException) {
|
||||
return new TransactionSystemAmbiguousException((TransactionCommitAmbiguousException) ex);
|
||||
}
|
||||
if (ex instanceof TransactionFailedException) {
|
||||
return new TransactionSystemUnambiguousException((TransactionFailedException) ex);
|
||||
}
|
||||
// Should not get here
|
||||
return ex;
|
||||
}
|
||||
|
||||
private <T> Flux<T> executeNewReactiveTransaction(
|
||||
org.springframework.transaction.reactive.TransactionCallback<T> callback) {
|
||||
// Buffer the output rather than attempting to stream results back from a now-defunct lambda.
|
||||
final List<T> out = new ArrayList<>();
|
||||
|
||||
return couchbaseClientFactory.getCluster().reactive().transactions().run(ctx -> {
|
||||
return Mono.defer(() -> {
|
||||
ReactiveTransaction status = new ReactiveTransaction() {
|
||||
boolean rollbackOnly = false;
|
||||
|
||||
@Override
|
||||
public boolean isNewTransaction() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRollbackOnly() {
|
||||
this.rollbackOnly = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRollbackOnly() {
|
||||
return rollbackOnly;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompleted() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return Flux.from(callback.doInTransaction(status)).doOnNext(v -> out.add(v)).then(Mono.defer(() -> {
|
||||
if (status.isRollbackOnly()) {
|
||||
return Mono.error(new TransactionRollbackRequestedException("TransactionStatus.isRollbackOnly() is set"));
|
||||
}
|
||||
return Mono.empty();
|
||||
}));
|
||||
});
|
||||
|
||||
}, this.options).thenMany(Flux.defer(() -> Flux.fromIterable(out))).onErrorMap(ex -> {
|
||||
if (ex instanceof RuntimeException) {
|
||||
return convert((RuntimeException) ex);
|
||||
}
|
||||
return ex;
|
||||
});
|
||||
}
|
||||
|
||||
// Propagation defines what happens when a @Transactional method is called from another @Transactional method.
|
||||
private boolean handlePropagation(TransactionDefinition definition) {
|
||||
boolean isExistingTransaction = TransactionalSupport.checkForTransactionInThreadLocalStorage().block().isPresent();
|
||||
|
||||
LOGGER.trace("Deciding propagation behaviour from {} and {}", definition.getPropagationBehavior(),
|
||||
isExistingTransaction);
|
||||
|
||||
switch (definition.getPropagationBehavior()) {
|
||||
case TransactionDefinition.PROPAGATION_REQUIRED:
|
||||
// Make a new transaction if required, else just execute the new method in the current transaction.
|
||||
return !isExistingTransaction;
|
||||
|
||||
case TransactionDefinition.PROPAGATION_SUPPORTS:
|
||||
// Don't appear to have the ability to execute the callback non-transactionally in this layer.
|
||||
throw new UnsupportedOperationException(
|
||||
"Propagation level 'support' has been specified which is not supported");
|
||||
|
||||
case TransactionDefinition.PROPAGATION_MANDATORY:
|
||||
if (!isExistingTransaction) {
|
||||
throw new IllegalTransactionStateException(
|
||||
"Propagation level 'mandatory' is specified but not in an active transaction");
|
||||
}
|
||||
return false;
|
||||
|
||||
case TransactionDefinition.PROPAGATION_REQUIRES_NEW:
|
||||
// This requires suspension of the active transaction. This will be possible to support in a future
|
||||
// release, if required.
|
||||
throw new UnsupportedOperationException(
|
||||
"Propagation level 'requires_new' has been specified which is not currently supported");
|
||||
|
||||
case TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
|
||||
// Don't appear to have the ability to execute the callback non-transactionally in this layer.
|
||||
throw new UnsupportedOperationException(
|
||||
"Propagation level 'not_supported' has been specified which is not supported");
|
||||
|
||||
case TransactionDefinition.PROPAGATION_NEVER:
|
||||
if (isExistingTransaction) {
|
||||
throw new IllegalTransactionStateException(
|
||||
"Existing transaction found for transaction marked with propagation 'never'");
|
||||
}
|
||||
return true;
|
||||
|
||||
case TransactionDefinition.PROPAGATION_NESTED:
|
||||
if (isExistingTransaction) {
|
||||
// Couchbase transactions cannot be nested.
|
||||
throw new UnsupportedOperationException(
|
||||
"Propagation level 'nested' has been specified which is not supported");
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
throw new UnsupportedOperationException(
|
||||
"Unknown propagation level " + definition.getPropagationBehavior() + " has been specified");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param definition reflects the @Transactional options
|
||||
*/
|
||||
private void setOptionsFromDefinition(TransactionDefinition definition) {
|
||||
if (definition != null) {
|
||||
if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) {
|
||||
if (options == null) {
|
||||
options = TransactionOptions.transactionOptions();
|
||||
}
|
||||
options = options.timeout(Duration.ofSeconds(definition.getTimeout()));
|
||||
}
|
||||
|
||||
if (!(definition.getIsolationLevel() == TransactionDefinition.ISOLATION_DEFAULT
|
||||
|| definition.getIsolationLevel() == TransactionDefinition.ISOLATION_READ_COMMITTED)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Couchbase Transactions run at Read Committed isolation - other isolation levels are not supported");
|
||||
}
|
||||
|
||||
// readonly is ignored as it is documented as being a hint that won't necessarily cause writes to fail
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
|
||||
// All Spring transactional code (currently) does not call the getTransaction, commit or rollback methods if
|
||||
// the transaction manager is a CallbackPreferringPlatformTransactionManager.
|
||||
// So these methods should only be hit if user is using PlatformTransactionManager directly. Spring supports this,
|
||||
// but due to the lambda-based nature of our transactions, we cannot.
|
||||
throw new UnsupportedOperationException(
|
||||
"Direct programmatic use of the Couchbase PlatformTransactionManager is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commit(TransactionStatus ignored) throws TransactionException {
|
||||
throw new UnsupportedOperationException(
|
||||
"Direct programmatic use of the Couchbase PlatformTransactionManager is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback(TransactionStatus ignored) throws TransactionException {
|
||||
throw new UnsupportedOperationException(
|
||||
"Direct programmatic use of the Couchbase PlatformTransactionManager is not supported");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.support.ResourceHolderSupport;
|
||||
|
||||
import com.couchbase.client.core.annotation.Stability;
|
||||
import com.couchbase.client.core.transaction.CoreTransactionAttemptContext;
|
||||
|
||||
/**
|
||||
* Container for couchbase transaction resources to hold in threadlocal or reactive context.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*
|
||||
*/
|
||||
@Stability.Internal
|
||||
public class CouchbaseResourceHolder extends ResourceHolderSupport {
|
||||
|
||||
private @Nullable CoreTransactionAttemptContext core; // which holds the atr
|
||||
Map<Integer, Object> getResultMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Create a new {@link CouchbaseResourceHolder} for a given {@link CoreTransactionAttemptContext session}.
|
||||
*
|
||||
* @param core the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}.
|
||||
*/
|
||||
public CouchbaseResourceHolder(@Nullable CoreTransactionAttemptContext core) {
|
||||
this.core = core;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the associated {@link CoreTransactionAttemptContext}. Can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
public CoreTransactionAttemptContext getCore() {
|
||||
return core;
|
||||
}
|
||||
|
||||
public Object transactionResultHolder(Object holder, Object o) {
|
||||
getResultMap.put(System.identityHashCode(o), holder);
|
||||
return holder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction;
|
||||
|
||||
import org.springframework.transaction.support.DefaultTransactionDefinition;
|
||||
|
||||
import com.couchbase.client.core.annotation.Stability;
|
||||
|
||||
/**
|
||||
* Couchbase Transaction Definition for Spring Data transaction framework.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
@Stability.Internal
|
||||
public class CouchbaseTransactionDefinition extends DefaultTransactionDefinition {
|
||||
public CouchbaseTransactionDefinition() {
|
||||
super();
|
||||
setIsolationLevel(ISOLATION_READ_COMMITTED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import com.couchbase.client.core.annotation.Stability;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.transaction.TransactionManager;
|
||||
import org.springframework.transaction.interceptor.TransactionAttribute;
|
||||
import org.springframework.transaction.interceptor.TransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* This allows reactive @Transactional support with Couchbase transactions.
|
||||
* <p>
|
||||
* The ReactiveTransactionManager does not support the lambda-based nature of Couchbase transactions,
|
||||
* and there is no reactive equivalent of CallbackPreferringTransactionManager (which does).
|
||||
* <p>
|
||||
* The solution: override the standard TransactionInterceptor and, if the
|
||||
* CouchbaseCallbackTransactionManager is the provided TransactionManager, defer to that.
|
||||
*
|
||||
* @author Graham Pople
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
@Stability.Internal
|
||||
public class CouchbaseTransactionInterceptor extends TransactionInterceptor
|
||||
implements MethodInterceptor, Serializable {
|
||||
|
||||
public CouchbaseTransactionInterceptor(TransactionManager ptm, TransactionAttributeSource tas) {
|
||||
super(ptm, tas);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
|
||||
final InvocationCallback invocation) throws Throwable {
|
||||
final TransactionAttributeSource tas = getTransactionAttributeSource();
|
||||
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
|
||||
|
||||
if (getTransactionManager() instanceof CouchbaseCallbackTransactionManager) {
|
||||
CouchbaseCallbackTransactionManager manager = (CouchbaseCallbackTransactionManager) getTransactionManager();
|
||||
|
||||
if (Mono.class.isAssignableFrom(method.getReturnType())) {
|
||||
return manager.executeReactive(txAttr, ignored -> {
|
||||
try {
|
||||
return (Mono<?>) invocation.proceedWithInvocation();
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}).singleOrEmpty();
|
||||
} else if (Flux.class.isAssignableFrom(method.getReturnType())) {
|
||||
return manager.executeReactive(txAttr, ignored -> {
|
||||
try {
|
||||
return (Flux<?>) invocation.proceedWithInvocation();
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return manager.execute(txAttr, ignored -> {
|
||||
try {
|
||||
return invocation.proceedWithInvocation();
|
||||
} catch (RuntimeException e) {
|
||||
throw e;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return super.invokeWithinTransaction(method, targetClass, invocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2012-2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction;
|
||||
|
||||
import org.springframework.transaction.support.DefaultTransactionStatus;
|
||||
|
||||
/**
|
||||
* Couchbase transaction status for Spring Data transaction framework.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class CouchbaseTransactionStatus extends DefaultTransactionStatus {
|
||||
|
||||
/**
|
||||
* Create a new {@code DefaultTransactionStatus} instance.
|
||||
*
|
||||
* @param transaction underlying transaction object that can hold state
|
||||
* for the internal transaction implementation
|
||||
* @param newTransaction if the transaction is new, otherwise participating
|
||||
* in an existing transaction
|
||||
* @param newSynchronization if a new transaction synchronization has been
|
||||
* opened for the given transaction
|
||||
* @param readOnly whether the transaction is marked as read-only
|
||||
* @param debug should debug logging be enabled for the handling of this transaction?
|
||||
* Caching it in here can prevent repeated calls to ask the logging system whether
|
||||
* debug logging should be enabled.
|
||||
* @param suspendedResources a holder for resources that have been suspended
|
||||
*/
|
||||
public CouchbaseTransactionStatus(Object transaction, boolean newTransaction, boolean newSynchronization, boolean readOnly, boolean debug, Object suspendedResources) {
|
||||
super(transaction,
|
||||
newTransaction,
|
||||
newSynchronization,
|
||||
readOnly,
|
||||
debug,
|
||||
suspendedResources);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionException;
|
||||
import org.springframework.transaction.reactive.TransactionCallback;
|
||||
import org.springframework.transaction.reactive.TransactionalOperator;
|
||||
|
||||
/**
|
||||
* The TransactionalOperator interface is another method to perform reactive transactions with Spring.
|
||||
* <p>
|
||||
* We recommend instead using a regular reactive SDK transaction, and performing Spring operations inside it.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class CouchbaseTransactionalOperator implements TransactionalOperator {
|
||||
private final CouchbaseCallbackTransactionManager manager;
|
||||
|
||||
CouchbaseTransactionalOperator(CouchbaseCallbackTransactionManager manager) {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
public static CouchbaseTransactionalOperator create(CouchbaseCallbackTransactionManager manager) {
|
||||
return new CouchbaseTransactionalOperator(manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> transactional(Mono<T> mono) {
|
||||
return transactional(Flux.from(mono)).singleOrEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> execute(TransactionCallback<T> action) throws TransactionException {
|
||||
return Flux.defer(() -> {
|
||||
TransactionDefinition def = new CouchbaseTransactionDefinition();
|
||||
return manager.executeReactive(def, action);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction.error;
|
||||
|
||||
import com.couchbase.client.core.error.CouchbaseException;
|
||||
|
||||
/**
|
||||
* A transaction rollback has been requested.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class TransactionRollbackRequestedException extends CouchbaseException {
|
||||
public TransactionRollbackRequestedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction.error;
|
||||
|
||||
import com.couchbase.client.java.transactions.error.TransactionCommitAmbiguousException;
|
||||
|
||||
/**
|
||||
* The transaction expired at the point of trying to commit it. It is ambiguous whether the transaction has committed
|
||||
* or not. Actors may be able to see the content of this transaction.
|
||||
*
|
||||
* This error is the result of inevitable and unavoidable edge cases when working with unreliable networks. For example,
|
||||
* consider an ordinary mutation being made over the network to any database. The mutation could succeed on the
|
||||
* database-side, and then just before the result is returned to the client, the network connection drops. The client
|
||||
* cannot receive the success result and will timeout - it is ambiguous to it whether the mutation succeeded or not.
|
||||
*
|
||||
* The transactions logic will work to resolve the ambiguity up until the transaction expires, but if unable to resolve
|
||||
* it in that time, it is forced to raise this error. The transaction may or may not have been successful, and
|
||||
* error-handling of this is highly application-dependent.
|
||||
*
|
||||
* An asynchronous cleanup process will try to complete the transaction: roll it back if it didn't commit, roll it
|
||||
* forwards if it did.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class TransactionSystemAmbiguousException extends TransactionSystemCouchbaseException {
|
||||
public TransactionSystemAmbiguousException(TransactionCommitAmbiguousException ex) {
|
||||
super(ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction.error;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.transaction.TransactionSystemException;
|
||||
|
||||
import com.couchbase.client.core.cnc.events.transaction.TransactionLogEvent;
|
||||
import com.couchbase.client.java.transactions.error.TransactionFailedException;
|
||||
|
||||
/**
|
||||
* A base class of transaction-level exceptions raised by Couchbase, allowing them to be handled in one place.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
abstract public class TransactionSystemCouchbaseException extends TransactionSystemException {
|
||||
private final TransactionFailedException internal;
|
||||
|
||||
public TransactionSystemCouchbaseException(TransactionFailedException ex) {
|
||||
super(ex.getMessage(), ex.getCause());
|
||||
this.internal = ex;
|
||||
}
|
||||
|
||||
/**
|
||||
* An in-memory log is built up during each transaction. The application may want to write this to their own logs, for
|
||||
* example upon transaction failure.
|
||||
*/
|
||||
public List<TransactionLogEvent> logs() {
|
||||
return internal.logs();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction.error;
|
||||
|
||||
import com.couchbase.client.java.transactions.error.TransactionFailedException;
|
||||
|
||||
/**
|
||||
* The transaction failed and unambiguously did not commit. No actors can see any part of this failed transaction.
|
||||
* <p>
|
||||
* The application does not need to do anything to rollback the transaction.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class TransactionSystemUnambiguousException extends TransactionSystemCouchbaseException {
|
||||
public TransactionSystemUnambiguousException(TransactionFailedException ex) {
|
||||
super(ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.couchbase.transaction.error;
|
||||
|
||||
import org.springframework.dao.UncategorizedDataAccessException;
|
||||
|
||||
import com.couchbase.client.core.error.transaction.TransactionOperationFailedException;
|
||||
import com.couchbase.client.core.error.transaction.internal.WrappedTransactionOperationFailedException;
|
||||
|
||||
/**
|
||||
* An opaque signal that something went wrong during the execution of an operation inside a transaction.
|
||||
* <p>
|
||||
* The application is not expected to catch or inspect this exception, and should allow it to propagate.
|
||||
* <p>
|
||||
* Internal state has been set that ensures that the transaction will act appropriately (including rolling back and
|
||||
* retrying if necessary) regardless of what the application does with this exception.
|
||||
*
|
||||
* @author Graham Pople
|
||||
*/
|
||||
public class UncategorizedTransactionDataAccessException extends UncategorizedDataAccessException
|
||||
implements WrappedTransactionOperationFailedException {
|
||||
private final TransactionOperationFailedException internal;
|
||||
|
||||
public UncategorizedTransactionDataAccessException(TransactionOperationFailedException err) {
|
||||
super(err.getMessage(), err.getCause());
|
||||
this.internal = err;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionOperationFailedException wrapped() {
|
||||
return internal;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user