背景
java中对于金额,我们通常使用Bigdecimal进行存储,而mongo中是不存在Bigdecimal类型,对应的是Decimal128。
项目中使用mongo的地方,为了能够在插入mongo时将Bigdecimal转为Decimal128,查询时将Decimal128转回Bigdecimal,可以利用spring中的org.springframework.core.convert.converter.Converter。如下
BigDecimalToDecimal128Converter.class
@WritingConverter
public class BigDecimalToDecimal128Converter implements Converter<BigDecimal, Decimal128> {
@Override
public Decimal128 convert(BigDecimal bigDecimal) {
return new Decimal128(bigDecimal);
}
}
Decimal128ToBigDecimalConverter.class
@ReadingConverter
public class Decimal128ToBigDecimalConverter implements Converter<Decimal128, BigDecimal> {
@Override
public BigDecimal convert(Decimal128 decimal128) {
return decimal128.bigDecimalValue();
}
}
在向容器中注入MongoTemplate时添加自定义的Converter
@Bean
public CustomConversions customConversions() {
List<Converter<?, ?>> converters = new ArrayList<>(2);
converters.add(new BigDecimalToDecimal128Converter());
converters.add(new Decimal128ToBigDecimalConverter());
return new CustomConversions(converters);
}
@Bean("customMongoTemplate")
public MongoTemplate mongoTemplate() {
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory());
MappingMongoConverter mongoMapping = (MappingMongoConverter) mongoTemplate.getConverter();
mongoMapping.setCustomConversions(customConversions());
mongoMapping.afterPropertiesSet();
return mongoTemplate;
}
这样我们就可以自由的对BigDecimal类型进行mongo存储和读取了。
问题
spring的特点就是:使用灵活,但是理解不到位带来的使用不当就会引起很多调试的工作量。
上述方法在正常情况下是没有问题的,但是真正项目中免不了错误使用带来问题。
反例:
实体类:
@Data
@Document(collection = "BookTestXyh")
public class Book {
private String bookNo;
private BigDecimal amount;
private Map<String, JSONObject> extend;
}
测试类:
public String test() {
Book book = new Book();
book.setBookNo("book1");
book.setAmount(new BigDecimal("10.11"));
Map<String, JSONObject> extend = new HashMap<>();
JSONObject data = new JSONObject();
data.put("amountTax", new BigDecimal("0.88"));
extend.put("param", data);
book.setExtend(extend);
mongoTemplate.insert(book);
Query query = Query.query(Criteria.where("bookNo").is("book1"));
List<Book> books = mongoTemplate.find(query, Book.class);
return "success";
}
测试类中先进行插入后执行查询。
插入结果:
没问题,BigDecimal正常转换成Decimal128。
那查询呢?
可以看到从mongo中读出,对于amount正确转换成BigDecimal,而对于extend中的amountTax,却依然是Decimal128,并未转换成BigDecimal。如果后续我们对于amountTax按照BigDecimal去使用,那就不对了。
剖析
为啥呢?
可以想下,从mongo中读数据到内存中必定会经过反序列化和类型转换,那目前的现象反序列化没问题,问题一定就在类型转换。
而amount字段能够正常类型转换,但是对于extend中的amountTax却没有完成转换。那一定是在解析嵌套结构时处出了问题。
JsonObject是什么类型?本质继承了Map,那看一下,spring mongo对于Map类型的转换(MappingMongoConverter#readMap):
protected Map<Object, Object> readMap(TypeInformation<?> type, DBObject dbObject, ObjectPath path) {
Assert.notNull(dbObject, "DBObject must not be null!");
Assert.notNull(path, "Object path must not be null!");
Class<?> mapType = typeMapper.readType(dbObject, type).getType();
TypeInformation<?> keyType = type.getComponentType();
Class<?> rawKeyType = keyType == null ? null : keyType.getType();
// Map Value对应的类型
// 对于extend的vlaue是Jsonobject,也就是Map<String,Object>,因此在转换amountTax时,对应的valueType是Object
TypeInformation<?> valueType = type.getMapValueType();
Class<?> rawValueType = valueType == null ? null : valueType.getType();
Map<Object, Object> map = CollectionFactory.createMap(mapType, rawKeyType, dbObject.keySet().size());
Map<String, Object> sourceMap = dbObject.toMap();
if (!DBRef.class.equals(rawValueType) && isCollectionOfDbRefWhereBulkFetchIsPossible(sourceMap.values())) {
bulkReadAndConvertDBRefMapIntoTarget(valueType, rawValueType, sourceMap, map);
return map;
}
for (Entry<String, Object> entry : sourceMap.entrySet()) {
if (typeMapper.isTypeKey(entry.getKey())) {
continue;
}
Object key = potentiallyUnescapeMapKey(entry.getKey());
if (rawKeyType != null) {
key = conversionService.convert(key, rawKeyType);
}
Object value = entry.getValue();
if (value instanceof DBObject) {
// 嵌套结构,继续递归解析
map.put(key, read(valueType, (DBObject) value, path));
} else if (value instanceof DBRef) {
map.put(key, DBRef.class.equals(rawValueType) ? value
: readAndConvertDBRef((DBRef) value, valueType, ObjectPath.ROOT, rawValueType));
} else {
Class<?> valueClass = valueType == null ? null : valueType.getType();
// 对于JsonObject,本质为Map<String,Object>,因此在转换amountTax字段时,spring认为我们想要将其转换成Object类型!
// 而Object是公共祖先父类,从Mongo中读出的Decimal128本身也是Object,所以,Spring认为就不需要再对amountTax进行类型转换了,反序列化得到的是什么就返回什么
map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass));
}
}
return map;
}
关键点就在map.put(key, getPotentiallyConvertedSimpleRead(value, valueClass));已注释。
总结
总结下为什么为踩坑。
1.对于mongo和java的类型转换理解不深,导致错误使用Converter,致使Decimal128ToBigDecimalConverter在某些情况下失效。
2.对于类型敏感的字段,不适合用Map<String,JsonObject>存mongo,需要考虑其他的数据结构。
解决
如何解决?
1.复写MappingMongoConverter,自定义类型转换逻辑。
2.改变数据结构。
反思
1.对于需要持久化的类,数据结构的设计很重要,当复杂场景出现问题时,改变数据结构代价很大。
2.mongo的优点就是非结构化数据存储,在入库时可以递归去转换字段类型,但是读库时却不能智能地类型转换。是spring对mongo的支持不完善,还是自身使用错误的问题?
对于本例,为何不能根据读到的类型去进行转换(发现读到的类型是Decimal128,就应用Converter去转Decimal128为BigDecimal),而是依据需要返回的类型去判断是否进行转换(发现需要返回Object,读到的Decimal128是Object,就直接返回)?