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

时间:2020-10-12 10:47:20

标签: java spring mongodb jackson spring-data-mongodb

我们正在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从事他的工作时,将读取转换器。

我们不能仅仅擦除或忽略旧数据,因为我们也需要阅读它们以研究它们。

我们如何设置不会读取旧格式的自定义阅读器?

3 个答案:

答案 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):

  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()) {
            // 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. 创建一个自定义mongo转换器,并向其提供自定义解串器。

实际上,我们不是直接向他提供序列化程序,而是通过配置了该自定义反序列化程序的 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. 创建并配置对象映射器,然后实例化自定义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);
    }
}