#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>
вместо justObject
. Это сложнее :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"
и получить данные отдельно для каждого типа. С помощью этого вы можете легко конвертировать в каждый тип отдельно.