spring mongo使用之类型转换踩坑记录

2,718 阅读3分钟

背景

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,就直接返回)?