Skip to content

Commit f761f3e

Browse files
committed
Add mechanism for save to do one of insert, replace or upsert.
Closes #1277.
1 parent 7e45fa2 commit f761f3e

8 files changed

+170
-24
lines changed

src/main/java/org/springframework/data/couchbase/repository/support/SimpleCouchbaseRepository.java

+20-6
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616

1717
package org.springframework.data.couchbase.repository.support;
1818

19-
import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty;
20-
2119
import java.util.Collection;
2220
import java.util.List;
2321
import java.util.Objects;
2422
import java.util.Optional;
2523
import java.util.stream.Collectors;
2624

2725
import org.springframework.data.couchbase.core.CouchbaseOperations;
26+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
27+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
2828
import org.springframework.data.couchbase.core.query.Query;
2929
import org.springframework.data.couchbase.repository.CouchbaseRepository;
3030
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
@@ -35,6 +35,7 @@
3535
import org.springframework.data.util.StreamUtils;
3636
import org.springframework.data.util.Streamable;
3737
import org.springframework.util.Assert;
38+
import org.springframework.util.ReflectionUtils;
3839

3940
import com.couchbase.client.java.query.QueryScanConsistency;
4041

@@ -71,12 +72,25 @@ public SimpleCouchbaseRepository(CouchbaseEntityInformation<T, String> entityInf
7172
@SuppressWarnings("unchecked")
7273
public <S extends T> S save(S entity) {
7374
Assert.notNull(entity, "Entity must not be null!");
74-
// if entity has non-null, non-zero version property, then replace()
7575
S result;
76-
if (hasNonZeroVersionProperty(entity, operations.getConverter())) {
77-
result = (S) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity);
78-
} else {
76+
77+
final CouchbasePersistentEntity<?> mapperEntity = operations.getConverter().getMappingContext()
78+
.getPersistentEntity(entity.getClass());
79+
final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty();
80+
final boolean versionPresent = versionProperty != null;
81+
final Long version = versionProperty == null || versionProperty.getField() == null ? null
82+
: (Long) ReflectionUtils.getField(versionProperty.getField(), entity);
83+
final boolean existingDocument = version != null && version > 0;
84+
85+
if (!versionPresent) { // the entity doesn't have a version property
86+
// No version field - no cas
7987
result = (S) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity);
88+
} else if (existingDocument) { // there is a version property, and it is non-zero
89+
// Updating existing document with cas
90+
result = (S) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity);
91+
} else { // there is a version property, but it's zero or not set.
92+
// Creating new document
93+
result = (S) operations.insertById(getJavaType()).inScope(getScope()).inCollection(getCollection()).one(entity);
8094
}
8195
return result;
8296
}

src/main/java/org/springframework/data/couchbase/repository/support/SimpleReactiveCouchbaseRepository.java

+21-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
1616

1717
package org.springframework.data.couchbase.repository.support;
1818

19-
import static org.springframework.data.couchbase.repository.support.Util.hasNonZeroVersionProperty;
20-
2119
import reactor.core.publisher.Flux;
2220
import reactor.core.publisher.Mono;
2321

@@ -28,12 +26,15 @@
2826
import org.reactivestreams.Publisher;
2927
import org.springframework.data.couchbase.core.CouchbaseOperations;
3028
import org.springframework.data.couchbase.core.ReactiveCouchbaseOperations;
29+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
30+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
3131
import org.springframework.data.couchbase.core.query.Query;
3232
import org.springframework.data.couchbase.repository.ReactiveCouchbaseRepository;
3333
import org.springframework.data.couchbase.repository.query.CouchbaseEntityInformation;
3434
import org.springframework.data.domain.Sort;
3535
import org.springframework.data.util.Streamable;
3636
import org.springframework.util.Assert;
37+
import org.springframework.util.ReflectionUtils;
3738

3839
/**
3940
* Reactive repository base implementation for Couchbase.
@@ -71,13 +72,26 @@ public SimpleReactiveCouchbaseRepository(CouchbaseEntityInformation<T, String> e
7172
@Override
7273
public <S extends T> Mono<S> save(S entity) {
7374
Assert.notNull(entity, "Entity must not be null!");
74-
// if entity has non-null, non-zero version property, then replace()
7575
Mono<S> result;
76-
if (hasNonZeroVersionProperty(entity, operations.getConverter())) {
76+
final CouchbasePersistentEntity<?> mapperEntity = operations.getConverter().getMappingContext()
77+
.getPersistentEntity(entity.getClass());
78+
final CouchbasePersistentProperty versionProperty = mapperEntity.getVersionProperty();
79+
final boolean versionPresent = versionProperty != null;
80+
final Long version = versionProperty == null || versionProperty.getField() == null ? null
81+
: (Long) ReflectionUtils.getField(versionProperty.getField(), entity);
82+
final boolean existingDocument = version != null && version > 0;
83+
84+
if (!versionPresent) { // the entity doesn't have a version property
85+
// No version field - no cas
86+
result = (Mono<S>) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection())
87+
.one(entity);
88+
} else if (existingDocument) { // there is a version property, and it is non-zero
89+
// Updating existing document with cas
7790
result = (Mono<S>) operations.replaceById(getJavaType()).inScope(getScope()).inCollection(getCollection())
7891
.one(entity);
79-
} else {
80-
result = (Mono<S>) operations.upsertById(getJavaType()).inScope(getScope()).inCollection(getCollection())
92+
} else { // there is a version property, but it's zero or not set.
93+
// Creating new document
94+
result = (Mono<S>) operations.insertById(getJavaType()).inScope(getScope()).inCollection(getCollection())
8195
.one(entity);
8296
}
8397
return result;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.couchbase.domain;
18+
19+
import org.springframework.data.couchbase.repository.Query;
20+
import org.springframework.data.repository.query.Param;
21+
import reactor.core.publisher.Flux;
22+
23+
import org.springframework.data.repository.reactive.ReactiveSortingRepository;
24+
import org.springframework.stereotype.Repository;
25+
26+
import java.util.List;
27+
28+
/**
29+
* @author Michael Reiche
30+
*/
31+
@Repository
32+
public interface ReactiveAirlineRepository extends ReactiveSortingRepository<Airline, String> {
33+
34+
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (name = $1)")
35+
List<User> getByName(@Param("airline_name")String airlineName);
36+
37+
}

src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryKeyValueIntegrationTests.java

+45-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors
2+
* Copyright 2012-2022 the original author or authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,22 +16,30 @@
1616

1717
package org.springframework.data.couchbase.repository;
1818

19-
import static org.junit.jupiter.api.Assertions.*;
19+
import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertFalse;
22+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
2025

2126
import java.lang.reflect.InvocationTargetException;
22-
import java.lang.reflect.Method;
2327
import java.util.ArrayList;
2428
import java.util.Arrays;
2529
import java.util.List;
2630
import java.util.Optional;
2731
import java.util.UUID;
2832

29-
import com.couchbase.client.java.kv.GetResult;
33+
import org.junit.jupiter.api.BeforeEach;
3034
import org.junit.jupiter.api.Test;
3135
import org.springframework.beans.factory.annotation.Autowired;
3236
import org.springframework.context.annotation.Configuration;
37+
import org.springframework.dao.DataIntegrityViolationException;
38+
import org.springframework.dao.DuplicateKeyException;
3339
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
3440
import org.springframework.data.couchbase.core.CouchbaseTemplate;
41+
import org.springframework.data.couchbase.domain.Airline;
42+
import org.springframework.data.couchbase.domain.AirlineRepository;
3543
import org.springframework.data.couchbase.domain.Course;
3644
import org.springframework.data.couchbase.domain.Library;
3745
import org.springframework.data.couchbase.domain.LibraryRepository;
@@ -50,6 +58,8 @@
5058
import org.springframework.data.couchbase.util.IgnoreWhen;
5159
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
5260

61+
import com.couchbase.client.java.kv.GetResult;
62+
5363
/**
5464
* Repository KV tests
5565
*
@@ -64,9 +74,17 @@ public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareInt
6474
@Autowired LibraryRepository libraryRepository;
6575
@Autowired SubscriptionTokenRepository subscriptionTokenRepository;
6676
@Autowired UserSubmissionRepository userSubmissionRepository;
77+
@Autowired AirlineRepository airlineRepository;
6778
@Autowired PersonValueRepository personValueRepository;
6879
@Autowired CouchbaseTemplate couchbaseTemplate;
6980

81+
@BeforeEach
82+
public void beforeEach() {
83+
super.beforeEach();
84+
couchbaseTemplate.removeByQuery(SubscriptionToken.class).withConsistency(REQUEST_PLUS).all();
85+
couchbaseTemplate.findByQuery(SubscriptionToken.class).withConsistency(REQUEST_PLUS).all();
86+
}
87+
7088
@Test
7189
void subscriptionToken() {
7290
SubscriptionToken st = new SubscriptionToken("id", 0, "type", "Dave Smith", "app123", "dev123", 0);
@@ -78,6 +96,29 @@ void subscriptionToken() {
7896
assertEquals(jdkResult.cas(), st.getVersion());
7997
}
8098

99+
@Test
100+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
101+
void saveReplaceUpsertInsert() {
102+
// the User class has a version.
103+
User user = new User(UUID.randomUUID().toString(), "f", "l");
104+
// save the document - we don't care how on this call
105+
userRepository.save(user);
106+
// Now set the version to 0, it should attempt an insert and fail.
107+
long saveVersion = user.getVersion();
108+
user.setVersion(0);
109+
assertThrows(DuplicateKeyException.class, () -> userRepository.save(user));
110+
user.setVersion(saveVersion + 1);
111+
assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user));
112+
userRepository.delete(user);
113+
114+
// Airline does not have a version
115+
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline");
116+
// save the document - we don't care how on this call
117+
airlineRepository.save(airline);
118+
airlineRepository.save(airline); // If it was an insert it would fail. Can't tell if it is an upsert or replace.
119+
airlineRepository.delete(airline);
120+
}
121+
81122
@Test
82123
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
83124
void saveAndFindById() {

src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848

4949
import javax.validation.ConstraintViolationException;
5050

51+
import org.junit.jupiter.api.BeforeEach;
5152
import org.junit.jupiter.api.Test;
5253
import org.springframework.beans.factory.annotation.Autowired;
5354
import org.springframework.context.ApplicationContext;
@@ -103,7 +104,7 @@
103104
import com.couchbase.client.java.env.ClusterEnvironment;
104105
import com.couchbase.client.java.json.JsonArray;
105106
import com.couchbase.client.java.kv.GetResult;
106-
import com.couchbase.client.java.kv.UpsertOptions;
107+
import com.couchbase.client.java.kv.InsertOptions;
107108
import com.couchbase.client.java.query.QueryOptions;
108109
import com.couchbase.client.java.query.QueryScanConsistency;
109110

@@ -131,6 +132,13 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
131132
String scopeName = "_default";
132133
String collectionName = "_default";
133134

135+
@BeforeEach
136+
public void beforeEach() {
137+
super.beforeEach();
138+
couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all();
139+
couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).all();
140+
}
141+
134142
@Test
135143
void shouldSaveAndFindAll() {
136144
Airport vie = null;
@@ -555,17 +563,18 @@ public void testTransient() {
555563
public void testCas() {
556564
User user = new User("1", "Dave", "Wilson");
557565
userRepository.save(user);
566+
long saveVersion = user.getVersion();
558567
user.setVersion(user.getVersion() - 1);
559568
assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user));
560-
user.setVersion(0);
569+
user.setVersion(saveVersion);
561570
userRepository.save(user);
562571
userRepository.delete(user);
563572
}
564573

565574
@Test
566575
public void testExpiration() {
567576
Airport airport = new Airport("1", "iata21", "icao21");
568-
airportRepository.withOptions(UpsertOptions.upsertOptions().expiry(Duration.ofSeconds(10))).save(airport);
577+
airportRepository.withOptions(InsertOptions.insertOptions().expiry(Duration.ofSeconds(10))).save(airport);
569578
Airport foundAirport = airportRepository.findByIata(airport.getIata());
570579
assertNotEquals(0, foundAirport.getExpiration());
571580
airportRepository.delete(airport);

src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryKeyValueIntegrationTests.java

+31-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors
2+
* Copyright 2012-2022 the original author or authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
2020
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
2122
import static org.junit.jupiter.api.Assertions.assertTrue;
2223

2324
import java.util.Optional;
@@ -27,9 +28,13 @@
2728
import org.springframework.beans.factory.annotation.Autowired;
2829
import org.springframework.context.annotation.Bean;
2930
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.dao.DataIntegrityViolationException;
32+
import org.springframework.dao.DuplicateKeyException;
3033
import org.springframework.data.auditing.DateTimeProvider;
3134
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
35+
import org.springframework.data.couchbase.domain.Airline;
3236
import org.springframework.data.couchbase.domain.Airport;
37+
import org.springframework.data.couchbase.domain.ReactiveAirlineRepository;
3338
import org.springframework.data.couchbase.domain.ReactiveAirportRepository;
3439
import org.springframework.data.couchbase.domain.ReactiveNaiveAuditorAware;
3540
import org.springframework.data.couchbase.domain.ReactiveUserRepository;
@@ -50,6 +55,31 @@ public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends Cluster
5055

5156
@Autowired ReactiveAirportRepository airportRepository;
5257

58+
@Autowired ReactiveAirlineRepository airlineRepository;
59+
60+
@Test
61+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
62+
void saveReplaceUpsertInsert() {
63+
// the User class has a version.
64+
User user = new User(UUID.randomUUID().toString(), "f", "l");
65+
// save the document - we don't care how on this call
66+
userRepository.save(user).block();
67+
// Now set the version to 0, it should attempt an insert and fail.
68+
long saveVersion = user.getVersion();
69+
user.setVersion(0);
70+
assertThrows(DuplicateKeyException.class, () -> userRepository.save(user).block());
71+
user.setVersion(saveVersion + 1);
72+
assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user).block());
73+
userRepository.delete(user);
74+
75+
// Airline does not have a version
76+
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline");
77+
// save the document - we don't care how on this call
78+
airlineRepository.save(airline).block();
79+
airlineRepository.save(airline).block(); // If it was an insert it would fail. Can't tell if an upsert or replace.
80+
airlineRepository.delete(airline).block();
81+
}
82+
5383
@Test
5484
void saveAndFindById() {
5585
User user = new User(UUID.randomUUID().toString(), "f", "l");

src/test/java/org/springframework/data/couchbase/repository/ReactiveCouchbaseRepositoryQueryIntegrationTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 the original author or authors.
2+
* Copyright 2017-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -118,9 +118,10 @@ void findBySimpleProperty() {
118118
public void testCas() {
119119
User user = new User("1", "Dave", "Wilson");
120120
userRepository.save(user).block();
121+
long saveVersion = user.getVersion();
121122
user.setVersion(user.getVersion() - 1);
122123
assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user).block());
123-
user.setVersion(0);
124+
user.setVersion(saveVersion);
124125
userRepository.save(user).block();
125126
userRepository.delete(user).block();
126127
}

src/test/java/org/springframework/data/couchbase/util/UnmanagedTestCluster.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ ClusterType type() {
6060

6161
@Override
6262
TestClusterConfig _start() throws Exception {
63-
bucketname = UUID.randomUUID().toString();
63+
bucketname = "my_bucket"; // UUID.randomUUID().toString();
6464

6565
Response postResponse = httpClient
6666
.newCall(new Request.Builder().header("Authorization", Credentials.basic(adminUsername, adminPassword))

0 commit comments

Comments
 (0)