Skip to content

Commit bccab34

Browse files
committed
Store BigDecimal and BigInteger as Numbers like the Java SDK does. (#1933)
Also be able to read BigDecimal and BigInteger that were written as String. Also does not lose precision by converting BigDecimal to double in the transcoder. Closes #1611.
1 parent ca7bbee commit bccab34

File tree

9 files changed

+142
-58
lines changed

9 files changed

+142
-58
lines changed

src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java

+11-34
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,27 @@
2424
import java.nio.charset.StandardCharsets;
2525
import java.time.YearMonth;
2626
import java.util.ArrayList;
27+
import java.util.Base64;
2728
import java.util.Collection;
28-
import java.util.HashMap;
2929
import java.util.List;
3030
import java.util.Map;
3131
import java.util.Optional;
3232
import java.util.UUID;
3333

34-
import com.couchbase.client.java.json.JsonArray;
35-
import com.couchbase.client.java.json.JsonObject;
36-
import com.couchbase.client.java.json.JsonValueModule;
37-
import com.fasterxml.jackson.core.type.TypeReference;
38-
import com.fasterxml.jackson.databind.JsonNode;
39-
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
40-
import com.fasterxml.jackson.databind.node.ObjectNode;
4134
import org.springframework.core.convert.converter.Converter;
4235
import org.springframework.data.convert.ReadingConverter;
4336
import org.springframework.data.convert.WritingConverter;
4437
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
4538
import org.springframework.data.couchbase.core.mapping.CouchbaseList;
46-
import org.springframework.util.Base64Utils;
4739

4840
import com.couchbase.client.core.encryption.CryptoManager;
41+
import com.couchbase.client.java.json.JsonArray;
42+
import com.couchbase.client.java.json.JsonObject;
43+
import com.couchbase.client.java.json.JsonValueModule;
4944
import com.fasterxml.jackson.core.JsonFactory;
5045
import com.fasterxml.jackson.core.JsonGenerator;
46+
import com.fasterxml.jackson.core.type.TypeReference;
47+
import com.fasterxml.jackson.databind.JsonNode;
5148
import com.fasterxml.jackson.databind.ObjectMapper;
5249

5350
/**
@@ -65,13 +62,11 @@ private OtherConverters() {}
6562
* @return the list of converters to register.
6663
*/
6764
public static Collection<Converter<?, ?>> getConvertersToRegister() {
68-
List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
65+
List<Converter<?, ?>> converters = new ArrayList<>();
6966

7067
converters.add(UuidToString.INSTANCE);
7168
converters.add(StringToUuid.INSTANCE);
72-
converters.add(BigIntegerToString.INSTANCE);
7369
converters.add(StringToBigInteger.INSTANCE);
74-
converters.add(BigDecimalToString.INSTANCE);
7570
converters.add(StringToBigDecimal.INSTANCE);
7671
converters.add(ByteArrayToString.INSTANCE);
7772
converters.add(StringToByteArray.INSTANCE);
@@ -114,16 +109,7 @@ public UUID convert(String source) {
114109
}
115110
}
116111

117-
@WritingConverter
118-
public enum BigIntegerToString implements Converter<BigInteger, String> {
119-
INSTANCE;
120-
121-
@Override
122-
public String convert(BigInteger source) {
123-
return source == null ? null : source.toString();
124-
}
125-
}
126-
112+
// to support reading BigIntegers that were written as Strings (now discontinued)
127113
@ReadingConverter
128114
public enum StringToBigInteger implements Converter<String, BigInteger> {
129115
INSTANCE;
@@ -134,16 +120,7 @@ public BigInteger convert(String source) {
134120
}
135121
}
136122

137-
@WritingConverter
138-
public enum BigDecimalToString implements Converter<BigDecimal, String> {
139-
INSTANCE;
140-
141-
@Override
142-
public String convert(BigDecimal source) {
143-
return source == null ? null : source.toString();
144-
}
145-
}
146-
123+
// to support reading BigDecimals that were written as Strings (now discontinued)
147124
@ReadingConverter
148125
public enum StringToBigDecimal implements Converter<String, BigDecimal> {
149126
INSTANCE;
@@ -160,7 +137,7 @@ public enum ByteArrayToString implements Converter<byte[], String> {
160137

161138
@Override
162139
public String convert(byte[] source) {
163-
return source == null ? null : Base64Utils.encodeToString(source);
140+
return source == null ? null : Base64.getEncoder().encodeToString(source);
164141
}
165142
}
166143

@@ -170,7 +147,7 @@ public enum StringToByteArray implements Converter<String, byte[]> {
170147

171148
@Override
172149
public byte[] convert(String source) {
173-
return source == null ? null : Base64Utils.decode(source.getBytes(StandardCharsets.UTF_8));
150+
return source == null ? null : Base64.getDecoder().decode(source.getBytes(StandardCharsets.UTF_8));
174151
}
175152
}
176153

src/main/java/org/springframework/data/couchbase/core/convert/translation/JacksonTranslationService.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private Object decodePrimitive(final JsonToken token, final JsonParser parser) t
220220
case VALUE_NUMBER_INT:
221221
return parser.getNumberValue();
222222
case VALUE_NUMBER_FLOAT:
223-
return parser.getDoubleValue();
223+
return parser.getDecimalValue();
224224
case VALUE_NULL:
225225
return null;
226226
default:

src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseSimpleTypes.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public abstract class CouchbaseSimpleTypes {
3838
Stream.of(JsonObject.class, JsonArray.class, Number.class).collect(toSet()), true);
3939

4040
public static final SimpleTypeHolder DOCUMENT_TYPES = new SimpleTypeHolder(
41-
Stream.of(CouchbaseDocument.class, CouchbaseList.class).collect(toSet()), true);
41+
Stream.of(CouchbaseDocument.class, CouchbaseList.class, Number.class).collect(toSet()), true);
4242

4343
private CouchbaseSimpleTypes() {}
4444

src/test/java/org/springframework/data/couchbase/core/mapping/MappingCouchbaseConverterTests.java

+8-8
Original file line numberDiff line numberDiff line change
@@ -200,45 +200,45 @@ void readsNoTypeAlias() {
200200
@Test
201201
void writesBigInteger() {
202202
CouchbaseDocument converted = new CouchbaseDocument();
203-
BigIntegerEntity entity = new BigIntegerEntity(new BigInteger("12345"));
203+
BigIntegerEntity entity = new BigIntegerEntity(new BigInteger("12345678901234567890123"));
204204

205205
converter.write(entity, converted);
206206
Map<String, Object> result = converted.export();
207207
assertThat(result.get("_class")).isEqualTo(entity.getClass().getName());
208-
assertThat(result.get("attr0")).isEqualTo(entity.attr0.toString());
208+
assertThat(result.get("attr0")).isEqualTo(entity.attr0);
209209
assertThat(converted.getId()).isEqualTo(BaseEntity.ID);
210210
}
211211

212212
@Test
213213
void readsBigInteger() {
214214
CouchbaseDocument source = new CouchbaseDocument();
215215
source.put("_class", BigIntegerEntity.class.getName());
216-
source.put("attr0", "12345");
216+
source.put("attr0", new BigInteger("12345678901234567890123"));
217217

218218
BigIntegerEntity converted = converter.read(BigIntegerEntity.class, source);
219-
assertThat(converted.attr0).isEqualTo(new BigInteger((String) source.get("attr0")));
219+
assertThat(converted.attr0).isEqualTo(source.get("attr0"));
220220
}
221221

222222
@Test
223223
void writesBigDecimal() {
224224
CouchbaseDocument converted = new CouchbaseDocument();
225-
BigDecimalEntity entity = new BigDecimalEntity(new BigDecimal("123.45"));
225+
BigDecimalEntity entity = new BigDecimalEntity(new BigDecimal("12345678901234567890123.45"));
226226

227227
converter.write(entity, converted);
228228
Map<String, Object> result = converted.export();
229229
assertThat(result.get("_class")).isEqualTo(entity.getClass().getName());
230-
assertThat(result.get("attr0")).isEqualTo(entity.attr0.toString());
230+
assertThat(result.get("attr0")).isEqualTo(entity.attr0);
231231
assertThat(converted.getId()).isEqualTo(BaseEntity.ID);
232232
}
233233

234234
@Test
235235
void readsBigDecimal() {
236236
CouchbaseDocument source = new CouchbaseDocument();
237237
source.put("_class", BigDecimalEntity.class.getName());
238-
source.put("attr0", "123.45");
238+
source.put("attr0", new BigDecimal("12345678901234567890123.45"));
239239

240240
BigDecimalEntity converted = converter.read(BigDecimalEntity.class, source);
241-
assertThat(converted.attr0).isEqualTo(new BigDecimal((String) source.get("attr0")));
241+
assertThat(converted.attr0).isEqualTo(source.get("attr0"));
242242
}
243243

244244
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2012-2024 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+
package org.springframework.data.couchbase.domain;
17+
18+
import java.math.BigDecimal;
19+
import java.math.BigInteger;
20+
21+
import org.springframework.data.annotation.PersistenceConstructor;
22+
import org.springframework.data.couchbase.core.mapping.Document;
23+
24+
@Document
25+
/**
26+
* @author Michael Reiche
27+
*/
28+
public class BigAirline extends Airline {
29+
BigInteger airlineNumber = new BigInteger("88881234567890123456"); // less than 63 bits, otherwise query truncates
30+
BigDecimal airlineDecimal = new BigDecimal("888812345678901.23"); // less than 53 bits in mantissa
31+
32+
@PersistenceConstructor
33+
public BigAirline(String id, String name, String hqCountry, Number airlineNumber, Number airlineDecimal) {
34+
super(id, name, hqCountry);
35+
this.airlineNumber = airlineNumber != null && !airlineNumber.equals("")
36+
? new BigInteger(airlineNumber.toString())
37+
: this.airlineNumber;
38+
this.airlineDecimal = airlineDecimal != null && !airlineDecimal.equals("")
39+
? new BigDecimal(airlineDecimal.toString())
40+
: this.airlineDecimal;
41+
}
42+
43+
public BigInteger getAirlineNumber() {
44+
return airlineNumber;
45+
}
46+
47+
public BigDecimal getAirlineDecimal() {
48+
return airlineDecimal;
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2017-2024 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 java.util.List;
20+
21+
import org.springframework.data.couchbase.repository.CouchbaseRepository;
22+
import org.springframework.data.couchbase.repository.DynamicProxyable;
23+
import org.springframework.data.couchbase.repository.Query;
24+
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
25+
import org.springframework.data.repository.query.Param;
26+
import org.springframework.stereotype.Repository;
27+
28+
/**
29+
* @author Michael Reiche
30+
*/
31+
@Repository
32+
public interface BigAirlineRepository extends CouchbaseRepository<BigAirline, String>,
33+
QuerydslPredicateExecutor<BigAirline>, DynamicProxyable<BigAirlineRepository> {
34+
35+
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (name = $1)")
36+
List<Airline> getByName(@Param("airline_name") String airlineName);
37+
38+
}

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

+13-6
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,14 @@
3333
import org.junit.jupiter.api.BeforeEach;
3434
import org.junit.jupiter.api.Test;
3535
import org.springframework.beans.factory.annotation.Autowired;
36-
import org.springframework.context.annotation.Configuration;
3736
import org.springframework.dao.DuplicateKeyException;
3837
import org.springframework.dao.OptimisticLockingFailureException;
39-
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
4038
import org.springframework.data.couchbase.core.CouchbaseTemplate;
4139
import org.springframework.data.couchbase.domain.Airline;
4240
import org.springframework.data.couchbase.domain.AirlineRepository;
43-
import org.springframework.data.couchbase.domain.Course;
41+
import org.springframework.data.couchbase.domain.BigAirline;
4442
import org.springframework.data.couchbase.domain.Config;
43+
import org.springframework.data.couchbase.domain.Course;
4544
import org.springframework.data.couchbase.domain.Library;
4645
import org.springframework.data.couchbase.domain.LibraryRepository;
4746
import org.springframework.data.couchbase.domain.PersonValue;
@@ -59,9 +58,6 @@
5958
import org.springframework.test.annotation.DirtiesContext;
6059
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
6160

62-
import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
63-
import com.couchbase.client.core.env.SecurityConfig;
64-
import com.couchbase.client.java.env.ClusterEnvironment;
6561
import com.couchbase.client.java.kv.GetResult;
6662

6763
/**
@@ -125,6 +121,17 @@ void saveReplaceUpsertInsert() {
125121
airlineRepository.delete(airline);
126122
}
127123

124+
@Test
125+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
126+
void saveBig() {
127+
BigAirline airline = new BigAirline(UUID.randomUUID().toString(), "MyAirline", null, null, null);
128+
airline = airlineRepository.save(airline);
129+
Optional<Airline> foundMaybe = airlineRepository.findById(airline.getId());
130+
BigAirline found = (BigAirline) foundMaybe.get();
131+
assertEquals(found, airline);
132+
airlineRepository.delete(airline);
133+
}
134+
128135
@Test
129136
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
130137
void saveAndFindById() {

src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java

+19-1
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,21 @@
2828
import org.junit.jupiter.api.AfterEach;
2929
import org.junit.jupiter.api.BeforeAll;
3030
import org.junit.jupiter.api.BeforeEach;
31+
import org.junit.jupiter.api.Disabled;
3132
import org.junit.jupiter.api.Test;
3233
import org.springframework.beans.factory.annotation.Autowired;
33-
import org.springframework.context.annotation.Configuration;
3434
import org.springframework.dao.DataRetrievalFailureException;
3535
import org.springframework.data.couchbase.core.CouchbaseTemplate;
3636
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
3737
import org.springframework.data.couchbase.core.RemoveResult;
3838
import org.springframework.data.couchbase.domain.Address;
3939
import org.springframework.data.couchbase.domain.AddressAnnotated;
40+
import org.springframework.data.couchbase.domain.Airline;
4041
import org.springframework.data.couchbase.domain.Airport;
4142
import org.springframework.data.couchbase.domain.AirportRepository;
4243
import org.springframework.data.couchbase.domain.AirportRepositoryAnnotated;
44+
import org.springframework.data.couchbase.domain.BigAirline;
45+
import org.springframework.data.couchbase.domain.BigAirlineRepository;
4346
import org.springframework.data.couchbase.domain.ConfigScoped;
4447
import org.springframework.data.couchbase.domain.User;
4548
import org.springframework.data.couchbase.domain.UserCol;
@@ -72,6 +75,7 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
7275

7376
@Autowired AirportRepositoryAnnotated airportRepositoryAnnotated;
7477
@Autowired AirportRepository airportRepository;
78+
@Autowired BigAirlineRepository bigAirlineRepository;
7579
@Autowired UserColRepository userColRepository;
7680
@Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository;
7781
@Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository;
@@ -103,6 +107,7 @@ public void beforeEach() {
103107
couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all();
104108
couchbaseTemplate.removeByQuery(UserCol.class).inScope(otherScope).inCollection(otherCollection).all();
105109
couchbaseTemplate.removeByQuery(Airport.class).inCollection(collectionName).all();
110+
couchbaseTemplate.removeByQuery(BigAirline.class).inCollection(collectionName).all();
106111
couchbaseTemplate.removeByQuery(Airport.class).inCollection(collectionName2).all();
107112
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
108113
}
@@ -126,6 +131,19 @@ void findByKey() {
126131
userColRepository.delete(found);
127132
}
128133

134+
@Test
135+
@Disabled // BigInteger and BigDecimal lose precision through Query
136+
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
137+
void saveBig() {
138+
BigAirline airline = new BigAirline(UUID.randomUUID().toString(), "MyAirline", null, null, null);
139+
airline = bigAirlineRepository.withCollection(collectionName).save(airline);
140+
List<Airline> foundMaybe = bigAirlineRepository.withCollection(collectionName)
141+
.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).getByName("MyAirline");
142+
BigAirline found = (BigAirline) foundMaybe.get(0);
143+
assertEquals(found, airline);
144+
bigAirlineRepository.withCollection(collectionName).delete(airline);
145+
}
146+
129147
@Test
130148
public void myTest() {
131149

0 commit comments

Comments
 (0)