Skip to content

Commit a89612d

Browse files
committed
Store BigDecimal and BigInteger as Numbers like the Java SDK does.
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 fccd22a commit a89612d

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)