Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Data MongoDB: Accessing and updating sub documents

First experiments with Spring Data and MongoDB were great. Now I've got the following structure (simplified):

public class Letter {
  @Id
  private String id;
  private List<Section> sections;
}

public class Section {
  private String id;
  private String content;
}

Loading and saving entire Letter objects/documents works like a charm. (I use ObjectId to generate unique IDs for the Section.id field.)

Letter letter1 = mongoTemplate.findById(id, Letter.class)
mongoTemplate.insert(letter2);
mongoTemplate.save(letter3);

As documents are big (200K) and sometimes only sub-parts are needed by the application: Is there a possibility to query for a sub-document (section), modify and save it? I'd like to implement a method like

Section s = findLetterSection(letterId, sectionId);
s.setText("blubb");
replaceLetterSection(letterId, sectionId, s);

And of course methods like:

addLetterSection(letterId, s); // add after last section
insertLetterSection(letterId, sectionId, s); // insert before given section
deleteLetterSection(letterId, sectionId); // delete given section

I see that the last three methods are somewhat "strange", i.e. loading the entire document, modifying the collection and saving it again may be the better approach from an object-oriented point of view; but the first use case ("navigating" to a sub-document/sub-object and working in the scope of this object) seems natural.

I think MongoDB can update sub-documents, but can SpringData be used for object mapping? Thanks for any pointers.

like image 371
Matthias Wuttke Avatar asked Aug 17 '12 12:08

Matthias Wuttke


People also ask

What is the difference between MongoOperations and MongoTemplate?

MongoTemplate provides a simple way for you to save, update, and delete your domain objects and map those objects to documents stored in MongoDB. You can save, update and delete the object as shown below. MongoOperations is the interface that MongoTemplate implements.

Can we use MongoTemplate and MongoRepository?

We can even utilize both of them in our programming practice as per our need and for performance enhancements. Moreover, MongoTemplate can offer an easier step to write a complex query than MongoRepository. Even some people in the industry also find that MongoTemplate is a better choice to write a complex query easily.

Which is better MongoTemplate or MongoRepository?

MongoTemplate is a bit more lower level where you need to write your own queries. With embedded documents and denormalization it can be easier to write complex queries with MongoTemplate. For simple things I would use MongoRepository. I've seen some examples where both are used together in a hybrid approach.


2 Answers

I figured out the following approach for slicing and loading only one subobject. Does it seem ok? I am aware of problems with concurrent modifications.

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);

This is an alternative solution loading all sections, but omitting the other (large) fields.

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);

This is the code I use for storing only a single collection element:

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);

It is nice to see how Spring Data can be used with "partial results" from MongoDB.

Any comments highly appreciated!

like image 189
Matthias Wuttke Avatar answered Sep 27 '22 20:09

Matthias Wuttke


I think Matthias Wuttke's answer is great, for anyone looking for a generic version of his answer see code below:

@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);
  }
}

This could for example be called using

mongoUtils.findNestedDocument(Shop.class, "items", shopId, itemId, Shop::getItems);
mongoUtils.removeNestedDocument(shopId, itemId, "items", Shop.class);

The Domain interface looks like this:

public interface Domain {

  UUID getId();
}

Notice: If the nested document's constructor contains elements with primitive datatype, it is important for the nested document to have a default (empty) constructor, which may be protected, in order for the class to be instantiatable with null arguments.

like image 20
user2035039 Avatar answered Sep 27 '22 21:09

user2035039