diff --git a/src/main/asciidoc/entity.adoc b/src/main/asciidoc/entity.adoc index b713edfb0..ed9c3dceb 100644 --- a/src/main/asciidoc/entity.adoc +++ b/src/main/asciidoc/entity.adoc @@ -77,6 +77,9 @@ The `@Id` annotation needs to be present because every document in Couchbase nee This key needs to be any string with a length of maximum 250 characters. Feel free to use whatever fits your use case, be it a UUID, an email address or anything else. +Writes to Couchbase-Server buckets can optionally be assigned durability requirements; which instruct Couchbase Server to update the specified document on multiple nodes in memory and/or disk locations across the cluster; before considering the write to be committed. +Default durability requirements can also be configured through the `@Document` annotation. +For example: `@Document(durabilityLevel = DurabilityLevel.MAJORITY)` will force mutations to be replicated to a majority of the Data Service nodes. [[datatypes]] == Datatypes and Converters diff --git a/src/main/asciidoc/template.adoc b/src/main/asciidoc/template.adoc index 66e6fe4e9..f4d0705e6 100644 --- a/src/main/asciidoc/template.adoc +++ b/src/main/asciidoc/template.adoc @@ -30,7 +30,7 @@ User found = couchbaseTemplate.findById(User.class).one(user.getId()); ---- ==== -If you wanted to use a custom durability requirement for the `upsert` operation you can chain it in: +If you wanted to use a custom (by default durability options from the `@Document` annotation will be used) durability requirement for the `upsert` operation you can chain it in: .Upsert with durability ==== diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java index d93a94b80..f1c78a26f 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableInsertByIdOperationSupport.java @@ -39,7 +39,8 @@ public ExecutableInsertByIdOperationSupport(final CouchbaseTemplate template) { public ExecutableInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java index 718fdf181..fa1d0afe5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableRemoveByIdOperationSupport.java @@ -31,6 +31,7 @@ * {@link ExecutableRemoveByIdOperation} implementations for Couchbase. * * @author Michael Reiche + * @author Tigran Babloyan */ public class ExecutableRemoveByIdOperationSupport implements ExecutableRemoveByIdOperation { @@ -50,7 +51,8 @@ public ExecutableRemoveById removeById() { public ExecutableRemoveById removeById(Class domainType) { return new ExecutableRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java index 878bffa6b..5151a039d 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableReplaceByIdOperationSupport.java @@ -39,7 +39,8 @@ public ExecutableReplaceByIdOperationSupport(final CouchbaseTemplate template) { public ExecutableReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java index 3315fefd3..bd5a71b36 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ExecutableUpsertByIdOperationSupport.java @@ -39,7 +39,8 @@ public ExecutableUpsertByIdOperationSupport(final CouchbaseTemplate template) { public ExecutableUpsertById upsertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ExecutableUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java index b7a325759..2c71a0910 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveInsertByIdOperationSupport.java @@ -45,6 +45,7 @@ * {@link ReactiveInsertByIdOperation} implementations for Couchbase. * * @author Michael Reiche + * @author Tigran Babloyan */ public class ReactiveInsertByIdOperationSupport implements ReactiveInsertByIdOperation { @@ -59,7 +60,8 @@ public ReactiveInsertByIdOperationSupport(final ReactiveCouchbaseTemplate templa public ReactiveInsertById insertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveInsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null, template.support()); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java index 7a32537ca..2e7427f27 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveRemoveByIdOperationSupport.java @@ -46,6 +46,7 @@ * {@link ReactiveRemoveByIdOperation} implementations for Couchbase. * * @author Michael Reiche + * @author Tigran Babloyan */ public class ReactiveRemoveByIdOperationSupport implements ReactiveRemoveByIdOperation { @@ -65,7 +66,8 @@ public ReactiveRemoveById removeById() { @Override public ReactiveRemoveById removeById(Class domainType) { return new ReactiveRemoveByIdSupport(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java index b6ab3a383..3c2ae98c3 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveReplaceByIdOperationSupport.java @@ -48,6 +48,7 @@ * {@link ReactiveReplaceByIdOperation} implementations for Couchbase. * * @author Michael Reiche + * @author Tigran Babloyan */ public class ReactiveReplaceByIdOperationSupport implements ReactiveReplaceByIdOperation { @@ -62,7 +63,8 @@ public ReactiveReplaceByIdOperationSupport(final ReactiveCouchbaseTemplate templ public ReactiveReplaceById replaceById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveReplaceByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null, template.support()); } diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java index a8e116067..01ec65eb4 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveUpsertByIdOperationSupport.java @@ -37,6 +37,7 @@ * {@link ReactiveUpsertByIdOperation} implementations for Couchbase. * * @author Michael Reiche + * @author Tigran Babloyan */ public class ReactiveUpsertByIdOperationSupport implements ReactiveUpsertByIdOperation { @@ -51,7 +52,8 @@ public ReactiveUpsertByIdOperationSupport(final ReactiveCouchbaseTemplate templa public ReactiveUpsertById upsertById(final Class domainType) { Assert.notNull(domainType, "DomainType must not be null!"); return new ReactiveUpsertByIdSupport<>(template, domainType, OptionsBuilder.getScopeFrom(domainType), - OptionsBuilder.getCollectionFrom(domainType), null, PersistTo.NONE, ReplicateTo.NONE, DurabilityLevel.NONE, + OptionsBuilder.getCollectionFrom(domainType), null, OptionsBuilder.getPersistTo(domainType), + OptionsBuilder.getReplicateTo(domainType), OptionsBuilder.getDurabilityLevel(domainType), null, template.support()); } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java index 2207adbed..a2caeec55 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/Document.java @@ -23,6 +23,9 @@ import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.Persistent; import org.springframework.data.couchbase.repository.Collection; @@ -36,6 +39,7 @@ * * @author Michael Nitschinger * @author Andrey Rubtsov + * @author Tigran Babloyan */ @Persistent @Inherited @@ -84,4 +88,22 @@ */ @AliasFor(annotation = ScanConsistency.class, attribute = "query") QueryScanConsistency queryScanConsistency() default QueryScanConsistency.NOT_BOUNDED; + + /** + * How many persisted copies of the modified record must exist on the given document. Default is {@link PersistTo#NONE}. + * For Couchbase version >= 6.5 see {@link #durabilityLevel()}. + */ + PersistTo persistTo() default PersistTo.NONE; + + /** + * How many replicas must this documents operations be propagated to. Default is {@link ReplicateTo#NONE}. + * For Couchbase version >= 6.5 see {@link #durabilityLevel()}. + */ + ReplicateTo replicateTo() default ReplicateTo.NONE; + + /** + * The optional durabilityLevel for all mutating operations, allows the application to wait until this replication + * (or persistence) is successful before proceeding + */ + DurabilityLevel durabilityLevel() default DurabilityLevel.NONE; } diff --git a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java index 754dbb2d3..308a3f5d5 100644 --- a/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java +++ b/src/main/java/org/springframework/data/couchbase/core/query/OptionsBuilder.java @@ -31,6 +31,8 @@ import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.couchbase.core.mapping.CouchbaseDocument; +import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.Expiry; import org.springframework.data.couchbase.repository.Collection; import org.springframework.data.couchbase.repository.ScanConsistency; import org.springframework.data.couchbase.repository.Scope; @@ -56,6 +58,7 @@ * Methods for building Options objects for Couchbae APIs. * * @author Michael Reiche + * @author Tigran Babloyan */ public class OptionsBuilder { @@ -225,6 +228,30 @@ public static String getScopeFrom(Class domainType) { } return null; } + + public static DurabilityLevel getDurabilityLevel(Class domainType) { + if (domainType == null) { + return DurabilityLevel.NONE; + } + Document document = AnnotatedElementUtils.findMergedAnnotation(domainType, Document.class); + return document != null ? document.durabilityLevel() : DurabilityLevel.NONE; + } + + public static PersistTo getPersistTo(Class domainType) { + if (domainType == null) { + return PersistTo.NONE; + } + Document document = AnnotatedElementUtils.findMergedAnnotation(domainType, Document.class); + return document != null ? document.persistTo() : PersistTo.NONE; + } + + public static ReplicateTo getReplicateTo(Class domainType) { + if (domainType == null) { + return ReplicateTo.NONE; + } + Document document = AnnotatedElementUtils.findMergedAnnotation(domainType, Document.class); + return document != null ? document.replicateTo() : ReplicateTo.NONE; + } /** * collection annotation diff --git a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java index 59eb29017..0c2a57333 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -43,21 +43,8 @@ import org.springframework.data.couchbase.core.ExecutableFindByIdOperation.ExecutableFindById; import org.springframework.data.couchbase.core.ExecutableRemoveByIdOperation.ExecutableRemoveById; import org.springframework.data.couchbase.core.ExecutableReplaceByIdOperation.ExecutableReplaceById; -import org.springframework.data.couchbase.core.support.OneAndAllEntity; -import org.springframework.data.couchbase.core.support.OneAndAllId; -import org.springframework.data.couchbase.core.support.WithDurability; -import org.springframework.data.couchbase.core.support.WithExpiry; -import org.springframework.data.couchbase.domain.Address; -import org.springframework.data.couchbase.domain.Config; -import org.springframework.data.couchbase.domain.NaiveAuditorAware; -import org.springframework.data.couchbase.domain.PersonValue; -import org.springframework.data.couchbase.domain.Submission; -import org.springframework.data.couchbase.domain.User; -import org.springframework.data.couchbase.domain.UserAnnotated; -import org.springframework.data.couchbase.domain.UserAnnotated2; -import org.springframework.data.couchbase.domain.UserAnnotated3; -import org.springframework.data.couchbase.domain.UserAnnotatedTouchOnRead; -import org.springframework.data.couchbase.domain.UserSubmission; +import org.springframework.data.couchbase.core.support.*; +import org.springframework.data.couchbase.domain.*; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; @@ -74,6 +61,7 @@ * * @author Michael Nitschinger * @author Michael Reiche + * @author Tigran Babloyan */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(Config.class) @@ -275,49 +263,58 @@ void findProjectingPath() { @Test void withDurability() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - Class clazz = User.class; // for now, just User.class. There is no Durability annotation. - // insert, replace, upsert - for (OneAndAllEntity operator : new OneAndAllEntity[] { couchbaseTemplate.insertById(clazz), - couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz) }) { - // create an entity of type clazz - Constructor cons = clazz.getConstructor(String.class, String.class, String.class); - User user = (User) cons.newInstance("" + operator.getClass().getSimpleName() + "_" + clazz.getSimpleName(), - "firstname", "lastname"); - - if (clazz.equals(User.class)) { // User.java doesn't have an durability annotation - operator = (OneAndAllEntity) ((WithDurability) operator).withDurability(PersistTo.ACTIVE, - ReplicateTo.NONE); - } + for (Class clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) { + // insert, replace, upsert + for (OneAndAllEntity operator : new OneAndAllEntity[]{couchbaseTemplate.insertById(clazz), + couchbaseTemplate.replaceById(clazz), couchbaseTemplate.upsertById(clazz)}) { + // create an entity of type clazz + Constructor cons = clazz.getConstructor(String.class, String.class, String.class); + User user = (User) cons.newInstance("" + operator.getClass().getSimpleName() + "_" + clazz.getSimpleName(), + "firstname", "lastname"); - // if replace, we need to insert a document to replace - if (operator instanceof ExecutableReplaceById) { - couchbaseTemplate.insertById(User.class).one(user); - } - // call to insert/replace/update - User returned = null; - - // occasionally gives "reactor.core.Exceptions$OverflowException: Could not emit value due to lack of requests" - for (int i = 1; i != 5; i++) { - try { - returned = (User) operator.one(user); - break; - } catch (Exception ofe) { - System.out.println("" + i + " caught: " + ofe); - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); - if (i == 4) { - throw ofe; + if (clazz.equals(User.class)) { // User.java doesn't have an durability annotation + operator = (OneAndAllEntity) ((WithDurability) operator).withDurability(PersistTo.ACTIVE, + ReplicateTo.NONE); + } else if (clazz.equals(UserAnnotatedReplicateTo.class)){ // override the replica count from the annotation with no replica + operator = (OneAndAllEntity) ((WithDurability) operator).withDurability(PersistTo.NONE, + ReplicateTo.NONE); + } + + // if replace, we need to insert a document to replace + if (operator instanceof ExecutableReplaceById) { + couchbaseTemplate.insertById(User.class).one(user); + } + // call to insert/replace/update + User returned = null; + + // occasionally gives "reactor.core.Exceptions$OverflowException: Could not emit value due to lack of requests" + for (int i = 1; i != 5; i++) { + try { + returned = (User) operator.one(user); + break; + } catch (Exception ofe) { + System.out.println("" + i + " caught: " + ofe); + couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + if (i == 4) { + throw ofe; + } + sleepSecs(1); } - sleepSecs(1); } - } - assertEquals(user, returned); - User found = couchbaseTemplate.findById(User.class).one(user.getId()); - assertEquals(user, found); - - if (operator instanceof ExecutableReplaceById) { - couchbaseTemplate.removeById().withDurability(PersistTo.ACTIVE, ReplicateTo.NONE).one(user.getId()); - User removed = (User) couchbaseTemplate.findById(user.getClass()).one(user.getId()); - assertNull(removed, "found should have been null as document should be removed"); + assertEquals(user, returned); + User found = couchbaseTemplate.findById(User.class).one(user.getId()); + assertEquals(user, found); + + if (operator instanceof ExecutableReplaceById) { + if (clazz.equals(UserAnnotatedReplicateTo.class)){ // override the replica count from the annotation with no replica + couchbaseTemplate.removeById(clazz).withDurability(PersistTo.ACTIVE, + ReplicateTo.NONE).one(user.getId()); + } else { + couchbaseTemplate.removeById(clazz).one(user.getId()); + } + User removed = (User) couchbaseTemplate.findById(user.getClass()).one(user.getId()); + assertNull(removed, "found should have been null as document should be removed"); + } } } @@ -474,6 +471,56 @@ void insertByIdwithDurability() { couchbaseTemplate.removeById(User.class).one(user.getId()); } + @Test + void insertByIdWithAnnotatedDurability() { + UserAnnotatedPersistTo user = new UserAnnotatedPersistTo(UUID.randomUUID().toString(), "firstname", "lastname"); + UserAnnotatedPersistTo inserted = null; + + // occasionally gives "reactor.core.Exceptions$OverflowException: Could not emit value due to lack of requests" + for (int i = 1; i != 5; i++) { + try { + inserted = couchbaseTemplate.insertById(UserAnnotatedPersistTo.class) + .one(user); + break; + } catch (Exception ofe) { + System.out.println("" + i + " caught: " + ofe); + couchbaseTemplate.removeByQuery(UserAnnotatedPersistTo.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + if (i == 4) { + throw ofe; + } + sleepSecs(1); + } + } + assertEquals(user, inserted); + assertThrows(DuplicateKeyException.class, () -> couchbaseTemplate.insertById(UserAnnotatedPersistTo.class).one(user)); + couchbaseTemplate.removeById(UserAnnotatedPersistTo.class).one(user.getId()); + } + + @Test + void insertByIdWithAnnotatedDurability2() { + UserAnnotatedDurability user = new UserAnnotatedDurability(UUID.randomUUID().toString(), "firstname", "lastname"); + UserAnnotatedDurability inserted = null; + + // occasionally gives "reactor.core.Exceptions$OverflowException: Could not emit value due to lack of requests" + for (int i = 1; i != 5; i++) { + try { + inserted = couchbaseTemplate.insertById(UserAnnotatedDurability.class) + .one(user); + break; + } catch (Exception ofe) { + System.out.println("" + i + " caught: " + ofe); + couchbaseTemplate.removeByQuery(UserAnnotatedDurability.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + if (i == 4) { + throw ofe; + } + sleepSecs(1); + } + } + assertEquals(user, inserted); + assertThrows(DuplicateKeyException.class, () -> couchbaseTemplate.insertById(UserAnnotatedDurability.class).one(user)); + couchbaseTemplate.removeById(UserAnnotatedDurability.class).one(user.getId()); + } + @Test void existsById() { String id = UUID.randomUUID().toString(); diff --git a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java index 8f6bf9eae..06ee0178c 100644 --- a/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/ReactiveCouchbaseTemplateKeyValueIntegrationTests.java @@ -48,13 +48,7 @@ import org.springframework.data.couchbase.core.support.OneAndAllIdReactive; import org.springframework.data.couchbase.core.support.WithDurability; import org.springframework.data.couchbase.core.support.WithExpiry; -import org.springframework.data.couchbase.domain.Config; -import org.springframework.data.couchbase.domain.PersonValue; -import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware; -import org.springframework.data.couchbase.domain.User; -import org.springframework.data.couchbase.domain.UserAnnotated; -import org.springframework.data.couchbase.domain.UserAnnotated2; -import org.springframework.data.couchbase.domain.UserAnnotated3; +import org.springframework.data.couchbase.domain.*; import org.springframework.data.couchbase.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; import org.springframework.data.couchbase.util.JavaIntegrationTests; @@ -68,6 +62,7 @@ * * @author Michael Nitschinger * @author Michael Reiche + * @author Tigran Babloyan */ @IgnoreWhen(clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(Config.class) @@ -158,36 +153,44 @@ void upsertAndFindById() { @Test void withDurability() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { - Class clazz = User.class; // for now, just User.class. There is no Durability annotation. - // insert, replace, upsert - for (OneAndAllEntityReactive operator : new OneAndAllEntityReactive[] { - reactiveCouchbaseTemplate.insertById(clazz), reactiveCouchbaseTemplate.replaceById(clazz), - reactiveCouchbaseTemplate.upsertById(clazz) }) { - // create an entity of type clazz - Constructor cons = clazz.getConstructor(String.class, String.class, String.class); - User user = (User) cons.newInstance("" + operator.getClass().getSimpleName() + "_" + clazz.getSimpleName(), - "firstname", "lastname"); - - if (clazz.equals(User.class)) { // User.java doesn't have an durability annotation - operator = (OneAndAllEntityReactive) ((WithDurability) operator).withDurability(PersistTo.ACTIVE, - ReplicateTo.NONE); - } + for (Class clazz : new Class[] { User.class, UserAnnotatedDurability.class, UserAnnotatedPersistTo.class, UserAnnotatedReplicateTo.class }) { + // insert, replace, upsert + for (OneAndAllEntityReactive operator : new OneAndAllEntityReactive[]{ + reactiveCouchbaseTemplate.insertById(clazz), reactiveCouchbaseTemplate.replaceById(clazz), + reactiveCouchbaseTemplate.upsertById(clazz)}) { + // create an entity of type clazz + Constructor cons = clazz.getConstructor(String.class, String.class, String.class); + User user = (User) cons.newInstance("" + operator.getClass().getSimpleName() + "_" + clazz.getSimpleName(), + "firstname", "lastname"); - // if replace, we need to insert a document to replace - if (operator instanceof ReactiveReplaceById) { - reactiveCouchbaseTemplate.insertById(User.class).one(user).block(); - } - // call to insert/replace/update - User returned = operator.one(user).block(); - assertEquals(user, returned); - User found = reactiveCouchbaseTemplate.findById(User.class).one(user.getId()).block(); - assertEquals(user, found); - - if (operator instanceof ReactiveReplaceByIdOperation.ReactiveReplaceById) { - reactiveCouchbaseTemplate.removeById().withDurability(PersistTo.ACTIVE, ReplicateTo.NONE).one(user.getId()) - .block(); - User removed = (User) reactiveCouchbaseTemplate.findById(user.getClass()).one(user.getId()).block(); - assertNull(removed, "found should have been null as document should be removed"); + if (clazz.equals(User.class)) { // User.java doesn't have an durability annotation + operator = (OneAndAllEntityReactive) ((WithDurability) operator).withDurability(PersistTo.ACTIVE, + ReplicateTo.NONE); + } else if (clazz.equals(UserAnnotatedReplicateTo.class)){ // override the replica count from the annotation with no replica + operator = (OneAndAllEntityReactive) ((WithDurability) operator).withDurability(PersistTo.NONE, + ReplicateTo.NONE); + } + + // if replace, we need to insert a document to replace + if (operator instanceof ReactiveReplaceById) { + reactiveCouchbaseTemplate.insertById(User.class).one(user).block(); + } + // call to insert/replace/update + User returned = operator.one(user).block(); + assertEquals(user, returned); + User found = reactiveCouchbaseTemplate.findById(User.class).one(user.getId()).block(); + assertEquals(user, found); + + if (operator instanceof ReactiveReplaceByIdOperation.ReactiveReplaceById) { + if (clazz.equals(UserAnnotatedReplicateTo.class)){ // override the replica count from the annotation with no replica + reactiveCouchbaseTemplate.removeById(clazz).withDurability(PersistTo.NONE, + ReplicateTo.NONE).one(user.getId()).block(); + } else { + reactiveCouchbaseTemplate.removeById(clazz).one(user.getId()).block(); + } + User removed = reactiveCouchbaseTemplate.findById(user.getClass()).one(user.getId()).block(); + assertNull(removed, "found should have been null as document should be removed"); + } } } @@ -330,6 +333,24 @@ void insertByIdwithDurability() { assertThrows(DuplicateKeyException.class, () -> reactiveCouchbaseTemplate.insertById(User.class).one(user).block()); } + @Test + void insertByIdWithAnnotatedDurability() { + UserAnnotatedPersistTo user = new UserAnnotatedPersistTo(UUID.randomUUID().toString(), "firstname", "lastname"); + UserAnnotatedPersistTo inserted = reactiveCouchbaseTemplate.insertById(UserAnnotatedPersistTo.class) + .one(user).block(); + assertEquals(user, inserted); + assertThrows(DuplicateKeyException.class, () -> reactiveCouchbaseTemplate.insertById(User.class).one(user).block()); + } + + @Test + void insertByIdWithAnnotatedDurability2() { + UserAnnotatedDurability user = new UserAnnotatedDurability(UUID.randomUUID().toString(), "firstname", "lastname"); + UserAnnotatedDurability inserted = reactiveCouchbaseTemplate.insertById(UserAnnotatedDurability.class) + .one(user).block(); + assertEquals(user, inserted); + assertThrows(DuplicateKeyException.class, () -> reactiveCouchbaseTemplate.insertById(User.class).one(user).block()); + } + @Test void existsById() { String id = UUID.randomUUID().toString(); diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability.java new file mode 100644 index 000000000..a770dee26 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2023 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.domain; + +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.util.UUID; + +/** + * Person entity for tests. + * + * @author Tigran Babloyan + */ +@Document(persistTo = PersistTo.ONE, replicateTo = ReplicateTo.ONE) +public class PersonWithDurability extends Person { + public PersonWithDurability() { + setId(UUID.randomUUID()); + setMiddlename("Nick"); + } + + public PersonWithDurability(String firstname, String lastname) { + this(); + setFirstname(firstname); + setLastname(lastname); + isNew(true); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability2.java b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability2.java new file mode 100644 index 000000000..a0877486f --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/PersonWithDurability2.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2023 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.domain; + +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.util.UUID; + +/** + * Person entity for tests. + * + * @author Tigran Babloyan + */ +@Document(durabilityLevel = DurabilityLevel.MAJORITY) +public class PersonWithDurability2 extends Person { + public PersonWithDurability2() { + setId(UUID.randomUUID()); + setMiddlename("Nick"); + } + + public PersonWithDurability2(String firstname, String lastname) { + this(); + setFirstname(firstname); + setLastname(lastname); + isNew(true); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurability.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurability.java new file mode 100644 index 000000000..ccdd6df75 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedDurability.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020-2023 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.domain; + +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.io.Serializable; + +/** + * Annotated User entity for tests + * + * @author Tigran Babloyan + */ + +@Document(durabilityLevel = DurabilityLevel.MAJORITY) +public class UserAnnotatedDurability extends User implements Serializable { + + public UserAnnotatedDurability(String id, String firstname, String lastname) { + super(id, firstname, lastname); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedPersistTo.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedPersistTo.java new file mode 100644 index 000000000..96fd16b7f --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedPersistTo.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-2023 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.domain; + +import com.couchbase.client.java.kv.PersistTo; +import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.io.Serializable; + +/** + * Annotated User entity for tests + * + * @author Tigran Babloyan + */ + +@Document(persistTo = PersistTo.ACTIVE) +public class UserAnnotatedPersistTo extends User implements Serializable { + + public UserAnnotatedPersistTo(String id, String firstname, String lastname) { + super(id, firstname, lastname); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedReplicateTo.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedReplicateTo.java new file mode 100644 index 000000000..db6fee247 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedReplicateTo.java @@ -0,0 +1,37 @@ +/* + * Copyright 2020-2023 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.domain; + +import com.couchbase.client.core.msg.kv.DurabilityLevel; +import com.couchbase.client.java.kv.ReplicateTo; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.io.Serializable; + +/** + * Annotated User entity for tests + * + * @author Tigran Babloyan + */ + +@Document(replicateTo = ReplicateTo.ONE) +public class UserAnnotatedReplicateTo extends User implements Serializable { + + public UserAnnotatedReplicateTo(String id, String firstname, String lastname) { + super(id, firstname, lastname); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java index 0e8685c1f..d8c82e52f 100644 --- a/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/transactions/CouchbaseTransactionalUnsettableParametersIntegrationTests.java @@ -31,6 +31,8 @@ import org.springframework.data.couchbase.CouchbaseClientFactory; import org.springframework.data.couchbase.core.CouchbaseOperations; import org.springframework.data.couchbase.domain.Person; +import org.springframework.data.couchbase.domain.PersonWithDurability; +import org.springframework.data.couchbase.domain.PersonWithDurability2; import org.springframework.data.couchbase.transaction.error.TransactionSystemUnambiguousException; import org.springframework.data.couchbase.util.Capabilities; import org.springframework.data.couchbase.util.ClusterType; @@ -54,6 +56,7 @@ * * @author Graham Pople * @author Michael Reiche + * @author Tigran Babloyan */ @IgnoreWhen(missesCapabilities = Capabilities.QUERY, clusterTypes = ClusterType.MOCKED) @SpringJUnitConfig(classes = { TransactionsConfig.class, @@ -96,6 +99,14 @@ public void insertWithDurability() { }); } + @DisplayName("Using insertById() with Durability set via annotations in a transaction is rejected at runtime") + @Test + public void insertWithDurabilityAnnotated() { + test((ops) -> { + ops.insertById(PersonWithDurability.class).one(new PersonWithDurability("Walter", "White")); + }); + } + @DisplayName("Using insertById().withExpiry in a transaction is rejected at runtime") @Test public void insertWithExpiry() { @@ -112,6 +123,14 @@ public void insertWithDurability2() { }); } + @DisplayName("Using insertById with Durability set via annotations in a transaction is rejected at runtime") + @Test + public void insertWithDurabilityAnnotated2() { + test((ops) -> { + ops.insertById(PersonWithDurability2.class).one(new PersonWithDurability2("Walter", "White")); + }); + } + @DisplayName("Using insertById().withOptions in a transaction is rejected at runtime") @Test public void insertWithOptions() { @@ -128,6 +147,14 @@ public void replaceWithDurability() { }); } + @DisplayName("Using replaceById() with Durability set via annotations in a transaction is rejected at runtime") + @Test + public void replaceWithAnnotatedDurability() { + test((ops) -> { + ops.replaceById(PersonWithDurability.class).one(new PersonWithDurability("Walter", "White")); + }); + } + @DisplayName("Using replaceById().withExpiry in a transaction is rejected at runtime") @Test public void replaceWithExpiry() { @@ -144,6 +171,14 @@ public void replaceWithDurability2() { }); } + @DisplayName("Using replaceById() with Durability set via annotations in a transaction is rejected at runtime") + @Test + public void replaceWithAnnotatedDurability2() { + test((ops) -> { + ops.replaceById(PersonWithDurability2.class).one(new PersonWithDurability2("Walter", "White")); + }); + } + @DisplayName("Using replaceById().withOptions in a transaction is rejected at runtime") @Test public void replaceWithOptions() { @@ -160,6 +195,14 @@ public void removeWithDurability() { }); } + @DisplayName("Using removeById() with Durability set via annotations in a transaction is rejected at runtime") + @Test + public void removeWithAnnotatedDurability() { + test((ops) -> { + ops.removeById(PersonWithDurability.class).oneEntity(new PersonWithDurability("Walter", "White")); + }); + } + @DisplayName("Using removeById().withDurability(durabilityLevel) in a transaction is rejected at runtime") @Test public void removeWithDurability2() { @@ -168,6 +211,14 @@ public void removeWithDurability2() { }); } + @DisplayName("Using removeById().withDurability(durabilityLevel) in a transaction is rejected at runtime") + @Test + public void removeWithAnnotatedDurability2() { + test((ops) -> { + ops.removeById(PersonWithDurability2.class).oneEntity(new PersonWithDurability2("Walter", "White")); + }); + } + @DisplayName("Using removeById().withOptions in a transaction is rejected at runtime") @Test public void removeWithOptions() {