将Java中的非结构化对象转换

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

Converting the unstructured object in java

问题

我正在使用MongoDb来处理非结构化文档。在执行聚合操作时,我得到的最终输出是非结构化对象。为了方便起见,我会提供一些示例数据。实际的对象有很多字段。

例如:

[
    { _id : "1", type: "VIDEO", videoUrl : "youtube.com/java"},
    { _id : "2", type: "DOCUMENT", documentUrl : "someurl.com/spring-boot-pdf"},
    { _id : "3", type: "ASSESSMENT", marks : 78}
]

上述对象类型的相应类是:

@Data
public class Video{
    private String _id;
    private String type;
    private String videoUrl;
}

@Data
public class Document{
    private String _id;
    private String type;
    private String documentUrl;
}

@Data
public class Assessment{
    private String _id;
    private String type;
    private Integer marks;
}

由于我无法指定转换器类,我得到的是所有对象的Object.class的列表,这是一个通用类型。

List<Object> list = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(YOUR_COLLECTION.class), Object.class).getMappedResults();

虽然它能够工作,但对于后端和前端开发人员(例如:Swagger UI)来说,这不够可读且难以维护。因此,我提出了一个解决方案,将所有字段放在一个类中。

@Data
@JsonInclude(JsonInclude.Include.NON_NULL) 
class MyConverter{
    private String _id;
    private String type;
    private String videoUrl;
    private String documentUrl;
    private Integer marks;
}

在这里,Jackson有助于忽略所有空字段。

现在我可以将MyConverter用作类型。

List<MyConverter> list = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(YOUR_COLLECTION.class), MyConverter.class).getMappedResults();

但我觉得这不是在实现标准应用程序时的良好实践。我想知道是否有任何方法可以避免通用类型类(例如:扩展任何抽象类)?或者这是我唯一的选择吗?

英文:

I'm using MongoDb for unstructured documents. When I do the aggregations, I'm getting final output as unstructured objects. I post some sample data for the easiness. Actual objects have many fields.
Eg :

[
	{ _id : &quot;1&quot;, type: &quot;VIDEO&quot;, videoUrl : &quot;youtube.com/java&quot;},
	{ _id : &quot;2&quot;, type: &quot;DOCUMENT&quot;, documentUrl : &quot;someurl.com/spring-boot-pdf&quot;},
	{ _id : &quot;3&quot;, type: &quot;ASSESSMENT&quot;, marks : 78}
]

The respective class for the types of above objects are

@Data
public class Video{
	private String _id;
	private String type;
	private String videoUrl;
}

@Data
public class Document{
	private String _id;
	private String type;
	private String documentUrl;
}

@Data
public class Assessment{
	private String _id;
	private String type;
	private Integer marks;
}

Since I can't specify the converter class, I get all objects as list of Object.class which is a general type for all.

List&lt;Object&gt; list = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(YOUR_COLLECTION.class), Object.class).getMappedResults();

It's working, but this is not readable and not maintainable for backend and front-end developers (eg : swagger ui). So I came up with a solution, that put all fields as a class.

@Data
@JsonInclude(JsonInclude.Include.NON_NULL) 
class MyConvetor{
	private String _id;
	private String type;
	private String videoUrl;
	private String documentUrl;
	private Integer marks;
}

Here Jackson helps to ignore all null fields

Now I can use MyConverter as Type

List&lt;MyConverter&gt; list = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(YOUR_COLLECTION.class), MyConverter.class).getMappedResults();

But I feel this is not a good practice when we implementing a standard application. I'd like to know, is there any way to avoid the general type class (e.g. extending any abstract class)? Or is this the only way I can do?

答案1

得分: 2

我不确定MongoDB在Java中是否提供了这种按字段进行动态转换的功能(需要指定哪个字段和哪个类)。但你可以手动实现。

首先,你需要为匹配字符串到类的操作定义类型(枚举值或某个映射)。你可以创建一个抽象的父类(例如 TypedObject)来更容易地使用和绑定所有目标类(VideoDocumentAssessment)。

接下来,你需要读取和映射Mongo中的值,因为你想在代码中读取所有数据。Object 是可以的,但我建议使用 Map<String, Object>(实际上你的 Object 就是这个Map - 你可以通过调用 list.get(0).toString() 来验证)。你也可以映射到 StringDBObject 或一些JSON对象 - 你需要手动读取 &quot;type&quot; 字段并从对象中获取所有数据。

最后,你可以将“数据包”(在我的示例中是 Map<String, Object>)转换为目标类。

现在,你可以按目标类使用转换后的对象。为了证明这些实际上是目标类,我使用 toString 打印所有字段的对象。

示例实现

类:

@Data
public abstract class TypedObject {
    private String _id;
    private String type;
}

@Data
@ToString(callSuper = true)
public class Video extends TypedObject {
    private String videoUrl;
}

@Data
@ToString(callSuper = true)
public class Document extends TypedObject {
    private String documentUrl;
}

@Data
@ToString(callSuper = true)
public class Assessment extends TypedObject {
    private Integer marks;
}

用于将字符串类型映射到类的枚举:

@RequiredArgsConstructor
public enum Type {
    VIDEO("VIDEO", Video.class),
    DOCUMENT("DOCUMENT", Document.class),
    ASSESSMENT("ASSESSMENT", Assessment.class);

    private final String typeName;
    private final Class<? extends TypedObject> clazz;

    public static Class<? extends TypedObject> getClazz(String typeName) {
        return Arrays.stream(values())
                .filter(type -> type.typeName.equals(typeName))
                .findFirst()
                .map(type -> type.clazz)
                .orElseThrow(IllegalArgumentException::new);
    }
}

将JSON数据转换为目标类的方法:

private static TypedObject toClazz(Map<String, Object> objectMap, ObjectMapper objectMapper) {
    Class<? extends TypedObject> type = Type.getClazz(objectMap.get("type").toString());
    return objectMapper.convertValue(objectMap, type);
}

读取JSON数据为“数据包”并使用上述方法:

String json = "[\n" +
        "    { _id : \"1\", type: \"VIDEO\", videoUrl : \"youtube.com/java\"},\n" +
        "    { _id : \"2\", type: \"DOCUMENT\", documentUrl : \"someurl.com/spring-boot-pdf\"},\n" +
        "    { _id : \"3\", type: \"ASSESSMENT\", marks : 78}\n" +
        "]";

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);

List<Map<String, Object>> readObjects = objectMapper.readValue(json, new TypeReference<>() {});

for (Map<String, Object> readObject : readObjects) {
    TypedObject convertedObject = toClazz(readObject, objectMapper);
    System.out.println(convertedObject);
}

备注:

  • 在示例中,我使用Jackson的ObjectMapper来读取JSON。这使得示例和测试更简单。我认为你可以将其替换为mongoTemplate.aggregate()。但无论如何,在toClazz方法中我仍然需要ObjectMapper来转换“数据包”。
  • 我使用Map<String, Object>而不是只用Object。这更加复杂:List<Map<String, Object>> readObjects = objectMapper.readValue(json, new TypeReference<>() {});。如果你愿意,你可以像这样做:List<Object> readObjects2 = (List<Object>) objectMapper.readValue(json, new TypeReference<List<Object>>() {});

结果:

Video(super=TypedObject(_id=1, type=VIDEO), videoUrl=youtube.com/java)
Document(super=TypedObject(_id=2, type=DOCUMENT), documentUrl=someurl.com/spring-boot-pdf)
Assessment(super=TypedObject(_id=3, type=ASSESSMENT), marks=78)

当然,你可以将TypedObject转换为你需要的目标类(我建议在转换前检查instance of),然后使用:

Video video = (Video) toClazz(readObjects.get(0), objectMapper);
System.out.println(video.getVideoUrl());

我假设你一次读取整个集合,并且你在一个列表中获取了所有混合在一起的类型(与你问题中的示例类似)。但你也可以尝试通过字段&quot;type&quot;在MongoDB中查找文档,并为每种类型单独获取数据。这样,你可以轻松地分别转换为每种类型。

英文:

I don't think so (or I don't know) if MongoDB in Java provides this kind of dynamic conversion by some field (it would require specify what field and what classes). But you can do it by hand.

First, you need to define your types (enum values or some map) for matching string to class. You can create abstract parent class (eg. TypedObject) for easier usage and binding all target classes (Video, Document, Assessment) .

Next you have to read and map values from Mongo to anything because you want to read all data in code. Object is good but I recommend Map&lt;String, Object&gt; (your Object actually is that Map - you can check it by invoking list.get(0).toString()). You can also map to String or DBObject or some JSON object - you have to read &quot;type&quot; field by hand and get all data from object.

At the end you can convert "bag of data" (Map&lt;String, Object&gt; in my example) to target class.

Now you can use converted objects by target classes. For proving these are actually target classes I print objects with toString all fields.

Example implementation

Classes:

@Data
public abstract class TypedObject {
    private String _id;
    private String type;
}

@Data
@ToString(callSuper = true)
public class Video extends TypedObject {
    private String videoUrl;
}

@Data
@ToString(callSuper = true)
public class Document extends TypedObject {
    private String documentUrl;
}

@Data
@ToString(callSuper = true)
public class Assessment extends TypedObject {
    private Integer marks;
}

Enum for mapping string types to classes:

@RequiredArgsConstructor
public enum Type {
    VIDEO(&quot;VIDEO&quot;, Video.class),
    DOCUMENT(&quot;DOCUMENT&quot;, Document.class),
    ASSESSMENT(&quot;ASSESSMENT&quot;, Assessment.class);

    private final String typeName;
    private final Class&lt;? extends TypedObject&gt; clazz;

    public static Class&lt;? extends TypedObject&gt; getClazz(String typeName) {
        return Arrays.stream(values())
                .filter(type -&gt; type.typeName.equals(typeName))
                .findFirst()
                .map(type -&gt; type.clazz)
                .orElseThrow(IllegalArgumentException::new);
    }
}

Method for converting "bag of data" from JSON to your target class:

    private static TypedObject toClazz(Map&lt;String, Object&gt; objectMap, ObjectMapper objectMapper) {
        Class&lt;? extends TypedObject&gt; type = Type.getClazz(objectMap.get(&quot;type&quot;).toString());
        return objectMapper.convertValue(objectMap, type);
    }

Read JSON to "bags of data" and use of the above:

    String json = &quot;[\n&quot; +
            &quot;    { _id : \&quot;1\&quot;, type: \&quot;VIDEO\&quot;, videoUrl : \&quot;youtube.com/java\&quot;},\n&quot; +
            &quot;    { _id : \&quot;2\&quot;, type: \&quot;DOCUMENT\&quot;, documentUrl : \&quot;someurl.com/spring-boot-pdf\&quot;},\n&quot; +
            &quot;    { _id : \&quot;3\&quot;, type: \&quot;ASSESSMENT\&quot;, marks : 78}\n&quot; +
            &quot;]&quot;;

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);

    List&lt;Map&lt;String, Object&gt;&gt; readObjects = objectMapper.readValue(json, new TypeReference&lt;&gt;() {});

    for (Map&lt;String, Object&gt; readObject : readObjects) {
        TypedObject convertedObject = toClazz(readObject, objectMapper);
        System.out.println(convertedObject);
    }

Remarks:

  • In example I use Jackson ObjectMapper for reading JSON. This makes the example and testing simpler. I think you can replace it with mongoTemplate.aggregate(). But anyway I need ObjectMapper in toClazz method for converting "bags of data".
  • I use Map&lt;String, Object&gt; instead of just Object. It is more complicated: List&lt;Map&lt;String, Object&gt;&gt; readObjects = objectMapper.readValue(json, new TypeReference&lt;&gt;() {});. If you want, you can do something like this: List&lt;Object&gt; readObjects2 = (List&lt;Object&gt;) objectMapper.readValue(json, new TypeReference&lt;List&lt;Object&gt;&gt;() {});

Result:

Video(super=TypedObject(_id=1, type=VIDEO), videoUrl=youtube.com/java)
Document(super=TypedObject(_id=2, type=DOCUMENT), documentUrl=someurl.com/spring-boot-pdf)
Assessment(super=TypedObject(_id=3, type=ASSESSMENT), marks=78)

Of course you can cast TypedObject to target class you need (I recommend checking instance of before casting) and use:

Video video = (Video) toClazz(readObjects.get(0), objectMapper);
System.out.println(video.getVideoUrl());

I assumed you read whole collection once and you get all types mixed up in one list (as in example in your question). But you can try find documents in MongoDB by field &quot;type&quot; and get data separately for each of type. With this you can easily convert to each type separately.

huangapple
  • 本文由 发表于 2020年10月11日 02:41:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/64296978.html
匿名

发表评论

匿名网友

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

确定