简体   繁体   English

如何从 Mongo DB 读取 com.fasterxml.jackson.databind.node.TextNode 并转换为 Map<String, Object> ?

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

We are using SpringDataMongoDB in a Spring-boot app to manage our data.我们在 Spring-boot 应用程序中使用 SpringDataMongoDB 来管理我们的数据。

Our previous model was this:我们之前的模型是这样的:

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

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

Which saved documents like so in the DB:在数据库中保存了这样的文件:

"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"
     },
  ...
 }

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<String,Object> errorBody;
    //...
}

The data are now saved like so:数据现在保存如下:

"response": {
  ...
        "errorBody": {
          "code": "Error code value",
          "message": "Error message value",
          "description": "Error description value",
          ...
        },
  ...
 }

Which, as you may have noticed is pretty much more simple.正如您可能已经注意到的那样,这要简单得多。

When reading the data, ex: repository.findAll()读取数据时,例如: 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.当然, TextNode类有一个以v作为参数的构造函数,但属性名称是_value并且ObjectNode没有默认构造函数:我们根本无法更改它。

We've created custom converters that we've added to our configurations.我们创建了自定义转换器,并将其添加到我们的配置中。

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);
        }
    }
}

We did the same for TextNode我们对TextNode做了同样的TextNode

But we still got the errors.但是我们仍然得到了错误。

The converters are read as we have a ZonedDateTimeConverter that is doing his job.转换器被读取,因为我们有一个ZonedDateTimeConverter正在做他的工作。

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 ?我们如何设置一个不会失败读取旧格式的自定义阅读器?

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.如果errorBody JSON Object包含以下任何键: _children_nodeFactory_class您知道它是旧格式,您需要迭代_children JSON Object键并获取_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("./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;
    }
}

Above deserialiser should handle both formats.上面的解串器应该处理这两种格式。

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.因此,您的问题是获取可读性良好的输出,然后您不需要更改第一个模型,而是扩展这些类并覆盖toString方法以在获取时更改其行为。

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 TextNode :您不能覆盖 toString 方法,因为自定义类只打印值

  • ObjectNode : I can see that there are at least four field inside this class that you want to fecth the value: code , message , description . ObjectNode :我可以看到这个类中至少有四个字段要赋值: codemessagedescription They are type of TextNode so you can replace them by thier extended classes.它们是TextNode 的类型,因此您可以用它们的扩展类替换它们。 Then overide the toString method so that It print fieldName: field.toString() for each field然后覆盖toString方法,以便它为每个字段打印fieldName: field.toString()

  • 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 JsonNode :然后您可以扩展此类并使用上面创建的自定义类,覆盖toString方法,以便它根据需要打印并使用它而不是常见的 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).您可以将其视为SOLID原则的一小部分,尤其是OCP (打开关闭原则:避免更改类行为但将其扩展为创建自定义行为)和LSP (Liskov 替换原则:子类型必须在行为上可替代他们的基本类型)。

Michal Ziober answer was not completely solving the problem as we need to tell SpringData MongoDb that we want him to use the custom deserializer (Annotating the model does not work with Spring data mongodb): Michal Ziober 的回答并没有完全解决问题,因为我们需要告诉 SpringData MongoDb 我们希望他使用自定义反序列化器(注释模型不适用于 Spring 数据 mongodb):

  1. Define the custom deserializer定义自定义解串器
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;
    }
}
  1. Create a Custom mongo converter and feed him with the custom deserializer.创建一个自定义 mongo 转换器并使用自定义反序列化器提供给他。

Actually we do not feed him with the serializer directly but by the mean of an ObjectMapper configured with that Custom deserializer实际上我们并没有直接给他提供序列化器,而是通过配置了自定义反序列化器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));
    }

}
  1. Create and configure the object mapper then instantiate the custom MongoMappingConverter and add it to Mongo configurations创建并配置对象映射器,然后实例化自定义 MongoMappingConverter 并将其添加到 Mongo 配置中
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);
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 Jackson com.fasterxml.jackson.databind.exc.MismatchedInputException 空字符串 - Jackson com.fasterxml.jackson.databind.exc.MismatchedInputException with empty string Jackson 对象映射器 com.fasterxml.jackson.databind.exc.MismatchedInputException - Jackson Object Mapper com.fasterxml.jackson.databind.exc.MismatchedInputException 无法读取文件:N / A com.fasterxml.jackson.databind.JsonMappingException - Could not read document: N/A com.fasterxml.jackson.databind.JsonMappingException 使用com.fasterxml.jackson.databind.JsonNode从Java中的json数组解析json对象; - parse json object from json array in java using com.fasterxml.jackson.databind.JsonNode; com.fasterxml.jackson.databind.JsonMappingException,同时将Json转换为Object - com.fasterxml.jackson.databind.JsonMappingException while converting Json to Object 如何使用 com.fasterxml.jackson.databind.ObjectMapper 解析和对象包含在数字数组中 - How to parse and object that have inside an array of numbers using com.fasterxml.jackson.databind.ObjectMapper 如何解决(在 com.fasterxml.jackson.databind)错误 - How to solve (at com.fasterxml.jackson.databind) error 如何解决 com.fasterxml.jackson.databind.ser 错误 - How To Solve com.fasterxml.jackson.databind.ser Error 如何解决com.fasterxml.jackson.databind.JsonMappingException? - How to solve the com.fasterxml.jackson.databind.JsonMappingException? 使用 Jackson 库将字符串转换为 Java 对象时,线程主 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException 中的异常 - Exception in thread main com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException while converting string to Java object using Jackson library
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM