Context
I’ve got a document model “Contact_DM” which has got a one-to-many (1:n) relationship to “Phone_DM”, defined in “Contact_Phone_RM”.
Goal
I want to iterate over all “Contact_DM”-documents and then subsequently all “Phone_DM”-documents of each contact using Java code.
Question
The outer loop is easy, but what’d be the ‘canon’ way to iterate all related docs of a specific kind (in my case “Contact_Phone_RM”)?
(I need this in a test class to make sure that my import created all the correct relations between documents)
Hi @juergen-smooth-ice ,
Which Data Services version are you using? The answer is dependent on it because we have changed this API dramatically on 2025.06
Oh, I thought I’d tagged it 2025.06… But that’s the one.
Hi @juergen-smooth-ice ,
I have missed the tag, I will pay more attention to the tags next time. There are multiple ways how to achieve this. This is an example of one of the ways:
QueryRoot.builder()
.targetDocumentModel("Contact_DM")
.projectionName("document")
.paging(Paging.builder()
.pageNumber(0)
.pageSize(10).build())
.link(QueryLink.builder()
.relationshipModel("Contact_Phone_RM")
.targetRole("phone").build())
.sort(List.of(
new Order("/__meta/docRef", Order.Direction.ASC, Order.NullHandling.NULLS_LAST)))
.build();
You control which Contact documents are loaded by paging property. Linked Phone documents are loaded per each Contact on the page (no paging here). I was missing information about targetRole, so please check Contact_Phone_RM what is a role defined there for Phone documents.
Property sort is needed, I picked docRef field, but you can pick any property of Contact_DM model.
If you are not interested in Contact_DM you can set property exclude = true. No Contact_DM documents will be returned and the paging will control Contact_Phone_DM documents.
Created instance of the QueryRoot can be passed into QueryService and the results will contain desired data. If you have more than 10 documents, you might want to make page number bigger and in the loop load the data until everything is loaded.
Ok, this is how it works now: 
// when
int linkedPhones = relationImportService.importRelations(getAccessDB(),
randomUniqueFileName,
"Rel_Contact_Phones_RM");
// then
assertEquals(6, linkedPhones);
QueryRoot queryRoot = QueryRoot.builder()
.targetDocumentModel("Rel_Contact_DM")
.projectionName("document")
.paging(Paging.builder()
.pageNumber(0)
.pageSize(10).build())
.link(QueryLink.builder()
.relationshipModel("Rel_Contact_Phones_RM")
.targetRole("Phone").build())
.sort(List.of(
new Order("/__meta/docRef", Order.Direction.ASC, Order.NullHandling.NULLS_LAST)))
.build();
QueryPage<Object> result = queryService.query(queryRoot, "de");
Map<String, Long> phonesPerContactCount = result.getContent().stream()
.filter(DocumentTreeResult.class::isInstance)
.map(DocumentTreeResult.class::cast)
.filter(tr -> tr.getTargetRole().equals("Phone"))
.collect(Collectors.groupingBy(dtr -> {
Pattern pattern = Pattern.compile("ContactID\": \"(.*?)\"");
Matcher matcher = pattern.matcher(dtr.getDocument().toString());
if (matcher.find()) {
return matcher.group(1);
} else {
throw new RuntimeException("no contact id found");
}
}, Collectors.counting()));
// expect
assertEquals(3, phonesPerContactCount.size());
phonesPerContactCount.forEach((contactId, count) -> {
switch (contactId) {
case "1" -> assertEquals(2, count);
case "2" -> assertEquals(1, count);
case "3" -> assertEquals(3, count);
}
});
I wouldn’t exactly call it sleek - if you consider that the data retrieval would take exactly one line of code when using JPA repos/entities. At least add some utility functions, maybe?
Here are some remarks to the code above:
- The
results property can be defined with type QueryPage<DocumentTreeResult> so you do not need to cast it in map.
- The
QueryRoot construction takes a lot of lines, but you can create a utility function that takes just page numbers because everything else is hardcoded. The DocumentTreeResult can also be converted to the IDocumentV2 and the kernel API can be used to retrieve values instead of Pattern/Matcher.
- JPA requires a mapping, which A12 cannot provide because we have no domain knowledge about the data of the project.
- Which Utility function you like DS to add ? There is A12 ticket collecting requirements in the testing area. Please see A12-17786. Add your requirements, we will address them as soon as we can.
Here is an example of how are we testing ourselves for better readability:
@Test(dataProvider = "invalidPagingData")
public void testQuery_invalidPaging(Integer pageNumber, int pageSize, String expectedErrorMessage) {
QueryRoot queryRoot = newQueryRoot(DocumentProjectionImplementation.PROJECTION_NAME, CONTRACT_DOCUMENT_MODEL, pageNumber, pageSize);
QueryInvalidInputException invalidInputException =
Assert.expectThrows(QueryInvalidInputException.class, () -> queryService.query(queryRoot, language));
Assert.assertTrue(invalidInputException.getMessage().contains(expectedErrorMessage));
}
The example is testing exceptions, but dataProvider can easily be turned into expected result set.