Skip to Content

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

 

But if you do like reading, I’d like to ask you to read the Intro, to know what this blog is about.

 

Intro

This blog is part of our series of blogs for beginners using SAP Cloud Platform SDK for service development (see here for info)

In this blog we’ll learn how to navigate from one entity to another entity.
But don’t expect that this is only about learning how to click a hyperlink…

 

Learning

Learn how to describe the navigation between related entities in an OData model
Learn how to implement navigation in your OData service (this blog is based on OData V4)

Prerequisite: know how to click a hyperlink…. and also see here

 

Project

How to create a project? See here.

 

Model

It is time to introduce a second entity type in our model. This will allow us to create a relation between them and this will enable the user of our service to “navigate” from one entity to the related entity.

In our example, we could capture the job which our person has.
One person can have only one job at a time (say, it is a full-time job…) , but one job can of course be related to multiple instances of person.
As such, it is a classic bi-directional relationship:

Person->Job: 1-to-1
Job->Employees: 1-to-many

As mentioned, we want to navigate from one selected person to his job.
We need to enhance the model, we need to add an entity type “Job” to our model
As usual, I’d like to keep it as simple as possible, to focus only on what we’re learning in this blog

Our definition of entity type “Job”

<EntityType Name="Job">
   <Key>
      <PropertyRef Name="JobId" />
   </Key>
   <Property Name="JobId" Type="Edm.Int32" />
   <Property Name="JobName" Type="Edm.String" />
</EntityType>

Now we want to add a navigation property from Person to Job

<EntityType Name="Person">
   . . .
   <NavigationProperty Name="Occupation" Type="demo.Job" Partner="Employees" />

And we also add navigation property from Job to Person

<EntityType Name="Job">
   . . .
   <NavigationProperty Name="Employees" Type="Collection(demo.Person)" Partner="Occupation" />

 

Explanation of the edmx snippets:

Name:

Be aware that the name that we provide as attribute for the <NavigationProperty> tag will end up in the URL.

The end user will use the name of the navigation property whenever he wants to navigate from a person to his job

Example:

<service>/odata/v4/DemoService/People(2)/Occupation

 

Note:
In many services you’ll find that the name of a navigation property is the same like the target entity
We could have called it “Job” or even “ToJob”.
I thought of giving a different name, I think it is less confusing.

 

Type:

The type of the navigation property is the entity type to which we’re navigating.
It has to be qualified, means preceded by the namespace
In case of 1-to-many relation, the type is a collection, e.g. Type=“Collection(demo.Person)

In our example, we’re specifying that we navigate from Person to one instance of entity type “Job”
In the other direction, we navigate from one Job to a collection of entity type “Person”

 

Partner:

This attribute is optional and we’re using it to indicate that we want bi-directional relationship.

What do we have to specify here?
It is the name of the navigation property of the foreign entity type.

Note that each entity type can have multiple navigation properties, so it is required to specify which one is the partner

 

Nullable:

Another optional attribute which can be used to ensure that the navigation target is never empty.

In our example, we assume that a person can be unemployed, so the navigation target can be empty for us. So  we don’t specify the Nullable attribute (default is true)

 

 

More changes are required to make the navigation working:

The entity set(s) need to be enriched.
A binding has to be added, to ensure that at runtime the navigation can be resolved:

<EntitySet Name="People" EntityType="demo.Person">
   <NavigationPropertyBinding Path="Occupation" Target="Jobs" />
</EntitySet>
<EntitySet Name="Jobs" EntityType="demo.Job" >
   <NavigationPropertyBinding Path="Employees" Target="People" />
</EntitySet>

 

Explanation:

 

Path:

This path is related to the actual entity set.  I mean, it is not about a navigation path to anything foreign. The “Path” of <NavigationPropertyBinding> is just the name of the navigation property
Depending on the modelling, it might be a path. That’s why this attribute is called “Path”.

Example:
For the entity set “People”, the corresponding entity type is “Person” and the “Person” has the  navigation property “Occupation”. Therefore, we’re giving “Occupation” for the “Path” 

 

Target:

This is simple: it is the name of the entity set. The entity set to which we’re navigating.
This doesn’t have anything to do with the cardinality. I mean, even if we navigate to a single entity, we still have to give here the name of the entity set.

 

OK.
That’s it.

 

Just a note:
In case you’re familiar with version 2 of OData, you’ll notice that in V4 the specification is a bit different.
See here for differences

 

 

I feel that all this is confusing and best is to look at an example.
This overview is meant to finally clarify all your confusion:

 

Code

As usual, we keep everything as simple as possible, so we use a simple model and implement only those methods which we need for this blog.

In order to be able to better test the service, we add simple code for QUERY and READ of both entity sets (People and Jobs)

But what we’re focusing on, are 2 methods for 2 types of navigation:
• Person->Job
• Job->People

The corresponding implementation is not complex, we only should have a look at two aspects

 

1) First: which operation

A relation can be either one-to-one or one-to-many
As such, what we get in the browser is either a list (one-to-many) or a single entry (one-to-one)
This means, what we’re doing is either implementing a QUERY or a READ operation.
As such, no new annotation is required for our Java method (e.g. anything like @Navigation is not required)

2) Second: which annotation attribute
As we’ve just learned, we’re using an existing annotation: @Query or @Read
So what is the difference to normal QUERY or READ?
There’s an additional attribute: the “sourceEntity”
Example:

@Read(serviceName = "DemoService", entity = "Jobs", sourceEntity = "People")
public ReadResponse getJobForPerson(...){
...

 

How do we have to interpret this whole annotation thing?
Let’s go through it, from left to right:

1) First: @Read
Declares that it is a READ operation.
Why READ?
Because it corresponds to a normal navigation with 1-to-1 relationship, which means that the target is a single entry.
Example: one person can have only one job

2) Second: “serviceName”
Declares the service which is invoked.
In our example it is called “DemoService”

3) Third: “entity”
The entity (“Jobs”) means: we have to return a Job, i.e. a single entry (because of Read) of entity type Job. This is indicated by the value “Jobs” (yes, it is plural, because it is the name of the corresponding entity set)

4) Fourth: “sourceEntity”
This single Job which we have to return, is not defined by a key (e.g. Jobs(1) ) but instead it depends on the Person, which has this job. And the information about the person is contained in the sourceEntity (e.g. People(2)

Example:
This URL:

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

will end up in a method with this annotation:

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

 

Note:
Have you noticed the coloring?

 

From the explanation above, we can deduct the steps which we have to follow in order to implement that example navigation:

Step 0: create a method to be called for that URL
And add the annotation as shown above

Step 1: get the requested personID from the URL
In the example above, it would be: 2

Step 2: get the requested person from the list of all people
In the example above, we find the person with id 2

Step 3: extract the JobId from the properties of this person
In the example above it is: 1

Step 4: search for the retrieved JobId in the list of all jobs

Step 5: set this job map as response for the operation

 

Now let’s get absolutely concrete

 

Implement navigation for Person -> Job

According to our model, the URL which we want to support looks for example like this:

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

The entity is “Jobs”.
Why “Jobs”??? There is no “Jobs” in the URL ???
Yes, we don’t see it in the URL, but we know that the navigation property “Occupation” has target “Jobs”.
Remember the model?

<NavigationPropertyBinding Path="Occupation" Target="Jobs" />

Agreed.

So, the “entity” is “Jobs”
The “sourceEntity” is “People”
The “serviceName” is of course our “DemoService”

As such, the FWK expects that we implement a method like the following:

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

Now we proceed and fill the method body with nice code (if you don’t mind, let’s ignore any error cases)

 

Step 1: retrieve the requested personID from the URL

Integer personId = (Integer) request.getSourceKeys().get("PersonId");

Step 2: find the requested person from the list of all people

Map<String, Object> personMap = findPerson(personId);

Step 3: extract the JobId from the properties of this person

Integer jobId = (Integer) personMap.get("JobId");

Step 4: search for the retrieved JobId in the list of all jobs

Map<String, Object> jobMap = findJob(jobId);

Step 5: set this job map as response for the operation

return ReadResponse.setSuccess().setData(jobMap).response();

 

Implement navigation for Job -> People

URL: e.g. https://<host>/odata/v4/DemoService/Jobs(1)/Employees

Method and annotation:

@Query(serviceName = "DemoService", entity = "People", sourceEntity = "Jobs")
public QueryResponse getPeopleForJob(QueryRequest request) {

Note that here we specify a @Query annotation, because the navigation property Employees (defined in the entity type “Job”) specifies a Type as collection of Person

<NavigationProperty Name="Employees" Type="Collection(demo.Person)" ...

 

The code:

 

Step 1: get the JobId from URL

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

Step 2: retrieve all people who have this job

We can directly loop over the people list, because our entity type “Person” has a property where his job is stored as id.
As such, we can just loop over all existing people and check if the JobId matches the one which is given in the URL.
If yes, we add this person to a temporary list

 

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

Step 3: set this people list as response for the operation

return QueryResponse.setSuccess().setDataAsMap(responsePeopleList).response();

 

 

Run

I’m proposing the following URLs for testing the above implementation for our 2 navigation cases:

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

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

 

 

Summary

This has been the first insight into implementing navigation in OData service (V4)
The next blog show that there’s even more what users can do in the context of navigation.
Make sure to not miss it, it will be interesting 😉

 

Links

Overview of blog series and link collection.

More about navigation: READ, CREATE, $expand, query options

 

 

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

<?xml version="1.0" encoding="utf-8"?>
<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 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.QueryRequest;
import com.sap.cloud.sdk.service.prov.api.request.ReadRequest;
import com.sap.cloud.sdk.service.prov.api.response.QueryResponse;
import com.sap.cloud.sdk.service.prov.api.response.ReadResponse;

public class ServiceImplementation {
	
	// static lists representing dummy database tables
	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>>();

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

	@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="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();
	}	
	
	@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();		
	}	
	
	
	/* 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;
	}

	
	// used for READ operation. Go through the list and find the person with requested PersonId
	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