If you're looking to grab a nested document from your MongoDB aggregation pipeline, you should use the replaceRoot operation. Fortunately, that's easy to do with MongoTemplate.

In this guide, I'll show you how to make it happen.

Or you can just go get the code on GitHub.

Otherwise, read on.

The Business Requirements

Your boss Smithers walks into your office. He's depressed because his favorite badminton player lost a tournament last night.

"Got another requirement for you," he sighs. "It's about that CRM app you're working on."

You seriously think about suggesting that Smithers take an anti-depressant.

"We need to identify all sales owners by source," he continues between audible sniffles. "It shouldn't be too hard because you've already identified all contacts by source."

He gets up and walks out of your office without saying another word.

Just Another Stepping Stone

As Smithers mentioned, you've already done some work with queries in MongoDB. You created a service request that returned contacts by source

To fullfill that request, you used a MongoDB aggregation pipeline with MongoTemplate in Spring Boot.

This time, though, you need to do something similar. Except instead of returning contacts by source, you'll return sales owners by source.

And for that, you're going to need the replaceRoot aggregation operation.

What Is replaceRoot?

So what exactly is replaceRoot? It's an operation that takes a nested document and makes it the root document.

Let me explain by example.

Here's a typical contact in your contacts database:

{
   "_id":"ObjectId('5fde1ac084dad94dbb7f82ae')",
   "firstName":"R2D2",
   "lastName":"Droid",
   "email":"r2d2@xmail.com",
   "phones":[
      
   ],
   "addresses":[
      
   ],
   "source":"EMAIL",
   "status":"ACTIVE",
   "statusChange":NumberLong(0),
   "linesOfBusiness":[
      "JAVA_ENTERPRISE"
   ],
   "company":"For Luke",
   "title":"Droid",
   "authority":false,
   "salesOwner":{
      "_id":"ObjectId('5f78d8fbc1d3246ab4303f2b')",
      "firstName":"Darth",
      "lastName":"Vader",
      "email":"darth@xmail.com",
      "username":"darth",
      "phoneNumber":"474-555-1212"
   },
   "_class":"com.careydevelopment.contact.model.Contact"
}

Take a look at the embedded document "salesOwner." It includes a bunch of info about R2D2's sales owner, such as his name, email address, and phone number.

If you perform a replaceRoot operation on the whole contact document by making "salesOwner" the new root, then the resulting document looks like this:

   "salesOwner":{
      "_id":"ObjectId('5f78d8fbc1d3246ab4303f2b')",
      "firstName":"Darth",
      "lastName":"Vader",
      "email":"darth@xmail.com",
      "username":"darth",
      "phoneNumber":"474-555-1212"
   }

In other words, that's your new document. And for the purposes of this requirement, that's all you need.

The Replacements

Now take a look at how to make it happen. Reminder: if you need a refresher on how to get up to speed with MongoTemplate in a Spring Boot application, take a look at my guide on that subject.

Once you're done with that, come on back over here.

Now, let's assume that your data set in the contacts database looks like this:

[ {
  "id" : "5fde12d60ab013769b67cf02",
  "firstName" : "Chew",
  "lastName" : "Bacca",
  "email" : "chewie@xmail.com",
  "source" : "WEBSITE_FORM",
  "status" : "ACTIVE",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR" ],
  "company" : "Working for Han",
  "title" : "Wookie",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fde1028792009283c603929",
  "firstName" : "JarJar",
  "lastName" : "Binks",
  "email" : "jarjar@xmail.com",
  "addresses" : [ {
    "street1" : "1400 Plum Way",
    "city" : "Onisius",
    "state" : "NM",
    "zip" : "80909",
    "addressType" : "HOME"
  } ],
  "source" : "WALKIN",
  "status" : "CONTACTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "FULL_STACK" ],
  "company" : "None",
  "title" : "Comic Relief",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fde117edd79e20e3ff6528c",
  "firstName" : "Lando",
  "lastName" : "Calrissian",
  "email" : "lando@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "WORK",
    "countryCode" : "us"
  } ],
  "source" : "INBOUND_SALES_CALL",
  "status" : "CONTACTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR", "DEV_OPS" ],
  "company" : "Cloud City",
  "title" : "Friend",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fde1ac084dad94dbb7f82ae",
  "firstName" : "R2D2",
  "lastName" : "Droid",
  "email" : "r2d2@xmail.com",
  "source" : "EMAIL",
  "status" : "ACTIVE",
  "statusChange" : 0,
  "linesOfBusiness" : [ "JAVA_ENTERPRISE" ],
  "company" : "For Luke",
  "title" : "Droid",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fdd0af34e9d6806f369abf0",
  "firstName" : "Boba",
  "lastName" : "Fett",
  "email" : "boba@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "HOME",
    "countryCode" : "us"
  } ],
  "addresses" : [ {
    "street1" : "1222 Galaxy Way",
    "city" : "Alterion",
    "state" : "AR",
    "zip" : "22222",
    "country" : "US",
    "addressType" : "HOME"
  } ],
  "source" : "INBOUND_SALES_CALL",
  "status" : "CONTACTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "DEV_OPS" ],
  "company" : "Empire",
  "title" : "Bounty Hunter",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fdd0cedaac5f75d62564ee7",
  "firstName" : "Jabba",
  "lastName" : "Hutt",
  "email" : "jabba@xmail.com",
  "source" : "EMAIL",
  "status" : "NEW",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR" ],
  "company" : "Sandz",
  "title" : "Large",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fdd0e7c870ef4713e179384",
  "firstName" : "Princess",
  "lastName" : "Leia",
  "email" : "leia@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "WORK",
    "countryCode" : "us"
  } ],
  "source" : "WALKIN",
  "status" : "INTERESTED",
  "statusChange" : 0,
  "linesOfBusiness" : [ "JAVA_ENTERPRISE", "ANGULAR" ],
  "company" : "Republic",
  "title" : "Princess",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fd5f37bde602d3bacef69db",
  "firstName" : "Luke",
  "lastName" : "Skywalker",
  "email" : "luke@tat2.com",
  "source" : "EMAIL",
  "sourceDetails" : "He emailed me",
  "status" : "NEW",
  "statusChange" : 0,
  "linesOfBusiness" : [ "ANGULAR" ],
  "company" : "International Business",
  "title" : "President",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "5fdd0f2aea599836ca3ddbf1",
  "firstName" : "Han",
  "lastName" : "Solo",
  "email" : "han@xmail.com",
  "addresses" : [ {
    "street1" : "111 Millennium Way",
    "city" : "Nessy",
    "state" : "CO",
    "addressType" : "HOME"
  } ],
  "source" : "EMAIL",
  "status" : "ACTIVE",
  "statusChange" : 0,
  "title" : "Pirate",
  "authority" : false,
  "salesOwner" : {
    "id" : "5f78d8fbc1d3246ab4303f2b",
    "firstName" : "Darth",
    "lastName" : "Vader",
    "email" : "darth@xmail.com",
    "username" : "darth",
    "phoneNumber" : "474-555-1212"
  }
}, {
  "id" : "6005e87163660b7b2e6a16df",
  "firstName" : "Governor",
  "lastName" : "Tarkin",
  "email" : "tarkin@xmail.com",
  "phones" : [ {
    "phone" : "(555) 555-5555",
    "phoneType" : "CELL"
  } ],
  "addresses" : [ {
    "city" : "Home City",
    "state" : "MN",
    "addressType" : "HOME"
  } ],
  "source" : "EMAIL",
  "status" : "ACTIVE",
  "company" : "No Moon",
  "title" : "Governor",
  "authority" : true,
  "salesOwner" : {
    "id" : "6005e76bac127e0f5d9a6560",
    "firstName" : "The",
    "lastName" : "Emperor",
    "email" : "theemperor@xmail.com",
    "username" : "theemperor",
    "phoneNumber" : "(555) 555-5555"
  }
} ]

Parse through that and you'll see that there are a couple of different sales owners in the mix: The Emperor and Darth Vader.

Each of those sales owners has at least one contact with "EMAIL" as the source. So you should ultimately get a list of two sales owners.

With that in mind, update ContactService with a new method:

	public List<SalesOwner> findSalesOwnersBySource(Source source) {
		AggregationOperation match = Aggregation.match(Criteria.where("source").is(source));
		AggregationOperation replaceRoot = Aggregation.replaceRoot("salesOwner");
		
		Aggregation aggregation = Aggregation.newAggregation(match, replaceRoot);
		
		List<SalesOwner> owners = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Contact.class), SalesOwner.class).getMappedResults();
		
		return owners;
	}

The first operation should look familiar if you visited the guide on how to handle matching in aggregation pipelines. It simply returns all contacts where the source is set to "EMAIL."

But since you don't want contacts, you need that second stage: replaceRoot. That stage makes the "salesOwner" field (which you've already seen) the new root. Once that stage is finished, all you'll have is the sales owner document.

And that's what you want.

Oh, by the way: note that the mongoTemplate.aggregate() method is using SalesOwner.class instead of Contact.class as you may have seen in previous guides. That's because you're returning SalesOwner objects instead of Contact objects here.

Now put together some initialization code that runs that pipeline:

@Component
public class ApplicationListenerInitialize implements ApplicationListener<ApplicationReadyEvent>  {
	
	@Autowired
	private ContactService contactService;
	
	
    public void onApplicationEvent(ApplicationReadyEvent event) {    	
    	List<SalesOwner> owners = contactService.findSalesOwnersBySource(Source.EMAIL);
    	
    	try {
	    	ObjectMapper objectMapper = new ObjectMapper();
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
	    	System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(owners));
    	} catch (Exception e) {
    		e.printStackTrace();
    	}
    }
}

Save that and run your Spring Boot application. Once it's loaded, you should see the output in red:

[ {
  "id" : "5f78d8fbc1d3246ab4303f2b",
  "firstName" : "Darth",
  "lastName" : "Vader",
  "email" : "darth@xmail.com",
  "username" : "darth",
  "phoneNumber" : "474-555-1212"
}, {
  "id" : "5f78d8fbc1d3246ab4303f2b",
  "firstName" : "Darth",
  "lastName" : "Vader",
  "email" : "darth@xmail.com",
  "username" : "darth",
  "phoneNumber" : "474-555-1212"
}, {
  "id" : "5f78d8fbc1d3246ab4303f2b",
  "firstName" : "Darth",
  "lastName" : "Vader",
  "email" : "darth@xmail.com",
  "username" : "darth",
  "phoneNumber" : "474-555-1212"
}, {
  "id" : "5f78d8fbc1d3246ab4303f2b",
  "firstName" : "Darth",
  "lastName" : "Vader",
  "email" : "darth@xmail.com",
  "username" : "darth",
  "phoneNumber" : "474-555-1212"
}, {
  "id" : "6005e76bac127e0f5d9a6560",
  "firstName" : "The",
  "lastName" : "Emperor",
  "email" : "theemperor@xmail.com",
  "username" : "theemperor",
  "phoneNumber" : "(555) 555-5555"
} ]

Well, that technically works. But do you really need to see Darth Vader's name over and over again?

Nope.

And do you really need to return all the info about the sales owner? Probably not.

So update that method you just created as follows:

	public List<SalesOwner> findSalesOwnersBySource(Source source) {
		AggregationOperation match = Aggregation.match(Criteria.where("source").is(source));
		AggregationOperation replaceRoot = Aggregation.replaceRoot("salesOwner");
		AggregationOperation group = Aggregation.group("lastName", "firstName");
		
		Aggregation aggregation = Aggregation.newAggregation(match, replaceRoot, group);
		
		List<SalesOwner> owners = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Contact.class), SalesOwner.class).getMappedResults();
		
		return owners;
	}

Pay attention to that third AggregationOperation. That's a group operation that will ensure you only return distinct values.

By the way: it will only return the fields you list in the group() method. So if you want more than just first name and last name, include more.

Now save everything and relaunch Spring Boot. You should see the following output:

[ {
  "firstName" : "The",
  "lastName" : "Emperor"
}, {
  "firstName" : "Darth",
  "lastName" : "Vader"
} ]

There you go. Now you have a unique list of all sales owners who manage a contact with the source set to "EMAIL."

Wrapping It Up

Now it's your turn. See where else in your own business applications you might need to use replaceRoot.

And then make it happen.

Also, feel free to just browse the source code on GitHub.

Have fun!

Photo by Karolina Grabowska from Pexels