How to read a com.fasterxml.jackson.databind.node.TextNode from a Mongo DB and convert to a Map <String, Object>?

huangapple go评论62阅读模式
英文:

How to read a com.fasterxml.jackson.databind.node.TextNode from a Mongo DB and convert to a Map <String, Object>?

问题

我们在一个Spring Boot应用程序中使用SpringDataMongoDB来管理我们的数据。

我们以前的模型是这样的:

public class Response implements Serializable {
    //...
    private JsonNode errorBody; //&lt;-- 动态类型
    //...
}

JsonNode 的全限定类名是 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 的转换器在正常工作。

我们不能简单地清除或忽略旧数据,因为我们还需要读取这些数据以进行研究。

我们如何设置一个不会在读取旧格式时失败的自定义读取器?

英文:

We are using SpringDataMongoDB in a Spring-boot app to manage our data.

Our previous model was this:

public class Response implements Serializable {
    //...
    private JsonNode errorBody; //&lt;-- Dynamic
    //...
}

JsonNode FQDN is com.fasterxml.jackson.databind.JsonNode

Which saved documents like so in the DB:

&quot;response&quot;: {
  ...
        &quot;errorBody&quot;: {
          &quot;_children&quot;: {
            &quot;code&quot;: {
              &quot;_value&quot;: &quot;Error-code-value&quot;,
              &quot;_class&quot;: &quot;com.fasterxml.jackson.databind.node.TextNode&quot;
            },
            &quot;message&quot;: {
              &quot;_value&quot;: &quot;Error message value&quot;,
              &quot;_class&quot;: &quot;com.fasterxml.jackson.databind.node.TextNode&quot;
            },
            &quot;description&quot;: {
              &quot;_value&quot;: &quot;Error description value&quot;,
              &quot;_class&quot;: &quot;com.fasterxml.jackson.databind.node.TextNode&quot;
            }
          },
          &quot;_nodeFactory&quot;: {
            &quot;_cfgBigDecimalExact&quot;: false
          },
          &quot;_class&quot;: &quot;com.fasterxml.jackson.databind.node.ObjectNode&quot;
     },
  ...
 }

We've saved hundreds of documents like this on the production database without ever the need to read them programmatically as they are just kind of logs.

As we noticed that this output could be difficult to read in the future, we've decided to change the model to this:

public class Response implements Serializable {
    //...
    private Map&lt;String,Object&gt; errorBody;
    //...
}

The data are now saved like so:

&quot;response&quot;: {
  ...
        &quot;errorBody&quot;: {
          &quot;code&quot;: &quot;Error code value&quot;,
          &quot;message&quot;: &quot;Error message value&quot;,
          &quot;description&quot;: &quot;Error description value&quot;,
          ...
        },
  ...
 }

Which, as you may have noticed is pretty much more simple.

When reading the data, ex: repository.findAll()

The new format is read without any issue.

But we face these issues with the old format:

org.springframework.data.mapping.MappingException: No property v found on entity class com.fasterxml.jackson.databind.node.TextNode to bind constructor parameter to!

Or

org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.fasterxml.jackson.databind.node.ObjectNode using constructor NO_CONSTRUCTOR with arguments

Of course the TextNode class has a constructor with v as param but the property name is _value and ObjectNode has no default constructor: We simply can't change that.

We've created custom converters that we've added to our configurations.

public class ObjectNodeWriteConverter implements Converter&lt;ObjectNode, DBObject&gt; {    
    @Override
    public DBObject convert(ObjectNode source) {
        return BasicDBObject.parse(source.toString());
    }
}
public class ObjectNodeReadConverter implements Converter&lt;DBObject, ObjectNode&gt; {
    @Override
    public ObjectNode convert(DBObject source) {
        try {
            return new ObjectMapper().readValue(source.toString(), ObjectNode.class);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

We did the same for TextNode

But we still got the errors.

The converters are read as we have a ZonedDateTimeConverter that is doing his job.

We can not just wipe out or ignore the old data as we need to read them too in order to study them.

How can we set up a custom reader that will not fail reading the old format ?

答案1

得分: 1

由于旧格式是预定义的,您知道它的结构,您可以实现自定义的反序列化器来同时处理旧格式和新格式。如果errorBody中的JSON对象包含以下任何键:_children_nodeFactory_class,则说明它是旧格式,您需要迭代_children中的键,并获取_value键以找到实际值。其余的键和值可以忽略。简单的实现可能如下所示:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.ToString;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class JsonMongo2FormatsApp {
    public static void main(String[] args) throws IOException {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        JsonMapper mapper = JsonMapper.builder().build();
        Response response = mapper.readValue(jsonFile, Response.class);
        System.out.println(response.getErrorBody());
    }
}

@Data
@ToString
class Response {

    @JsonDeserialize(using = ErrorMapJsonDeserializer.class)
    private Map<String, String> errorBody;
}

class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, String>> {

    @Override
    public Map<String, String> 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, String> deserialize(ObjectNode root) {
        JsonNode children = root.get("_children");
        Map<String, String> result = new LinkedHashMap<>();
        children.fields().forEachRemaining(entry -> {
            result.put(entry.getKey(), entry.getValue().get("_value").toString());
        });

        return result;
    }

    private Map<String, String> toMap(ObjectNode objectNode) {
        Map<String, String> result = new LinkedHashMap<>();
        objectNode.fields().forEachRemaining(entry -> {
            result.put(entry.getKey(), entry.getValue().toString());
        });

        return result;
    }
}

以上的反序列化器应该可以处理这两种格式。

英文:

Since old format is predefined and you know a structure of it you can implement custom deserialiser to handle old and new format at the same time. If errorBody JSON Object contains any of these keys: _children, _nodeFactory or _class you know it is an old format and you need to iterate over keys in _children JSON Object and get _value key to find a real value. Rest of keys and values you can ignore. Simple implementation could look like below:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.ToString;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class JsonMongo2FormatsApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File(&quot;./resource/test.json&quot;).getAbsoluteFile();
JsonMapper mapper = JsonMapper.builder().build();
Response response = mapper.readValue(jsonFile, Response.class);
System.out.println(response.getErrorBody());
}
}
@Data
@ToString
class Response {
@JsonDeserialize(using = ErrorMapJsonDeserializer.class)
private Map&lt;String, String&gt; errorBody;
}
class ErrorMapJsonDeserializer extends JsonDeserializer&lt;Map&lt;String, String&gt;&gt; {
@Override
public Map&lt;String, String&gt; 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&lt;String&gt; oldFormatKeys = Arrays.asList(&quot;_children&quot;, &quot;_nodeFactory&quot;, &quot;_class&quot;);
final Iterator&lt;String&gt; iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map&lt;String, String&gt; deserialize(ObjectNode root) {
JsonNode children = root.get(&quot;_children&quot;);
Map&lt;String, String&gt; result = new LinkedHashMap&lt;&gt;();
children.fields().forEachRemaining(entry -&gt; {
result.put(entry.getKey(), entry.getValue().get(&quot;_value&quot;).toString());
});
return result;
}
private Map&lt;String, String&gt; toMap(ObjectNode objectNode) {
Map&lt;String, String&gt; result = new LinkedHashMap&lt;&gt;();
objectNode.fields().forEachRemaining(entry -&gt; {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}

Above deserialiser should handle both formats.

答案2

得分: 1

根据我理解您的问题,对于第一个模型,您在保存或读取数据库方面并没有问题,但是一旦您想要提取这些数据,您注意到输出难以阅读。因此,您的问题是提取一个易于阅读的输出,您不需要更改第一个模型,而是需要扩展这些类并覆盖 toString 方法以在提取时更改其行为。

至少有三个类需要扩展:

  • TextNode:您无法覆盖 toString 方法,因此定制的类只会打印该值。

  • ObjectNode:我可以看到该类内至少有四个字段,您想要提取这些字段的值:codemessagedescription。它们都是 TextNode 类型,因此您可以将它们替换为它们的扩展类。然后,覆盖 toString 方法,使其为每个字段打印 fieldName: field.toString()

  • JsonNode:然后,您可以扩展此类并使用上面创建的定制类,覆盖 toString 方法,以按您的要求进行打印,并在通用 JsonNode 的位置使用它。

以这种方式工作将使您避免更改保存或读取数据的方式,而只需在视图上提取数据。

您可以将其视为SOLID原则的一小部分,特别是OCP(开闭原则:避免更改类的行为,而是扩展它以创建自定义行为)和LSP(Liskov 替换原则:子类型必须能够替代其基本类型的行为)。

英文:

As I understood your issue, with the first model, you didn't really have a problem to save or to read in database but, once you wanted to fetch these datas, you noticed that the output is difficult to read. So your problem is to fetch a well readable output then you don't need to change the first model but to extends these classes and overide the toString method to change its behavior while fetching.

There are at least three classes to extends:

  • TextNode : you can't overide the toString method do that the custom class just print the value

  • ObjectNode : I can see that there are at least four field inside this class that you want to fecth the value: code, message, description. They are type of TextNode so you can replace them by thier extended classes. Then overide the toString method so that It print fieldName: field.toString() for each field

  • JsonNode : You can then extend this class and use the custom classes created above, overide the toString method so that It print as you want and use It instead of the common JsonNode

To work like that will make you avoid the way you save or you read the datas but just to fecth on the view.

You can consider it as a little part of the SOLID principle especially the OCP (Open an close principle: avoid to change the class behavoir but extends it to create a custom behavior) and the LSP (Liskov Substitution Principle: Subtypes must be behaviorlly substituable for thier base types).

答案3

得分: 0

Michal Ziober的回答并没有完全解决问题,因为我们需要告诉SpringData MongoDb我们希望它使用自定义的反序列化器(在模型上加注释在Spring data mongodb中不起作用):

  1. 定义自定义反序列化器
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()) {
// 忽略除JSON对象外的所有内容
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;
}
}
  1. 创建自定义Mongo转换器并将其传递给自定义反序列化器。
    实际上,我们不直接传递序列化器,而是通过配置了该自定义反序列化器的ObjectMapper来传递。
public class CustomMappingMongoConverter extends MappingMongoConverter {
// 在实例化期间将传递的已配置objectMapper
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);
}
}
// 如果您也想使用自定义objectMapper进行序列化
@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));
}
}
  1. 创建和配置对象映射器,然后实例化自定义的MongoMappingConverter并将其添加到Mongo配置中。
public class MongoConfiguration extends AbstractMongoClientConfiguration {
// ... 其他配置方法bean
@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);
}
}
英文:

Michal Ziober's answer did not completely solve the problem as we need to tell SpringData MongoDb that we want it to use the custom deserializer
(Annotating the model does not work with Spring data mongodb):

  1. Define the custom deserializer
public class ErrorMapJsonDeserializer extends JsonDeserializer&lt;Map&lt;String, Object&gt;&gt; {
@Override
public Map&lt;String, Object&gt; 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&lt;String&gt; oldFormatKeys = Arrays.asList(&quot;_children&quot;, &quot;_nodeFactory&quot;, &quot;_class&quot;);
final Iterator&lt;String&gt; iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map&lt;String, Object&gt; deserialize(ObjectNode root) {
JsonNode children = root.get(&quot;_children&quot;);
if (children.isArray()) {
children = children.get(0);
children = children.get(&quot;_children&quot;);
}
return extractValues(children);
}
private Map&lt;String, Object&gt; extractValues(JsonNode children) {
Map&lt;String, Object&gt; result = new LinkedHashMap&lt;&gt;();
children.fields().forEachRemaining(entry -&gt; {
String key = entry.getKey();
if (!key.equals(&quot;_class&quot;))
result.put(key, entry.getValue().get(&quot;_value&quot;).toString());
});
return result;
}
private Map&lt;String, Object&gt; toMap(ObjectNode objectNode) {
Map&lt;String, Object&gt; result = new LinkedHashMap&lt;&gt;();
objectNode.fields().forEachRemaining(entry -&gt; {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
  1. Create a Custom mongo converter and pass it the custom deserializer.

Actually we do not pass the serializer directly but by means of an ObjectMapper configured with that Custom deserializer

public class CustomMappingMongoConverter extends MappingMongoConverter {
//The configured objectMapper that will be passed during instantiation
private ObjectMapper objectMapper; 
public CustomMappingMongoConverter(DbRefResolver dbRefResolver, MappingContext&lt;? extends MongoPersistentEntity&lt;?&gt;, MongoPersistentProperty&gt; mappingContext, ObjectMapper objectMapper) {
super(dbRefResolver, mappingContext);
this.objectMapper = objectMapper;
}
@Override
public &lt;S&gt; S read(Class&lt;S&gt; 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));
}
}
  1. Create and configure the object mapper then instantiate the custom MongoMappingConverter and add it to Mongo configurations
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);
}
}

huangapple
  • 本文由 发表于 2020年10月12日 18:47:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/64316345.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定