Skip to Content

In the previous blog we’ve learned how to define and implement navigation between 2 entities in an OData V4 service, based on the SAP Cloud Platform SDK for service development.

We’ve covered the most usual cases:

  • One-to-one relation:
    In our example, we can navigate from one person to his job
  • One-to-many navigation:
    In our example, we can navigate from one job to all people who have this job.

In the current blog, we’ll add few more use cases:

  • One-to-many navigation and access of single entity:
    In our example, we navigate from job to people, however not all people: we read only one of them
  • CREATE on a navigation: yes, it is possible to fire a CREATE request on a URL which points to a navigation property
  • System Query Options:
    when navigating to a collection, then it is possible to apply query options like $top
  • $expand:
    The fwk supports $expand in a generic way, no additional code is required on top of what we already have done.

 

 

Intro

You may have noticed that this blog is part of our series of blogs for beginners using SAP Cloud Platform SDK for service development (see here for info)

Note:
If you don’t like reading, just scroll down to the end of this page to view the full source code.

 

Learning

Nothing new with respect to OData and edmx.
Some additional aspects in the implementation will be new here, however, not much surprising
Some use cases when using the service, also won’t surprise you so much

Prerequisites: see here

 

Project

See here for how to create a project.

 

Model

We can reuse the same edmx file which we were using in the previous blog.
No changes required

 

Code

First of all, we can reuse the same java file which we were using in the previous blog.
We’ll enhance it afterwards.
OK, I let you copy the file and in the meanwhile I’ll be waiting…

…copying…

OK, now that you’ve copied the java file, let’s add the required code.

 

Implement navigation for Job -> People and access a single Person

This is a bit special case in the navigation.
As you’ve realized, when navigating from a job to the assigned people, we end up in a list.
This is somehow a list.
I mean it: it is a list.
Even if the list is an empty list or a list which contains only one entry…
It is still a list, because we’ve modeled it like this.
So, if it is really a list… why not address a single entry of the list?
Just like we do for a normal READ?

Let’s look at our example:

We can do a READ for one single Person:

https://<host>/odata/v4/DemoService/People(2)

And NOW, we can also do a READ for exactly THAT list of People which we get when following the navigation.
With other words: there’s a list of people who all have the same job. Now we want to READ only one Person of this list.

The URL is as follows:

https://<host>/odata/v4/DemoService/Jobs(1)/Employees(2)

Note:
You might wonder why this is done at all. We could similarly retrieve this person from the person-collection and applying a filter, etc.
Yes, it is true, often there are several ways of achieving the desired goal.
However, small differences can be in the way how a service is used and in performance considerations. Imagine a smartphone app which has strong requirements to limit the network traffic, so going through the flow, navigate from one screen to the next and only then a small amount of data is fetched by the underlying OData service.

 

What does that mean implementation-wise?

The implementation is partly the same like in previous blog where we had : Job->People
The annotation looks very similar, only difference is @Read instead of @Query

@Read(serviceName = "DemoService", entity = "People", sourceEntity = "Jobs")
public ReadResponse getPersonForJob(ReadRequest request) {

 

Step 1: We first need to get the list of people who have the chosen job.

Integer jobId = (Integer) request.getSourceKeys().get("JobId");
		
// identify those people who have this job
List<Map<String,Object>> allPeopleList = getAllPeople();
List<Map<String,Object>> peopleForJobList = new ArrayList<Map<String, Object>>();
for(Map<String, Object> personMap : allPeopleList) {
	if(((Integer)personMap.get("JobId")).equals(jobId)) {
		peopleForJobList.add(personMap);
	}
}

 

Step 2: get the requested person from the URL.

After step 1 we have a list, the list of people who have that job. We then want to do a READ on this list. Before we can do that READ, we have to know which person we have to read. It is in the URL. We obtain that info just like in a normal read:

Integer requestedPersonId = (Integer) request.getKeys().get("PersonId");

Note that here we use request.getKeys() whereas in step 1 we called request.getSourceKeys()

 

Step 3: find the requested person

Note that we don’t search in the list of all existing people. We have to search in the temporary list which we’ve filled in step 1.

It is the list of people which have the requested job, we’ve accessed this list via navigation
Once we’ve found it, we can return it.

for(Map<String, Object> person : peopleForJobList){
	if (((Integer)person.get("PersonId")).equals(requestedPersonId)) {
		// found the person
		return ReadResponse.setSuccess().setData(person).response();
	}
}

 

Step 4: little error handling

What if the user specifies a key which is valid, but not true?
Sorry for this cryptic phrase…

I mean:
We know that this person exists, we can do a READ for it:

…/DemoService/People(4)

We get the same person if we use navigation + READ:

…/DemoService/Jobs(2)/Employees(4)

So we’re pointing to an existing person if we call:

…/DemoService/Jobs(1)/Employees(4)

However, that call has to fail, because this person doesn’t have the job(1)
And this is exactly what we assume goes wrong if the loop in the previous step doesn’t find a person
As such, at the end we can return an error response like this:

return ReadResponse.setError(
		ErrorResponse.getBuilder()
		.setMessage("Error: Couldn't find requested person")
		.setStatusCode(404)
		.response());

 

 

Implement CREATE on a navigation link

The spec says about creation of entities:

“If the target URL for the collection is a navigation link, the new entity is automatically linked to the entity containing the navigation link.”

For us, who have to implement this requirement, this means:
Implement a CREATE operation and take the source entity into account.

With other words:

With respect to annotations, we’ve already used twice the following annotation attributes:

@Query(serviceName = “DemoService”, entity = “People”, sourceEntity = “Jobs”)

@Read(serviceName = “DemoService”, entity = “People”, sourceEntity = “Jobs”)

Now again, the attributes are the same, we only have to change the operation:

@Create(serviceName = “DemoService”, entity = “People”, sourceEntity = “Jobs”)

 

With other other words:

The one-to-many navigation ends up in a collection
e.g.

https://<host>/odata/v4/DemoService/Jobs(2)/Employees

As you know, when firing a “normal” POST request, it is usually fired against a collection …
so why not use THIS collection?

The code for CREATE on a navigation property is almost the same like for a normal CREATE.
The difference:
The “normal” CREATE uses the request body as input
This “special” CREATE uses the request body, but also the URL as input

More concrete.

The URL shows that we create a “Person” (because “Employees” is of type “Person”)

https://<host>/odata/v4/DemoService/Jobs(2)/Employees

When creating a “Person”, we need the following properties to be filled with data:

<Property Name="PersonId" Type="Edm.Int32" />
<Property Name="Name" Type="Edm.String" />
<Property Name="JobId" Type="Edm.Int32" />

 

When executing a “normal” CREATE, these properties are specified in the request payload (we’ve done that in a previous blog)

e.g.

{
     "PersonId": 6,
     "Name": "Martin",
     "JobId": 1
}

However, in case of CREATE on navigation property, we already have the value for “JobId” in the URL, it is a 2:

https://<host>/odata/v4/DemoService/Jobs(2)/Employees

As such, the user of our service can remove this property from the request payload, when doing a POST request.
But even if it is present in the request payload, we have to ignore it in our implementation.
As such, instead of taking the value for “JobId” from the request body, we take it from the URL

 

Now coming to the implementation.

 

We create a method like this:

@Create(serviceName = "DemoService", entity = "People", sourceEntity = "Jobs")
public CreateResponse createPersonForJob(CreateRequest request) {

We retrieve the value for “JobId” from the URL:

Integer jobIdFromRequest = (Integer) request.getSourceKeys().get("JobId");

We retrieve all other values from the request body:

Map<String, Object> requestBody = request.getMapData();
Integer idToCreate = (Integer) requestBody.get("PersonId");  
String nameToCreate = (String) requestBody.get("Name"); 

We use all that for doing the creation:

Map<String, Object> createdPerson = createPerson(idToCreate, nameToCreate, jobIdFromRequest);

 

Note:
It is important that we store the created instance of “Person” in a variable like “createdPerson”
Why?
Because we have to set is in the response body.
Why this cannot be done by the FWK?
Because in this special case, the real response is different from the data which is sent in the request body, so the FWK cannot know.
E.g., as I mentioned, if the user specifies a “JobId” in the request payload, we will ignore it and create the person with the correct “JobId”, which is in the URL
Therefore, we have to return exactly the data which we have created

 

return CreateResponse.setSuccess().setData(createdPerson).response();

 

 

That’s it about coding for this tutorial.

The following topics don’t require any implementation.

So we can relax for a moment…

 

Run

Navigation to-many with READ:

First check for available entries:

https://<host>/odata/v4/DemoService/Jobs(1)/Employees

Choose one of them and execute the READ:

https://<host>/odata/v4/DemoService/Jobs(1)/Employees(2)

 

That’s it.

Now an example for error case:

https://<host>/odata/v4/DemoService/Jobs(1)/Employees(1)

The response which we’ve set in our code can be viewed in the browser

 

CREATE on navigation link:

First you can check the URL for this navigation:
GET: https://<host>/odata/v4/DemoService/Jobs(3)/Employees

It returns an empty collection, because none of our people has this Job (don’t ask why, I’m sure it is a nice job…)

Now take this URL to fire a CREATE operation:

URL https://<host>/odata/v4/DemoService/Jobs(3)/Employees
HTTP Verb POST
Request body {
“PersonId”: 6,
“Name”: “Maxi”
}
Request Header Name: Content-Type
Value: application/json

 

 

After the successful creation, you can verify the result by re-executing the request like above and realizing that it isn’t empty anymore:

GET: https://<host>/odata/v4/DemoService/Jobs(3)/Employees

And you can also check the “normal” People collection to verify that the new person is there:

GET: https://<host>/odata/v4/DemoService/People

 

 

System Query Option: $expand:

 

You might have heard about this OData feature.

In case that not: here are few words:

It is like a shortcut of a navigation
It is like combining 2 separate requests in one request
It is like a READ request and a navigation within one call, and the data is all visible in the response payload.
It is easier to understand when looking at examples.
And you may just go ahead and try it out.
How does it come?
Because the “SDK for service development” has out-of-the-box support for $expand
Yes, you don’t need to implement it manually

Note:
Of course, there are valid concerns about performance. A manual implementation can be and should be more efficient than the generic FWK support.

But for today, we’re just happy that we can try it without any effort. And someday in future we’ll do it manually… maybei n a future blog… oh yes, for sure we’ll do it…

Example:
An end user would fire e.g. these 3 requests in a row:

-> QUERY the list:

    https://<host>/odata/v4/DemoService/Jobs

-> Choose one entry:

            https://<host>/odata/v4/DemoService/Jobs(1)

-> Follow the navigation link:

                 https://<host>/odata/v4/DemoService/Jobs(1)/Employees 

-> At the end he has all required information, Jobs and People

However:

==> Using $expand, this can be achieved in one single call:

       https://<host>/v4/DemoService/Jobs?$expand=Employees

Result looks as follows:

 

What do we see here?

We have the list of Jobs, like we get when invoking a normal QUERY. But in addition, for each entry the navigation property (“Employees”) is rendered.
If you don’t believe, you can again compare to normal QUERY: odata/v4/DemoService/Jobs
Furthermore, the value of the navigation property is rendered. It is the same like when following the navigation property in the previous calls.
Since it is a one-to-many relation, what we get is a json-array – you can see the brackets:  [  and   ]

How to compose that URL for expand?

You take the desired QUERY – URL
and append ?
and append $expand
and append =
and append the <nameOfNavigationProperty>

The name of the navigation property is just the same like you were using for the “normal” navigation.

 

Next test-URL:
$expand can be added not only to a QUERY but also to a READ URL

https://<host>/odata/v4/DemoService/Jobs(1)?$expand=Employees

 

And finally, we try as well the expand on a to-one navigation property, for QUERY and READ:

https://<host>/odata/v4/DemoService/People?$expand=Occupation

https://<host>odata/v4/DemoService/People(2)?$expand=Occupation

 

Note:
I know that you’re curious…
You’re wondering: what is the FWK doing? How can it do the expand? Is there some black magic happening…?
Don’t worry, the FWK is just a human, like us.
What it does, when an expand is detected, the FWK calls our implementation for the “normal” QUERY (or: READ) and for each (or: the) entry, it follows the navigation property and calls our implementation for it.
I know that you’re skeptical…
You’re wondering: urgh – is that efficient…?
Don’t worry, the FWK is just a human, like us.
In this case it is us who need to decide if we can afford to rely on the FWK-functionality. Of course, such generic support cannot be performant. But for small amount of data it is just nice to get it for free.
Stay tuned to learn how to overwrite such generic FWK-functionality with own specific implementation.

 

More System Query Options:

For this chapter we don’t need much explanation.
What we’re learning here is that the result of a navigation can be treated like a “normal” entity collection or a “normal” single resource.
As such, we can apply system query options.
Again, like in “normal” QUERY, we don’t need any manual implementation.

So you can go ahead and try the following URLs:

 

$count=true

This call renders the number of entities (of the target collection, i.e. the Employees) in the response

https://<host>/odata/v4/DemoService/Jobs(0)/Employees?$count=true

 

$top=1

This call returns only one entity instead of 2:

https://<host>/odata/v4/DemoService/Jobs(0)/Employees?$top=1

 

$select

This call includes only the one specified property in the response, instead of all 3

https://<host>/odata/v4/DemoService/Jobs(0)/Employees?$select=Name

This call includes 2 properties (comma-separated list of select properties)

https://<host>/odata/v4/DemoService/Jobs(0)/Employees?$select=Name,PersonId

This call uses the star operator to include all properties

https://<host>/odata/v4/DemoService/Jobs(0)/Employees?$select=*

 

$select&$count&$top

This call combines all 3 query options

https://<host>/odata/v4/DemoService/Jobs(0)/Employees?$count=true&$select=Name&$top=1

 

$select&$expand

This call combines just a $select and $expand.

https://<host>/odata/v4/DemoService/Jobs?$select=JobName&$expand=Employees

There’s nothing strange about it.
However, maybe you’re wondering because you might have expected that only those properties can be expanded, which are contained in the $select statement.
This was the case in OData V2
Like this:

https://<host>/odata/v4/DemoService/Jobs?$select=JobName,Employees&$expand=Employees

This call works fine, but it isn’t required. The expanded navigation properties are automatically considered part of $select
More info here.

 

$select applied to expanded entities

This call shows how to apply $select to expanded entities, such that the payload of the expanded entities is smaller
The call does an expand but renders only the specified properties in the expanded section

https://<host>/odata/v4/DemoService/Jobs?$expand=Employees($select=Name)

 

$select plus $select for expand

Here we can see how $select is applied to both, the “normal” QUERY and the expanded entities

<host>/odata/v4/DemoService/Jobs?$select=JobName&$expand=Employees($select=Name)

 

Appendix 1: Source code of Model file: DemoService.xml

<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
	<edmx:DataServices>
		<Schema Namespace="demo" xmlns="http://docs.oasis-open.org/odata/ns/edm">
			<EntityType Name="Person">
				<Key>
					<PropertyRef Name="PersonId" />
				</Key>
				<Property Name="PersonId" Type="Edm.Int32" />
				<Property Name="Name" Type="Edm.String" />
				<Property Name="JobId" Type="Edm.Int32" />
 				<NavigationProperty Name="Occupation" Type="demo.Job" Partner="Employees" />
			</EntityType>
			<EntityType Name="Job">
				<Key>
					<PropertyRef Name="JobId" />
				</Key>
				<Property Name="JobId" Type="Edm.Int32" />
				<Property Name="JobName" Type="Edm.String" />
                <NavigationProperty Name="Employees" Type="Collection(demo.Person)" Partner="Occupation" />
			</EntityType>
			<EntityContainer Name="container">
				<EntitySet Name="People" EntityType="demo.Person">
						<NavigationPropertyBinding Path="Occupation" Target="Jobs" />
                </EntitySet>
				<EntitySet Name="Jobs" EntityType="demo.Job" >
						<NavigationPropertyBinding Path="Employees" Target="People" />
                </EntitySet>
			</EntityContainer>
		</Schema>
	</edmx:DataServices>
</edmx:Edmx>

Appendix 2: Source code of Java class file: ServiceImplementation.java

package com.example.DemoService;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sap.cloud.sdk.service.prov.api.operations.Create;
import com.sap.cloud.sdk.service.prov.api.operations.Query;
import com.sap.cloud.sdk.service.prov.api.operations.Read;
import com.sap.cloud.sdk.service.prov.api.request.CreateRequest;
import com.sap.cloud.sdk.service.prov.api.request.QueryRequest;
import com.sap.cloud.sdk.service.prov.api.request.ReadRequest;
import com.sap.cloud.sdk.service.prov.api.response.CreateResponse;
import com.sap.cloud.sdk.service.prov.api.response.ErrorResponse;
import com.sap.cloud.sdk.service.prov.api.response.QueryResponse;
import com.sap.cloud.sdk.service.prov.api.response.ReadResponse;

public class ServiceImplementation {
	
	
	private static final List<Map<String, Object>> peopleList = new ArrayList<Map<String, Object>>();
	private static final List<Map<String, Object>> jobList = new ArrayList<Map<String, Object>>();

	
	/* PERSON */
	
	@Query(serviceName="DemoService", entity="People")
	public QueryResponse getPeople(QueryRequest request) {
		List<Map<String, Object>> peopleList = getAllPeople();
		return QueryResponse.setSuccess().setDataAsMap(peopleList).response();
	}

	
	@Read(serviceName="DemoService", entity="People")
	public ReadResponse getPerson(ReadRequest request) {
		Integer id = (Integer) request.getKeys().get("PersonId");
		Map<String, Object> personMap = findPerson(id);
		return ReadResponse.setSuccess().setData(personMap).response();
	}	
	
	/* JOB */
	
	@Query(serviceName="DemoService", entity="Jobs")
	public QueryResponse getJobs(QueryRequest request) {
		List<Map<String, Object>> jobList = getAllJobs();
		return QueryResponse.setSuccess().setDataAsMap(jobList).response();
	}
	
	
	@Read(serviceName="DemoService", entity="Jobs")
	public ReadResponse getJob(ReadRequest request) {
		Integer id = (Integer) request.getKeys().get("JobId");
		Map<String, Object> jobMap = findJob(id);
		return ReadResponse.setSuccess().setData(jobMap).response();
	}	
	
	
	/* NAVIGATION */
	
	// Navigation from one person to one Job 
	// Example URI: srv/people(1)/Occupation
	@Read(serviceName = "DemoService", entity = "Jobs", sourceEntity = "People")
	public ReadResponse getJobForPerson(ReadRequest request) {
		
		Integer personId = (Integer) request.getSourceKeys().get("PersonId");
		Map<String, Object> personMap = findPerson(personId);
		
		Integer jobId = (Integer) personMap.get("JobId");
		Map<String, Object> jobMap = findJob(jobId);
		
		return ReadResponse.setSuccess().setData(jobMap).response();
	}	
	
	
	// Navigation from one job to all people who have this job. 
	// Example URI: /srv/Jobs(1)/Employees
	@Query(serviceName = "DemoService", entity = "People", sourceEntity = "Jobs")
	public QueryResponse getPeopleForJob(QueryRequest request) {
		List<Map<String,Object>> responsePeopleList = new ArrayList<Map<String, Object>>();

		Integer jobId = (Integer) request.getSourceKeys().get("JobId");
		
		List<Map<String,Object>> allPeopleList = getAllPeople();
		for(Map<String, Object> personMap : allPeopleList) {
			if(((Integer)personMap.get("JobId")).equals(jobId)) {
				responsePeopleList.add(personMap);
			}
		}
		
		return QueryResponse.setSuccess().setDataAsMap(responsePeopleList).response();		
	}	

	
	// Navigation to collection and READ one entry 
	// Example URI: /srv/Jobs(1)/Employees(2)
	@Read(serviceName = "DemoService", entity = "People", sourceEntity = "Jobs")
	public ReadResponse getPersonForJob(ReadRequest request) {

		//sourceEntity: Job
		Integer jobId = (Integer) request.getSourceKeys().get("JobId");
		
		// identify those people who have this job
		List<Map<String,Object>> allPeopleList = getAllPeople();
		List<Map<String,Object>> peopleForJobList = new ArrayList<Map<String, Object>>();
		for(Map<String, Object> personMap : allPeopleList) {
			if(((Integer)personMap.get("JobId")).equals(jobId)) {
				peopleForJobList.add(personMap);
			}
		}
		
		//now do the READ on this collection. First get the requested person-id from the request-URL
		Integer requestedPersonId = (Integer) request.getKeys().get("PersonId");
		// search for the requested person, but only in the collection of people, which we reached via navigation
		for(Map<String, Object> person : peopleForJobList){
			if (((Integer)person.get("PersonId")).equals(requestedPersonId)) {
				// found the person
				return ReadResponse.setSuccess().setData(person).response();
			}
		}
		
		// Error: the person ID in the request (navigation target) must be wrong
		return ReadResponse.setError(
				ErrorResponse.getBuilder()
				.setMessage("Error: Couldn't find requested person")
				.setStatusCode(404)
				.response());
	}	
	
	
	// CREATE on a navigation property. 
	// Example URL: DemoService/Jobs(2)/Employees
	@Create(serviceName = "DemoService", entity = "People", sourceEntity = "Jobs")
	public CreateResponse createPersonForJob(CreateRequest request) {

		// the value for "JobId" is retrieved from the request URI
		Integer jobIdFromRequest = (Integer) request.getSourceKeys().get("JobId");
		
		// other values are retrieved from the request body
		Map<String, Object> requestBody = request.getMapData();
		Integer idToCreate = (Integer) requestBody.get("PersonId");  
		String nameToCreate = (String) requestBody.get("Name"); 
		
		// do the actual creation in database
		Map<String, Object> createdPerson = createPerson(idToCreate, nameToCreate, jobIdFromRequest);
		return CreateResponse.setSuccess().setData(createdPerson).response();
	}	
	
	
	
	/* DUMMY DATABASE */
	
	private List<Map<String, Object>> getAllPeople(){
		if(peopleList.isEmpty()) {
			createPerson(1, "Anna", 1);
			createPerson(2, "Berta", 1);
			createPerson(3, "Claudia", 2);
			createPerson(4, "Debbie", 2);
		}
		
		return peopleList;
	}
	
	private Map<String, Object> createPerson(Integer personId, String name, Integer jobId){
		Map<String, Object> personMap = new HashMap<String, Object>();
		personMap.put("PersonId", personId);
		personMap.put("Name", name);
		personMap.put("JobId", jobId);
		peopleList.add(personMap);

		return personMap;
	}

	private Map<String, Object> findPerson(Integer requiredPersonId){
		List<Map<String,Object>> peopleList = getAllPeople();
		for(Map<String, Object> personMap : peopleList) {
			if(((Integer)personMap.get("PersonId")).equals(requiredPersonId)) {
				return personMap;
			}
		}
		return null;
	}
	
	private List<Map<String, Object>> getAllJobs(){
		if(jobList.isEmpty()) {
			createJob(1, "Software Engineer");
			createJob(2, "Musician");
			createJob(3, "Architect");
		}
		return jobList;
	}

	private Map<String, Object> createJob(Integer jobId, String name){
		Map<String, Object> jobMap = new HashMap<String, Object>();
		jobMap.put("JobId", jobId);
		jobMap.put("JobName", name);
		jobList.add(jobMap);
		
		return jobMap;
	}

	private Map<String, Object> findJob(Integer requiredJobId){
		List<Map<String,Object>> jobList = getAllJobs();
		for(Map<String, Object> jobMap : jobList) {
			if(((Integer)jobMap.get("JobId")).equals(requiredJobId)) {
				return jobMap;
			}
		}
		return null;
	}

}

 

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply