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.
This commit is contained in:
Michael Reiche
2024-04-08 16:50:39 -07:00
committed by GitHub
parent fccd22a00b
commit 25aaac47e2
9 changed files with 142 additions and 58 deletions

View File

@@ -24,30 +24,27 @@ import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValueModule;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbaseList;
import org.springframework.util.Base64Utils;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValueModule;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
@@ -65,13 +62,11 @@ public final class OtherConverters {
* @return the list of converters to register.
*/
public static Collection<Converter<?, ?>> getConvertersToRegister() {
List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
List<Converter<?, ?>> converters = new ArrayList<>();
converters.add(UuidToString.INSTANCE);
converters.add(StringToUuid.INSTANCE);
converters.add(BigIntegerToString.INSTANCE);
converters.add(StringToBigInteger.INSTANCE);
converters.add(BigDecimalToString.INSTANCE);
converters.add(StringToBigDecimal.INSTANCE);
converters.add(ByteArrayToString.INSTANCE);
converters.add(StringToByteArray.INSTANCE);
@@ -114,16 +109,7 @@ public final class OtherConverters {
}
}
@WritingConverter
public enum BigIntegerToString implements Converter<BigInteger, String> {
INSTANCE;
@Override
public String convert(BigInteger source) {
return source == null ? null : source.toString();
}
}
// to support reading BigIntegers that were written as Strings (now discontinued)
@ReadingConverter
public enum StringToBigInteger implements Converter<String, BigInteger> {
INSTANCE;
@@ -134,16 +120,7 @@ public final class OtherConverters {
}
}
@WritingConverter
public enum BigDecimalToString implements Converter<BigDecimal, String> {
INSTANCE;
@Override
public String convert(BigDecimal source) {
return source == null ? null : source.toString();
}
}
// to support reading BigDecimals that were written as Strings (now discontinued)
@ReadingConverter
public enum StringToBigDecimal implements Converter<String, BigDecimal> {
INSTANCE;
@@ -160,7 +137,7 @@ public final class OtherConverters {
@Override
public String convert(byte[] source) {
return source == null ? null : Base64Utils.encodeToString(source);
return source == null ? null : Base64.getEncoder().encodeToString(source);
}
}
@@ -170,7 +147,7 @@ public final class OtherConverters {
@Override
public byte[] convert(String source) {
return source == null ? null : Base64Utils.decode(source.getBytes(StandardCharsets.UTF_8));
return source == null ? null : Base64.getDecoder().decode(source.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -220,7 +220,7 @@ public class JacksonTranslationService implements TranslationService, Initializi
case VALUE_NUMBER_INT:
return parser.getNumberValue();
case VALUE_NUMBER_FLOAT:
return parser.getDoubleValue();
return parser.getDecimalValue();
case VALUE_NULL:
return null;
default:

View File

@@ -38,7 +38,7 @@ public abstract class CouchbaseSimpleTypes {
Stream.of(JsonObject.class, JsonArray.class, Number.class).collect(toSet()), true);
public static final SimpleTypeHolder DOCUMENT_TYPES = new SimpleTypeHolder(
Stream.of(CouchbaseDocument.class, CouchbaseList.class).collect(toSet()), true);
Stream.of(CouchbaseDocument.class, CouchbaseList.class, Number.class).collect(toSet()), true);
private CouchbaseSimpleTypes() {}

View File

@@ -200,12 +200,12 @@ public class MappingCouchbaseConverterTests {
@Test
void writesBigInteger() {
CouchbaseDocument converted = new CouchbaseDocument();
BigIntegerEntity entity = new BigIntegerEntity(new BigInteger("12345"));
BigIntegerEntity entity = new BigIntegerEntity(new BigInteger("12345678901234567890123"));
converter.write(entity, converted);
Map<String, Object> result = converted.export();
assertThat(result.get("_class")).isEqualTo(entity.getClass().getName());
assertThat(result.get("attr0")).isEqualTo(entity.attr0.toString());
assertThat(result.get("attr0")).isEqualTo(entity.attr0);
assertThat(converted.getId()).isEqualTo(BaseEntity.ID);
}
@@ -213,21 +213,21 @@ public class MappingCouchbaseConverterTests {
void readsBigInteger() {
CouchbaseDocument source = new CouchbaseDocument();
source.put("_class", BigIntegerEntity.class.getName());
source.put("attr0", "12345");
source.put("attr0", new BigInteger("12345678901234567890123"));
BigIntegerEntity converted = converter.read(BigIntegerEntity.class, source);
assertThat(converted.attr0).isEqualTo(new BigInteger((String) source.get("attr0")));
assertThat(converted.attr0).isEqualTo(source.get("attr0"));
}
@Test
void writesBigDecimal() {
CouchbaseDocument converted = new CouchbaseDocument();
BigDecimalEntity entity = new BigDecimalEntity(new BigDecimal("123.45"));
BigDecimalEntity entity = new BigDecimalEntity(new BigDecimal("12345678901234567890123.45"));
converter.write(entity, converted);
Map<String, Object> result = converted.export();
assertThat(result.get("_class")).isEqualTo(entity.getClass().getName());
assertThat(result.get("attr0")).isEqualTo(entity.attr0.toString());
assertThat(result.get("attr0")).isEqualTo(entity.attr0);
assertThat(converted.getId()).isEqualTo(BaseEntity.ID);
}
@@ -235,10 +235,10 @@ public class MappingCouchbaseConverterTests {
void readsBigDecimal() {
CouchbaseDocument source = new CouchbaseDocument();
source.put("_class", BigDecimalEntity.class.getName());
source.put("attr0", "123.45");
source.put("attr0", new BigDecimal("12345678901234567890123.45"));
BigDecimalEntity converted = converter.read(BigDecimalEntity.class, source);
assertThat(converted.attr0).isEqualTo(new BigDecimal((String) source.get("attr0")));
assertThat(converted.attr0).isEqualTo(source.get("attr0"));
}
@Test

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2024 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 java.math.BigDecimal;
import java.math.BigInteger;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.couchbase.core.mapping.Document;
@Document
/**
* @author Michael Reiche
*/
public class BigAirline extends Airline {
BigInteger airlineNumber = new BigInteger("88881234567890123456"); // less than 63 bits, otherwise query truncates
BigDecimal airlineDecimal = new BigDecimal("888812345678901.23"); // less than 53 bits in mantissa
@PersistenceConstructor
public BigAirline(String id, String name, String hqCountry, Number airlineNumber, Number airlineDecimal) {
super(id, name, hqCountry);
this.airlineNumber = airlineNumber != null && !airlineNumber.equals("")
? new BigInteger(airlineNumber.toString())
: this.airlineNumber;
this.airlineDecimal = airlineDecimal != null && !airlineDecimal.equals("")
? new BigDecimal(airlineDecimal.toString())
: this.airlineDecimal;
}
public BigInteger getAirlineNumber() {
return airlineNumber;
}
public BigDecimal getAirlineDecimal() {
return airlineDecimal;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2017-2024 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 java.util.List;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.DynamicProxyable;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
/**
* @author Michael Reiche
*/
@Repository
public interface BigAirlineRepository extends CouchbaseRepository<BigAirline, String>,
QuerydslPredicateExecutor<BigAirline>, DynamicProxyable<BigAirlineRepository> {
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (name = $1)")
List<Airline> getByName(@Param("airline_name") String airlineName);
}

View File

@@ -33,15 +33,14 @@ import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.domain.Airline;
import org.springframework.data.couchbase.domain.AirlineRepository;
import org.springframework.data.couchbase.domain.Course;
import org.springframework.data.couchbase.domain.BigAirline;
import org.springframework.data.couchbase.domain.Config;
import org.springframework.data.couchbase.domain.Course;
import org.springframework.data.couchbase.domain.Library;
import org.springframework.data.couchbase.domain.LibraryRepository;
import org.springframework.data.couchbase.domain.PersonValue;
@@ -59,9 +58,6 @@ import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import com.couchbase.client.core.env.SecurityConfig;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.kv.GetResult;
/**
@@ -125,6 +121,17 @@ public class CouchbaseRepositoryKeyValueIntegrationTests extends ClusterAwareInt
airlineRepository.delete(airline);
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void saveBig() {
BigAirline airline = new BigAirline(UUID.randomUUID().toString(), "MyAirline", null, null, null);
airline = airlineRepository.save(airline);
Optional<Airline> foundMaybe = airlineRepository.findById(airline.getId());
BigAirline found = (BigAirline) foundMaybe.get();
assertEquals(found, airline);
airlineRepository.delete(airline);
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void saveAndFindById() {

View File

@@ -28,18 +28,21 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.RemoveResult;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.AddressAnnotated;
import org.springframework.data.couchbase.domain.Airline;
import org.springframework.data.couchbase.domain.Airport;
import org.springframework.data.couchbase.domain.AirportRepository;
import org.springframework.data.couchbase.domain.AirportRepositoryAnnotated;
import org.springframework.data.couchbase.domain.BigAirline;
import org.springframework.data.couchbase.domain.BigAirlineRepository;
import org.springframework.data.couchbase.domain.ConfigScoped;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserCol;
@@ -72,6 +75,7 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
@Autowired AirportRepositoryAnnotated airportRepositoryAnnotated;
@Autowired AirportRepository airportRepository;
@Autowired BigAirlineRepository bigAirlineRepository;
@Autowired UserColRepository userColRepository;
@Autowired UserSubmissionAnnotatedRepository userSubmissionAnnotatedRepository;
@Autowired UserSubmissionUnannotatedRepository userSubmissionUnannotatedRepository;
@@ -103,6 +107,7 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
couchbaseTemplate.removeByQuery(User.class).inCollection(collectionName).all();
couchbaseTemplate.removeByQuery(UserCol.class).inScope(otherScope).inCollection(otherCollection).all();
couchbaseTemplate.removeByQuery(Airport.class).inCollection(collectionName).all();
couchbaseTemplate.removeByQuery(BigAirline.class).inCollection(collectionName).all();
couchbaseTemplate.removeByQuery(Airport.class).inCollection(collectionName2).all();
couchbaseTemplate.findByQuery(Airport.class).withConsistency(REQUEST_PLUS).inCollection(collectionName).all();
}
@@ -126,6 +131,19 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
userColRepository.delete(found);
}
@Test
@Disabled // BigInteger and BigDecimal lose precision through Query
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void saveBig() {
BigAirline airline = new BigAirline(UUID.randomUUID().toString(), "MyAirline", null, null, null);
airline = bigAirlineRepository.withCollection(collectionName).save(airline);
List<Airline> foundMaybe = bigAirlineRepository.withCollection(collectionName)
.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).getByName("MyAirline");
BigAirline found = (BigAirline) foundMaybe.get(0);
assertEquals(found, airline);
bigAirlineRepository.withCollection(collectionName).delete(airline);
}
@Test
public void myTest() {

View File

@@ -39,16 +39,12 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.mapping.event.ValidatingCouchbaseEventListener;
import org.springframework.data.couchbase.core.query.QueryCriteriaDefinition;
import org.springframework.data.couchbase.domain.Airline;
import org.springframework.data.couchbase.domain.AirlineRepository;
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
import org.springframework.data.couchbase.domain.QAirline;
import org.springframework.data.couchbase.domain.time.AuditingDateTimeProvider;
import org.springframework.data.couchbase.repository.auditing.EnableCouchbaseAuditing;
import org.springframework.data.couchbase.repository.auditing.EnableReactiveCouchbaseAuditing;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
@@ -63,9 +59,6 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import com.couchbase.client.core.env.SecurityConfig;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;