Spring Data MongoDB: доступ и обновление поддокументов
Первые эксперименты с Spring Data и MongoDB были великолепными. Теперь у меня есть следующая структура (упрощенно):
public class Letter {
@Id
private String id;
private List<Section> sections;
}
public class Section {
private String id;
private String content;
}
Загрузка и сохранение целых объектов / документов Letter работает как шарм. (Я использую ObjectId для генерации уникальных идентификаторов для поля Section.id.)
Letter letter1 = mongoTemplate.findById(id, Letter.class)
mongoTemplate.insert(letter2);
mongoTemplate.save(letter3);
Поскольку документы имеют большой размер (200 КБ), а иногда приложению требуются только отдельные части: есть ли возможность запросить дополнительный документ (раздел), изменить и сохранить его? Я хотел бы реализовать такой метод, как
Section s = findLetterSection(letterId, sectionId);
s.setText("blubb");
replaceLetterSection(letterId, sectionId, s);
И конечно методы как:
addLetterSection(letterId, s); // add after last section
insertLetterSection(letterId, sectionId, s); // insert before given section
deleteLetterSection(letterId, sectionId); // delete given section
Я вижу, что последние три метода несколько "странные", то есть загрузка всего документа, изменение коллекции и ее повторное сохранение может быть лучшим подходом с объектно-ориентированной точки зрения; но первый вариант использования ("переход" к вложенному документу / подобъекту и работа в области действия этого объекта) кажется естественным.
Я думаю, что MongoDB может обновлять поддокументы, но можно ли использовать SpringData для сопоставления объектов? Спасибо за любые указатели.
3 ответа
Я понял следующий подход для нарезки и загрузки только одного подобъекта. Кажется ли это нормально? Я знаю о проблемах с одновременными изменениями.
Query query1 = Query.query(Criteria.where("_id").is(instance));
query1.fields().include("sections._id");
LetterInstance letter1 = mongoTemplate.findOne(query1, LetterInstance.class);
LetterSection emptySection = letter1.findSectionById(sectionId);
int index = letter1.getSections().indexOf(emptySection);
Query query2 = Query.query(Criteria.where("_id").is(instance));
query2.fields().include("sections").slice("sections", index, 1);
LetterInstance letter2 = mongoTemplate.findOne(query2, LetterInstance.class);
LetterSection section = letter2.getSections().get(0);
Это альтернативное решение, загружающее все разделы, но пропускающее другие (большие) поля.
Query query = Query.query(Criteria.where("_id").is(instance));
query.fields().include("sections");
LetterInstance letter = mongoTemplate.findOne(query, LetterInstance.class);
LetterSection section = letter.findSectionById(sectionId);
Это код, который я использую для хранения только одного элемента коллекции:
MongoConverter converter = mongoTemplate.getConverter();
DBObject newSectionRec = (DBObject)converter.convertToMongoType(newSection);
Query query = Query.query(Criteria.where("_id").is(instance).and("sections._id").is(new ObjectId(newSection.getSectionId())));
Update update = new Update().set("sections.$", newSectionRec);
mongoTemplate.updateFirst(query, update, LetterInstance.class);
Приятно видеть, как Spring Data можно использовать с "частичными результатами" из MongoDB.
Любые комментарии высоко ценятся!
Я думаю, что ответ Матиаса Ваттке хорош, для тех, кто ищет общую версию своего ответа, см. Код ниже:
@Service
public class MongoUtils {
@Autowired
private MongoTemplate mongo;
public <D, N extends Domain> N findNestedDocument(Class<D> docClass, String collectionName, UUID outerId, UUID innerId,
Function<D, List<N>> collectionGetter) {
// get index of subdocument in array
Query query = new Query(Criteria.where("_id").is(outerId).and(collectionName + "._id").is(innerId));
query.fields().include(collectionName + "._id");
D obj = mongo.findOne(query, docClass);
if (obj == null) {
return null;
}
List<UUID> itemIds = collectionGetter.apply(obj).stream().map(N::getId).collect(Collectors.toList());
int index = itemIds.indexOf(innerId);
if (index == -1) {
return null;
}
// retrieve subdocument at index using slice operator
Query query2 = new Query(Criteria.where("_id").is(outerId).and(collectionName + "._id").is(innerId));
query2.fields().include(collectionName).slice(collectionName, index, 1);
D obj2 = mongo.findOne(query2, docClass);
if (obj2 == null) {
return null;
}
return collectionGetter.apply(obj2).get(0);
}
public void removeNestedDocument(UUID outerId, UUID innerId, String collectionName, Class<?> outerClass) {
Update update = new Update();
update.pull(collectionName, new Query(Criteria.where("_id").is(innerId)));
mongo.updateFirst(new Query(Criteria.where("_id").is(outerId)), update, outerClass);
}
}
Это можно назвать, например, используя
mongoUtils.findNestedDocument(Shop.class, "items", shopId, itemId, Shop::getItems);
mongoUtils.removeNestedDocument(shopId, itemId, "items", Shop.class);
Domain
Интерфейс выглядит так:
public interface Domain {
UUID getId();
}
Примечание: если конструктор вложенного документа содержит элементы с примитивным типом данных, важно, чтобы вложенный документ имел конструктор по умолчанию (пустой), который может быть защищен, чтобы класс мог быть инстанцируемым с нулевыми аргументами.
Решение
Это мое решение этой проблемы:
Объект должен быть обновлен
@Getter
@Setter
@Document(collection = "projectchild")
public class ProjectChild {
@Id
private String _id;
private String name;
private String code;
@Field("desc")
private String description;
private String startDate;
private String endDate;
@Field("cost")
private long estimatedCost;
private List<String> countryList;
private List<Task> tasks;
@Version
private Long version;
}
Кодирование решения
public Mono<ProjectChild> UpdateCritTemplChild(
String id, String idch, String ownername) {
Query query = new Query();
query.addCriteria(Criteria.where("_id")
.is(id)); // find the parent
query.addCriteria(Criteria.where("tasks._id")
.is(idch)); // find the child which will be changed
Update update = new Update();
update.set("tasks.$.ownername", ownername); // change the field inside the child that must be updated
return template
// findAndModify:
// Find/modify/get the "new object" from a single operation.
.findAndModify(
query, update,
new FindAndModifyOptions().returnNew(true), ProjectChild.class
)
;
}