diff --git a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java index beae803d..ddb7358a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java +++ b/src/main/java/org/springframework/data/couchbase/core/ReactiveFindByIdOperationSupport.java @@ -182,13 +182,12 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati CommonOptions getOptions; final CouchbasePersistentEntity entity = template.getConverter().getMappingContext() .getRequiredPersistentEntity(domainType); - Duration entityExpiryAnnotation = entity.getExpiryDuration(); - if (expiry != null || entityExpiryAnnotation == null || !entityExpiryAnnotation.isZero() - || options instanceof GetAndTouchOptions) { + Boolean isTouchOnRead = entity.isTouchOnRead(); + if (expiry != null || isTouchOnRead || options instanceof GetAndTouchOptions) { if (expiry != null) { expiryToUse = expiry; - } else if (entityExpiryAnnotation == null || !entityExpiryAnnotation.isZero()) { - expiryToUse = entityExpiryAnnotation; + } else if (isTouchOnRead) { + expiryToUse = entity.getExpiryDuration(); } else { expiryToUse = Duration.ZERO; } @@ -209,7 +208,7 @@ public class ReactiveFindByIdOperationSupport implements ReactiveFindByIdOperati } return getOptions; } - + } } 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 0741dce6..59eb2901 100644 --- a/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/CouchbaseTemplateKeyValueIntegrationTests.java @@ -56,6 +56,7 @@ 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.util.ClusterType; import org.springframework.data.couchbase.util.IgnoreWhen; @@ -136,24 +137,60 @@ class CouchbaseTemplateKeyValueIntegrationTests extends JavaIntegrationTests { assertEquals(user2, foundUser2); // now set user1 expiration back to 1 second with getAndTouch using the @Document(expiry=1) annotation + // This will have no effect as UserAnnotated does not have touchOnGet foundUser1 = couchbaseTemplate.findById(UserAnnotated.class).one(user1.getId()); user1.setVersion(foundUser1.getVersion());// version will have changed assertEquals(user1, foundUser1); // user1 should be gone, user2 should still be there int tries = 0; - Collection foundUsers; + Collection foundUsers; do { - sleepSecs(1); - foundUsers = (Collection) couchbaseTemplate.findById(User.class) + sleepSecs(3); + foundUsers = (Collection) couchbaseTemplate.findById(UserAnnotated.class) + .all(Arrays.asList(user1.getId(), user2.getId())); + } while (tries++ < 7 && foundUsers.size() != 2 && !user2.equals(foundUsers.iterator().next())); + assertEquals(2, foundUsers.size(), "should have found exactly 2 users"); + } finally { + couchbaseTemplate.removeByQuery(UserAnnotated.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + } + } + + @Test + void findByIdWithExpiryAnnotationTouchOnRead() { + try { + UserAnnotatedTouchOnRead user1 = new UserAnnotatedTouchOnRead(UUID.randomUUID().toString(), "user1", "user1"); + UserAnnotatedTouchOnRead user2 = new UserAnnotatedTouchOnRead(UUID.randomUUID().toString(), "user2", "user2"); + + Collection upserts = (Collection) couchbaseTemplate.upsertById(UserAnnotatedTouchOnRead.class) + .all(Arrays.asList(user1, user2)); + + // explicitly set expiry to 10 seconds + UserAnnotatedTouchOnRead foundUser1 = couchbaseTemplate.findById(UserAnnotatedTouchOnRead.class).withExpiry(Duration.ofSeconds(10)).one(user1.getId()); + user1.setVersion(foundUser1.getVersion());// version will have changed + assertEquals(user1, foundUser1); + UserAnnotatedTouchOnRead foundUser2 = couchbaseTemplate.findById(UserAnnotatedTouchOnRead.class).withExpiry(Duration.ofSeconds(10)).one(user2.getId()); + user2.setVersion(foundUser2.getVersion());// version will have changed + assertEquals(user2, foundUser2); + + // now set user1 expiration back to 1 second with getAndTouch using the @Document(expiry=1) annotation + foundUser1 = couchbaseTemplate.findById(UserAnnotatedTouchOnRead.class).one(user1.getId()); + user1.setVersion(foundUser1.getVersion());// version will have changed + assertEquals(user1, foundUser1); + + // user1 should be gone, user2 should still be there + int tries = 0; + Collection foundUsers; + do { + sleepSecs(3); + foundUsers = (Collection) couchbaseTemplate.findById(UserAnnotatedTouchOnRead.class) .all(Arrays.asList(user1.getId(), user2.getId())); } while (tries++ < 7 && foundUsers.size() != 1 && !user2.equals(foundUsers.iterator().next())); - assertEquals(1, foundUsers.size(), "should have found exactly 1 user"); - assertEquals(user2, foundUsers.iterator().next()); + assertEquals(1, foundUsers.size(), "should have found exactly 1 user1"); + assertEquals(user2.getId(), foundUsers.iterator().next().getId()); } finally { - couchbaseTemplate.removeByQuery(User.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); + couchbaseTemplate.removeByQuery(UserAnnotatedTouchOnRead.class).withConsistency(QueryScanConsistency.REQUEST_PLUS).all(); } - } @Test void upsertAndFindById() { diff --git a/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedTouchOnRead.java b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedTouchOnRead.java new file mode 100644 index 00000000..0155f62e --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/UserAnnotatedTouchOnRead.java @@ -0,0 +1,64 @@ +/* + * 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.json.JsonObject; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.data.couchbase.core.mapping.Document; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.Serializable; + +/** + * Annoted User entity for tests + * + * @author Michael Reiche + */ + +@Document(expiry = 1, touchOnRead = true) +public class UserAnnotatedTouchOnRead extends User implements Serializable { + + JsonNode custom = new ObjectNode(JsonNodeFactory.instance); + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + if(custom== null){ + out.writeBoolean(false); + } else { + out.writeBoolean(true); + new ObjectMapper().configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false).writeValue((OutputStream)out, custom); + } + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + if(in.readBoolean()){ + this.custom = new ObjectMapper().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false).readValue((InputStream)in, JsonNode.class); + } + } + public UserAnnotatedTouchOnRead(String id, String firstname, String lastname) { + super(id, firstname, lastname); + } +}