我们正在Spring-boot应用程序中使用SpringDataMongoDB来管理我们的数据。
我们以前的模型是这样的:
public class Response implements Serializable {
//...
private JsonNode errorBody; //<-- Dynamic
//...
}
JsonNode
FQDN为com.fasterxml.jackson.databind.JsonNode
将这样保存的文档保存在数据库中:
"response": {
...
"errorBody": {
"_children": {
"code": {
"_value": "Error-code-value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"message": {
"_value": "Error message value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"description": {
"_value": "Error description value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
}
},
"_nodeFactory": {
"_cfgBigDecimalExact": false
},
"_class": "com.fasterxml.jackson.databind.node.ObjectNode"
},
...
}
我们已经在生产数据库中保存了数百个这样的文档 ,因为它们只是一种日志,因此无需以编程方式读取它们。
由于我们注意到将来可能很难读取此输出,因此我们决定将模型更改为此:
public class Response implements Serializable {
//...
private Map<String,Object> errorBody;
//...
}
现在像这样保存数据:
"response": {
...
"errorBody": {
"code": "Error code value",
"message": "Error message value",
"description": "Error description value",
...
},
...
}
您可能已经注意到,这要简单得多。
读取数据时,例如:repository.findAll()
读取新格式没有任何问题。
但是我们面对旧格式的这些问题:
org.springframework.data.mapping.MappingException: No property v found on entity class com.fasterxml.jackson.databind.node.TextNode to bind constructor parameter to!
或
org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.fasterxml.jackson.databind.node.ObjectNode using constructor NO_CONSTRUCTOR with arguments
当然,TextNode
类具有一个以v
作为参数的构造函数,但是属性名称为_value
并且ObjectNode
没有默认的构造函数:我们根本无法更改那。
我们已经创建了自定义转换器,并将其添加到配置中。
public class ObjectNodeWriteConverter implements Converter<ObjectNode, DBObject> {
@Override
public DBObject convert(ObjectNode source) {
return BasicDBObject.parse(source.toString());
}
}
public class ObjectNodeReadConverter implements Converter<DBObject, ObjectNode> {
@Override
public ObjectNode convert(DBObject source) {
try {
return new ObjectMapper().readValue(source.toString(), ObjectNode.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
我们对TextNode
做过同样的事情
但是我们仍然遇到错误。
在我们有ZonedDateTimeConverter
从事他的工作时,将读取转换器。
我们不能仅仅擦除或忽略旧数据,因为我们也需要阅读它们以研究它们。
我们如何设置不会读取旧格式的自定义阅读器?
答案 0 :(得分:1)
由于旧格式是预定义的,并且您知道它的结构,因此可以实现自定义反序列化程序以同时处理旧格式和新格式。如果errorBody
JSON Object
包含以下任何一个键:_children
,_nodeFactory
或_class
,则说明它是一种旧格式,您需要遍历{ {1}} _children
并获得JSON Object
键以找到实际值。您可以忽略的其余键和值。简单的实现可能如下所示:
_value
以上解串器应同时处理两种格式。
答案 1 :(得分:1)
据我了解,对于第一个模型,您确实没有保存或读取数据库的问题,但是,一旦要获取这些数据,就会注意到输出很难读取。因此,您的问题是要获取可读性良好的输出,则无需更改第一个模型,而只需扩展这些类并覆盖 toString 方法即可在获取时更改其行为。
至少要扩展三个类:
TextNode:您不能覆盖toString方法,即自定义类仅会打印值
ObjectNode:我可以看到此类中至少有四个字段要影响值:代码,消息,说明。它们是 TextNode 的类型,因此您可以将其替换为扩展类。然后覆盖 toString 方法,以便为每个字段打印CREATE TABLE dbo.TableName
(ID INT Identity(1, 1)
,NAME VARCHAR(100)
,Image VARBINARY(MAX))
GO
--Import Image to SQL Server
INSERT INTO dbo.TableName
(NAME
,Image
)
SELECT 'Capture1.PNG'
,BulkColumn
FROM Openrowset(BULK 'C:\MyImage.PNG', Single_Blob) AS Image
JsonNode:然后,您可以扩展此类并使用上面创建的自定义类,覆盖 toString 方法,以便根据需要打印并使用它,而不是普通的JsonNode >
要像这样工作,就可以避免保存或读取数据的方式,而只是影响视图。
您可以将其视为 SOLID 原则的一小部分,尤其是 OCP (打开一个封闭的原则:避免更改类行为,而将其扩展以创建一个自定义行为)和 LSP (Liskov替换原理:子类型必须可以在行为上替代其基本类型)。
答案 2 :(得分:0)
Michal Ziober的答案不能完全解决问题,因为我们需要告诉SpringData MongoDb我们希望他使用自定义反序列化器 (注释模型不适用于Spring数据mongodb):
public class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, Object>> {
@Override
public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
if (!root.isObject()) {
// ignore everything except JSON Object
return Collections.emptyMap();
}
ObjectNode objectNode = (ObjectNode) root;
if (isOldFormat(objectNode)) {
return deserialize(objectNode);
}
return toMap(objectNode);
}
protected boolean isOldFormat(ObjectNode objectNode) {
final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
final Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map<String, Object> deserialize(ObjectNode root) {
JsonNode children = root.get("_children");
if (children.isArray()) {
children = children.get(0);
children = children.get("_children");
}
return extractValues(children);
}
private Map<String, Object> extractValues(JsonNode children) {
Map<String, Object> result = new LinkedHashMap<>();
children.fields().forEachRemaining(entry -> {
String key = entry.getKey();
if (!key.equals("_class"))
result.put(key, entry.getValue().get("_value").toString());
});
return result;
}
private Map<String, Object> toMap(ObjectNode objectNode) {
Map<String, Object> result = new LinkedHashMap<>();
objectNode.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
实际上,我们不是直接向他提供序列化程序,而是通过配置了该自定义反序列化程序的 ObjectMapper
public class CustomMappingMongoConverter extends MappingMongoConverter {
//The configured objectMapper that will be passed during instatiation
private ObjectMapper objectMapper;
public CustomMappingMongoConverter(DbRefResolver dbRefResolver, MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, ObjectMapper objectMapper) {
super(dbRefResolver, mappingContext);
this.objectMapper = objectMapper;
}
@Override
public <S> S read(Class<S> clazz, Bson dbObject) {
try {
return objectMapper.readValue(dbObject.toString(), clazz);
} catch (IOException e) {
throw new RuntimeException(dbObject.toString(), e);
}
}
//in case you want to serialize with your custom objectMapper as well
@Override
public void write(Object obj, Bson dbo) {
String string = null;
try {
string = objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(string, e);
}
((DBObject) dbo).putAll((DBObject) BasicDBObject.parse(string));
}
}
public class MongoConfiguration extends AbstractMongoClientConfiguration {
//... other configuration method beans
@Bean
@Override
public MappingMongoConverter mappingMongoConverter() throws Exception {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new SimpleModule() {
{
addDeserializer(Map.class, new ErrorMapJsonDeserializer());
}
});
return new CustomMappingMongoConverter(dbRefResolver, mongoMappingContext(), objectMapper);
}
}