Преобразование неструктурированного объекта в java

#java #spring-boot #spring-data #spring-data-mongodb

#java #spring-boot #spring-данные #spring-data-mongodb

Вопрос:

Я использую 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 MyConvetor{
    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();
  

Но я чувствую, что это не очень хорошая практика, когда мы реализуем стандартное приложение. Я хотел бы знать, есть ли какой-либо способ избежать класса общего типа (например, расширения любого абстрактного класса)? Или это единственный способ, которым я могу это сделать?

Комментарии:

1. Ваши данные уже были помечены как неструктурированные, что, на мой взгляд, является проблемой, большей, чем общий тип данных, отправляемый вашим клиентам. Кроме того, я думаю, что ваши клиенты получают правильно заданную структуру данных, особенно. с type полем, определяющим тип содержимого. Вы можете рассматривать @JsonInclude(JsonInclude.Include.NON_NULL) как механизм сериализации, даже если он действительно привносит запах кода в контекст или требование. Однако вы можете изменить одну вещь: сделать videoUrl , documentUrl , и marks частью составного типа в таких полях, как , и т.д. videoDetails

2. @ernest_k это имеет смысл. Но я только что опубликовал пример полей только для удобства. Это была моя ошибка, которую я еще не включил. Каждый мой класс имеет более 20 полей. Итак, я не могу создавать составные поля, нет? В этом проблема

Ответ №1:

Я так не думаю (или я не знаю), предоставляет ли MongoDB в Java такое динамическое преобразование по некоторому полю (для этого потребуется указать, какое поле и какие классы). Но вы можете сделать это вручную.

Во-первых, вам нужно определить свои типы (значения перечисления или некоторую карту) для сопоставления строки с классом. Вы можете создать абстрактный родительский класс (например. TypedObject ) для упрощения использования и привязки всех целевых классов ( Video , Document , Assessment ) .

Затем вам нужно прочитать и сопоставить значения из Mongo с чем угодно, потому что вы хотите прочитать все данные в коде. Object это хорошо, но я рекомендую Map<String, Object> ( Object на самом деле это та карта — вы можете проверить ее, вызвав list.get(0).toString() ). Вы также можете сопоставить String или DBObject или какой-либо объект JSON — вам нужно прочитать "type" поле вручную и получить все данные из объекта.

В конце вы можете преобразовать «пакет данных» ( 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() . Но в любом случае мне нужен ObjectMapper toClazz метод для преобразования «пакетов данных».
  • Я использую Map<String, Object> вместо just 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());
  

Я предположил, что вы прочитали всю коллекцию один раз, и все типы смешались в одном списке (как в примере в вашем вопросе). Но вы можете попробовать найти документы в MongoDB по полю "type" и получить данные отдельно для каждого типа. С помощью этого вы можете легко конвертировать в каждый тип отдельно.