Skip to content

Commit ef56e3f

Browse files
authored
Add mechanism for save to do one of insert, replace or upsert. (#1316)
Closes #1277.
1 parent 5c5dde3 commit ef56e3f

7 files changed

+162
-18
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

+39
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

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

19+
20+
import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
1921
import static org.junit.jupiter.api.Assertions.assertEquals;
2022
import static org.junit.jupiter.api.Assertions.assertFalse;
2123
import static org.junit.jupiter.api.Assertions.assertNotEquals;
24+
import static org.junit.jupiter.api.Assertions.assertThrows;
2225
import static org.junit.jupiter.api.Assertions.assertTrue;
2326

2427
import java.lang.reflect.InvocationTargetException;
@@ -28,11 +31,16 @@
2831
import java.util.Optional;
2932
import java.util.UUID;
3033

34+
import org.junit.jupiter.api.BeforeEach;
3135
import org.junit.jupiter.api.Test;
3236
import org.springframework.beans.factory.annotation.Autowired;
3337
import org.springframework.context.annotation.Configuration;
38+
import org.springframework.dao.DataIntegrityViolationException;
39+
import org.springframework.dao.DuplicateKeyException;
3440
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
3541
import org.springframework.data.couchbase.core.CouchbaseTemplate;
42+
import org.springframework.data.couchbase.domain.Airline;
43+
import org.springframework.data.couchbase.domain.AirlineRepository;
3644
import org.springframework.data.couchbase.domain.Course;
3745
import org.springframework.data.couchbase.domain.Library;
3846
import org.springframework.data.couchbase.domain.LibraryRepository;
@@ -67,9 +75,17 @@ public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareInt
6775
@Autowired LibraryRepository libraryRepository;
6876
@Autowired SubscriptionTokenRepository subscriptionTokenRepository;
6977
@Autowired UserSubmissionRepository userSubmissionRepository;
78+
@Autowired AirlineRepository airlineRepository;
7079
@Autowired PersonValueRepository personValueRepository;
7180
@Autowired CouchbaseTemplate couchbaseTemplate;
7281

82+
@BeforeEach
83+
public void beforeEach() {
84+
super.beforeEach();
85+
couchbaseTemplate.removeByQuery(SubscriptionToken.class).withConsistency(REQUEST_PLUS).all();
86+
couchbaseTemplate.findByQuery(SubscriptionToken.class).withConsistency(REQUEST_PLUS).all();
87+
}
88+
7389
@Test
7490
void subscriptionToken() {
7591
SubscriptionToken st = new SubscriptionToken("id", 0, "type", "Dave Smith", "app123", "dev123", 0);
@@ -82,6 +98,29 @@ void subscriptionToken() {
8298
subscriptionTokenRepository.delete(st);
8399
}
84100

101+
@Test
102+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
103+
void saveReplaceUpsertInsert() {
104+
// the User class has a version.
105+
User user = new User(UUID.randomUUID().toString(), "f", "l");
106+
// save the document - we don't care how on this call
107+
userRepository.save(user);
108+
// Now set the version to 0, it should attempt an insert and fail.
109+
long saveVersion = user.getVersion();
110+
user.setVersion(0);
111+
assertThrows(DuplicateKeyException.class, () -> userRepository.save(user));
112+
user.setVersion(saveVersion + 1);
113+
assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user));
114+
userRepository.delete(user);
115+
116+
// Airline does not have a version
117+
Airline airline = new Airline(UUID.randomUUID().toString(), "MyAirline");
118+
// save the document - we don't care how on this call
119+
airlineRepository.save(airline);
120+
airlineRepository.save(airline); // If it was an insert it would fail. Can't tell if it is an upsert or replace.
121+
airlineRepository.delete(airline);
122+
}
123+
85124
@Test
86125
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
87126
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;
@@ -104,7 +105,7 @@
104105
import com.couchbase.client.java.env.ClusterEnvironment;
105106
import com.couchbase.client.java.json.JsonArray;
106107
import com.couchbase.client.java.kv.GetResult;
107-
import com.couchbase.client.java.kv.UpsertOptions;
108+
import com.couchbase.client.java.kv.InsertOptions;
108109
import com.couchbase.client.java.query.QueryOptions;
109110
import com.couchbase.client.java.query.QueryScanConsistency;
110111

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

136+
@BeforeEach
137+
public void beforeEach() {
138+
super.beforeEach();
139+
couchbaseTemplate.removeByQuery(User.class).withConsistency(REQUEST_PLUS).all();
140+
couchbaseTemplate.findByQuery(User.class).withConsistency(REQUEST_PLUS).all();
141+
}
142+
135143
@Test
136144
void shouldSaveAndFindAll() {
137145
Airport vie = null;
@@ -556,17 +564,18 @@ public void testTransient() {
556564
public void testCas() {
557565
User user = new User("1", "Dave", "Wilson");
558566
userRepository.save(user);
567+
long saveVersion = user.getVersion();
559568
user.setVersion(user.getVersion() - 1);
560569
assertThrows(DataIntegrityViolationException.class, () -> userRepository.save(user));
561-
user.setVersion(0);
570+
user.setVersion(saveVersion);
562571
userRepository.save(user);
563572
userRepository.delete(user);
564573
}
565574

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

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

+30
Original file line numberDiff line numberDiff line change
@@ -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;
@@ -53,6 +58,31 @@ public class ReactiveCouchbaseRepositoryKeyValueIntegrationTests extends Cluster
5358

5459
@Autowired ReactiveAirportRepository airportRepository;
5560

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

0 commit comments

Comments
 (0)