SAP Cloud Platform SDK for service development: Create OData Service [2] QUERY, READ, SQO
After the first exciting fun-event of having our first OData service in the cloud, after celebrating the success and enjoying the applause… now let’s go the next steps and learn a bit more
Note:
If you don’t like reading, just scroll down to the end of this page to view the full source code.
Intro
Only little bit more learning, we want to proceed with only small steps.
Small-step-by-small-step walking through our series of blogs for beginners using SAP Cloud Platform SDK for service development (see here for info)
In this blog, we’re going to implement the 2 GET operations, still in a very simple fashion, and cover few topics.
See here for prerequisites.
Learnings
Basic QUERY
Basic READ with key
Basic error handling
Basic logging
System Query Options (SQO)
Project
I suggest to create a new project in each tutorial. You can choose a project name (ArtifactID) of your choice, you only need to be aware that the project name will be used as application name when deploying to Cloud Foundry.
How to create a project? See here.
Model
For this blog we don’t need to enhance the model. The model remains the same, it has one entity type and one entity set.(How and where to create the model file)
Here it is:
<?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="UniqueId" />
</Key>
<Property Name="UniqueId" Type="Edm.Int32" />
<Property Name="Name" Type="Edm.String" />
</EntityType>
<EntityContainer Name="container" >
<EntitySet Name="People" EntityType="demo.Person" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Code: QUERY
The QUERY operation ?
This is, to execute a query on the existing set of records.
With other words: give me the list of all entries
Or: give me the people
Whenever the end-user (of our service) wants to get the list of the currently stored people, he executes a GET request on a URL like the following
<host>/<service>/<entitySet>
The name of the entity set has to be taken from the service metadata, as defined by us.
An OData service exposes the list of entity sets in the so-called service document, which can be found at:
<host>/<service>
Of course. the metadata document exposes all metadata, including the entity sets:
<host>/<service>/$metadata
In our example, the entity set has the name People
So the user fires the following URL:
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People
Now, the framework identifies this URL as a QUERY operation and looks into our service implementation to find a method which is suitable, to provide the data for this request.
As such, the framework searches for a method which is annotated with the @query annotation
So let’s quickly implement this method (you remember how and where create the Java class?)
@Query(serviceName="DemoService", entity="People")
public QueryResponse getPeople(QueryRequest request) {
List<Map<String, Object>> peopleMap = getPeople();
return QueryResponse.setSuccess().setDataAsMap(peopleMap).response();
}
As you can see, it is the same like in the previous blog: get the data and pass it to the response.
As you know, for the READ operation:
The user requests one single entity via URL, so we provide one map in the code
Now, for the QUERY operation:
The users requests the list of people via URL, so we provide a list of maps via code
My opinion: it is easy to understand, since it is using standard java objects
Note:
In the annotation, we specify the same entity set (People) for READ and QUERY operation.
Means, all operations like QUERY, READ and also DELETE, CREATE, are referring to one type of data. In this case it is “People” and we know from our model, that the type is “Person”
That’s why it is enough to specify service name, entity set name and operation name.
Now let’s see how we create the data.
I mean, in the above code box we call a helper method to get the list of people.
But how is it implemented?
Actually, it doesn’t matter. I thought, let’s continue using dummy data, I believe it makes the code easier to understand.
private List<Map<String, Object>> getPeople(){
List<Map<String, Object>> peopleMap = new ArrayList<Map<String, Object>>();
peopleMap.add(createPerson(0, "Anna"));
peopleMap.add(createPerson(1, "Berta"));
peopleMap.add(createPerson(2, "Claudia"));
peopleMap.add(createPerson(3, "Debbie"));
return peopleMap;
}
private Map<String, Object> createPerson(int id, String name){
Map<String, Object> personMap = new HashMap<String, Object>();
personMap.put("UniqueId", id);
personMap.put("Name", name);
return personMap;
}
This dummy code initializes a list, such that we can see some names when invoking the QUERY.
Please don’t worry: it is just dummy code, meant to be easy to understand.
Run
I think at this point we can already go ahead and test the service.
You know how, so don’t expect detailed description.
So once you call the QUERY operation, you’ll see the list of 4 names:
Happy…..?
I have the feeling that there’s a lack of excitement…
So I suggest to try the following calls, just to see that the FWK is your friend:
$top
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People?$top=1
$count
https:// <yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People/$count
$select
In the select statement you have to give the exact name of the property(ies) which you want to see
<…>/odata/v4/DemoService/People?$select=Name
You may try as well
<…>/odata/v4/DemoService/People?$select=Name,UniqueId
and
<…>/odata/v4/DemoService/People?$select=*
…….
How do you feel now?
Maybe little bit more excited now…?
You’ve now executed a few System Query Options, as specified by OData.
These OData features are implemented in the framework itself, so you don’t have to do anything.
E.g. you don’t have to parse the URI, you don’t have to do any calculations.
if you’re now loudly shouting:
performance…!!!
…Then you’re right…
Of course, you can override the default framework-implementation to make it faster, if required.
But nevertheless, I like it, because I get the convenience (fwk takes care) but I can also override it if I want to (e.g. once my users start complaining about poor performance of my service…)
Code: READ
No, no, we’re not going to repeat the same like in the previous blog
But we can take the opportunity to make the implementation a little bit more realistic and introduce little bit more knowledge.
The usual procedure of a user interaction with an OData service (holds true also for interaction via any User Interface) is as follows:
User does QUERY operation to get a list of all existing entries
User chooses one entry and remembers the key (id) of it
User does READ for a single entry using the key
Example:
First the QUERY:
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People
Then the READ:
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People(1)
Understood.
In our service implementation, what do we have to do?
When the user invokes this URL
https://<yourapp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People(1)
then it is detected as READ operation and the FWK calls us.
Yes, we know that already.
1) First Step
What we have to do first, is to retrieve exactly the desired person, the one which has a UniqueId with value 1
As such, we need to access the value (e.g. 1) which is passed in the READ-URL
To enable us to do so, in the method signature we get the instance of ReadRequest
We can ask the ReadRequest for the key:
Map<String, Object> keyPredicates = request.getKeys();
Why is the key passed as plural and as map?
Because in OData we can have composed key: like in a database, several properties together can define the key.
In our case, we know that there’s only one key property, and we know the name and the type.
(Because we’ve defined all that in the edmx file)
As such, we can access the key as follows:
Object keyObject = keyPredicates.get("UniqueId");
Integer id = (Integer)keyObject;
The String “UniqueId” has to match the exact name of the key property, so it is best practice to copy it from the edmx file
The key object can be cast to int, because the property “UniqueId” is defined as Edm.Int32
Note:
Just for your information, in case you’re wondering:
Above, I’ve told you that the URL for a READ operation just appends the key value to the URL of QUERY:e.g. https://<yourHost>/odata/v4/DemoService/People(3)
However, this is only a shortcut, which is only possible if the entity (e.g. Person) has only one key property
The complete notation is like this: Specify the property name and the value
e.g. https://<yourHost>/odata/v4/DemoService/People(UniqueId=3)
2) Second Step
Now that we have extracted the value of the UniquId of the desired person, we can go ahead and retrieve the desired “Person”-entry from the database.
In our dummy implementation, we just get the list of people, then access the person which was requested by our end-user:
List<Map<String, Object>> peopleList = getPeople();
Map<String, Object> requestedPersonMap = new HashMap<String, Object>();
for(Map<String, Object> personMap : peopleList) {
if(((Integer)personMap.get("UniqueId")).equals(id)) {
// found it
requestedPersonMap = personMap;
}
}
Note that in a productive usage you wouldn’t fetch the whole list, you would directly access one entry, using e.g. the appropriate SQL clause.
3) Third Step
We can use this opportunity in this blog, to also introduce some error handling.
Imagine, the end-user calls our service and passes a value for “UniqueId” which doesn’t exist (maybe because that person was removed)? In that case, we don’t find the requested Person in our database and our service has to react appropriately.
But what to do in such a weird situation?
So what we need is: our OData service has to send a response which is an error response and has an error message and the correct HTTP status code
These 3 requirements are met by The SDK, which has a suitable and convenient API:
We can just call setError(errorResponseObject) and pass an instance on a configurable ErrorResponse object:
return ReadResponse.setError(response);
The ErrorResponse object follows the builder pattern which allows method chaining for configuring the response with message and status code:
ErrorResponse response = ErrorResponse.getBuilder()
.setMessage("some error text")
.setStatusCode(404) // a valid HTTP status code
.response();
This pattern has the advantage that it requires less code.
Note:
In case you’re not familiar with HTTP status codes….
In such case you have to google for it.
The specification tells you which status code you have to set for each possible error (or success) case.
In this case, we’re setting 404, which is the code for not finding the requested resource.
OK, I’ve googled for you, here’s a link: HTTP RFC7231 section 6: Status Codes
You’ve understood: we’re responsible for setting the right number, the framework cannot know about problems occurring inside our data.
Note:
In most cases, we’ll be dealing with evil users who send bad requests, and we’ll be returning a status code starting with a 4, which indicates bad requestsNote:
Don’t worry, in future we won’t be setting a number, it makes sense to reuse existing constants in well-trusted and well-known librariesNote:
Don’t get confused about the difference in creating response in success case and in error case.
See:ReadResponse.setSuccess() returns a Builder
but
ReadResponse.setError(resp) returns a Response
Such that in success case we write:
return ReadResponse.setSuccess()…response();
But in error case we write:
ErrorResponse response = ErrorResponse.getBuilder()…response();
return ReadResponse.setError(response);
4) Fourth Step
Usually, whenever an error occurs, we like to write information into the log.
Here’s a simple way to do it :
Logger logger = LoggerFactory.getLogger(ServiceImplementation.class);
logger.error("Your text");
You’ll find your text in the console, resp. in the Cloud Foundry log of SAP Cloud Platform:
Finally, our error handling looks like this:
if(requestedPersonMap.isEmpty()) {
Logger logger = LoggerFactory.getLogger(ServiceImplementation.class);
logger.error("Person with UniqueId " + id + " doesn't exist! Service request was invoked with invalid key value");
ErrorResponse response = ErrorResponse.getBuilder()
.setMessage("Person with UniqueId " + id + " doesn't exist!")
.setStatusCode(404)
.response();
return ReadResponse.setError(response);
}
Deploy’n’Enjoy
Now that we’re done with today’s lesson, we can
rebuild with maven,
deploy with Cloud Foundry Client,
run with browser
QUERY
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People
READ
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People(1)
ERROR CASES
Call the service with an id that doesn’t exist:
e.g.
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People(-1)
or
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People(5)
The error message which we defined is displayed in the browser.
To see the status code, you have to open the developer tools. Most browsers support that via shortcut F12, or as well Ctrl+Shift+i
Once opened the developer tools, go to “Network” tab.
The screenshot below shows our error message and our Status Code:
Let’s try another error case.
This one:
https://<yourApp>.cfapps.sap.hana.ondemand.com/odata/v4/DemoService/People(UniqueId=’1′)
This kind of invalid input is handled by Olingo library, which returns a meaningful error message and the correct status code (400 which is Bad Request).
You may wonder: What’s wrong with 1 ???
Reason is that the type of the key property “UniqueId” is defined as Edm.Int32 (you can verify it in your edmx or in the $metadata document), but we’ve passed the value ‘2’ which is an Edm.String
The (small) difference is with the inverted commas
Note:
For such kind of error, our implementation is not even reached.
So no need to worry about handling such kind of errors.
You see how nice is it when frameworks make our life easier….
Summary
That’s it for today.
This blog was about reading operations
Next blog will be talking about modifying operations
Don’t miss it !
Links
Overview of blog series and link collection.
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="UniqueId" />
</Key>
<Property Name="UniqueId" Type="Edm.Int32" />
<Property Name="Name" Type="Edm.String" />
</EntityType>
<EntityContainer Name="container" >
<EntitySet Name="People" EntityType="demo.Person" />
</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.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.ErrorResponse;
import com.sap.cloud.sdk.service.prov.api.response.QueryResponse;
import com.sap.cloud.sdk.service.prov.api.response.ReadResponse;
public class ServiceImplementation {
@Query(serviceName="DemoService", entity="People")
public QueryResponse getPeople(QueryRequest request) {
List<Map<String, Object>> peopleMap = getPeople();
return QueryResponse.setSuccess().setDataAsMap(peopleMap).response();
}
@Read(serviceName="DemoService", entity="People")
public ReadResponse getPerson(ReadRequest request) {
// retrieve the requested person from URI
Map<String, Object> keyPredicates = request.getKeys();
Object keyObject = keyPredicates.get("UniqueId");
Integer id = (Integer)keyObject;
// search the requested person in the database
List<Map<String, Object>> peopleList = getPeople();
Map<String, Object> requestedPersonMap = new HashMap<String, Object>();
for(Map<String, Object> personMap : peopleList) {
if(((Integer)personMap.get("UniqueId")).equals(id)) {
// found it
requestedPersonMap = personMap;
}
}
// handle error: "not found"
if(requestedPersonMap.isEmpty()) {
Logger logger = LoggerFactory.getLogger(ServiceImplementation.class);
logger.error("Person with UniqueId " + id + " doesn't exist! Service request was invoked with invalid key value");
ErrorResponse response = ErrorResponse.getBuilder()
.setMessage("Person with UniqueId " + id + " doesn't exist!")
.setStatusCode(404)
.response();
return ReadResponse.setError(response);
}
return ReadResponse.setSuccess().setData(requestedPersonMap).response();
}
/* Dummy Database */
private List<Map<String, Object>> getPeople(){
List<Map<String, Object>> peopleMap = new ArrayList<Map<String, Object>>();
peopleMap.add(createPerson(0, "Anna"));
peopleMap.add(createPerson(1, "Berta"));
peopleMap.add(createPerson(2, "Claudia"));
peopleMap.add(createPerson(3, "Debbie"));
return peopleMap;
}
private Map<String, Object> createPerson(Integer id, String name){
Map<String, Object> personMap = new HashMap<String, Object>();
personMap.put("UniqueId", id);
personMap.put("Name", name);
return personMap;
}
}
BTW, please send me your feedback … e.g. if you find a compilation error which my compiler didn’t find, or any other suggestion.
And if you have suggestions for making the dummy code easier…. Please go ahead 😉
Hi Carlos,
I've made some slight modifications on the getPerson method to make usage of lambda expressions and make the code easier to understand:
Hope you enjoy!
Regards,
Ivan
Hi Ivan,
thanks very much for contributing!
You're a great buddy !
That's great and I'd like to encourage you and everybody to share similar suggestions!