Skip to content

Using the OData API

This section covers how to use the OData API to perform common queries, create/update/delete (CUD) operations, and service operations. Each section includes code for using the generated proxy classes with Dynamic API, followed by another code snippet using Dynamic API alone.

An instance of the DataService class is required to interact with the OData endpoint that it represents. It relies on parsed metadata from the service document provided by the OData endpoint. For proxy classes, a generated service class that is a subclass of DataService should be used.

Every query requires the construction of a corresponding DataQuery, passed as a parameter to the DataService executeQuery method. With proxy classes, you can use the getter methods for the entity set provided by the generated service class. Internally, those getter methods make use of DataQuery and executeQuery in their implementation. It is therefore important to have a good understanding of the DataQuery class to query an OData service provider.

The first couple of code samples include the lookup of the entity set, entity type, and property required for parameters when used without proxy classes. Subsequent samples skip these lookups to make the code easier to read. The following variable naming convention is used for entity set, entity type, and properties.

  • Entity type: <Entity Type Name>Type e.g. eventType
  • Entity Set: <Entity Type Name>Set e.g. eventSet
  • Property: <Entity Type Name><Property Name>Property e.g. eventEventIDProp

Query

Collection/Entity Set

// OData Query: /Events

// Proxy Class
// Use generated service class getter method for collection/entity set
// Implementation code creates a DataQuery to indicate the entity set to be queried
List<Event> events = eventService.getEvents();
for (Event event: events) {
    String eventName = event.getName();
    // Get the start date time of the event
    LocalDateTime startDateTime = event.getStartDateTime();
    ...
}

// Dynamic API
// Use DataService to look up the entity set with the name of the entity set element from the service document
// Get the entity set
EntitySet eventSet = dataService.getEntitySet("Events");
// Get the type of the entity
EntityType eventType = eventSet.getEntityType();
// Use the from method of DataQuery to specify the entity set to retrieve from
DataQuery query = new DataQuery().from(eventSet);
EntityValueList events = dataService.executeQuery(query).getEntityList();
// Get event name value from result
Property eventNameProp = eventType.getProperty("Name");
Property eventStartDateTimeProp = eventType.getProperty("StartDateTime");
for (EntityValue event : events) {
    String eventName = eventNameProp.getString(event);
    LocalDateTime startDateTime =
        LocalDateTime.castRequired(eventStartDateTimeProp.getValue(event));
    ...
}
// OData Query: /Events

// Proxy Class
// Use generated service class getter method for collection/entity set
// Implementation code creates a DataQuery to indicate the entity set to be queried
val events = eventService.events
for (event in events) {
    val eventName = event.name
    // Get the start date time of the event
    val startDateTime = event.startDateTime
    ...
}

// Dynamic API
// Use DataService to look up the entity set with the name of the entity set element from the service document
// Get the entity set
val eventSet = dataService.getEntitySet("Events")
// Get the type of the entity
val eventType = eventSet.entityType
// Use the from method of DataQuery to specify the entity set to retrieve from
val query = DataQuery().from(eventSet)
val events = dataService.executeQuery(query).entityList
// Get event name value from result
val eventNameProp = eventType.getProperty("Name")
val eventStartDateTimeProp = eventType.getProperty("StartDateTime")
for (event in events) {
    val eventName = eventNameProp.getString(event)
    val startDateTime =
        LocalDateTime.castRequired(eventStartDateTimeProp.getValue(event))
    ...
}

With the generated service class, you don't have to specify the entity set from which retrieval is to take place as it is already in its implementation. In addition, a native list of strongly typed class instead of a generic EntityValueList is returned.

Client-Driven Paging

The client determines the size of a page and uses the skip and top query options to request a specific page. This is known as client-driven paging.

// OData Query: /Events?$skip=100&$top=50
// Page size = 50, reading third page

// Proxy Class
// Use a query for getEvents
DataQuery query = new DataQuery().skip(100).top(50);
List<Event> events = eventService.getEvents(query);

// Dynamic API
DataQuery query = new DataQuery().from(eventSet).skip(100).top(50);
EntityValueList events = dataService.executeQuery(query).getEntityList();
// OData Query: /Events?$skip=100&$top=50
// Page size = 50, reading third page

// Proxy Class
// Use a query for getEvents
val query = DataQuery().skip(100).top(50)
val events = eventService.getEvents(query)

// Dynamic API
val query = DataQuery().from(eventSet).skip(100).top(50)
val events = dataService.executeQuery(query).entityList

Please be aware that if the membership of the collection is changing, paging may return an entity twice or not at all. This is how paging works against a changing collection.

InlineCount

InlineCount is a system query option that indicates that the response to the request includes a count of the number of entities satisfying the condition in the filter query option. If no filter query option is specified, it is assumed to be the entire collection. InlineCount is particular useful for client-driven paging to determine the total number of pages of entities satisfying the query, allowing it to prepare UI for a user to retrieve any one of the pages.

// OData: /Events?$inlinecount=allpages&$top=50&$skip=0

// Proxy Class
// When using executeQuery instead of getEvents or getEvent we need to
// specify which entity set the query is running against
DataQuery query = new DataQuery()
    .from(EntitySets.events)
    .skip(0)
    .top(50)
    .inlineCount();
QueryResult result = eventService.executeQuery(query);
long count = result.getInlineCount();
// a list of 50 events will be returned
List<Event> events = Event.list(result.getEntityList());

// Dynamic API
DataQuery query = new DataQuery()
    .from(eventSet)
    .skip(0)
    .top(50)
    .inlineCount();
QueryResult result = dataService.executeQuery(query);
long count = result.getInlineCount();
// a list of 50 events will be returned
EntityValueList events = result.getEntityList();
// OData: /Events?$inlinecount=allpages&$top=50&$skip=0

// Proxy Class
// When using executeQuery instead of getEvents or getEvent we need to
// specify which entity set the query is running against
val query = DataQuery()
    .from(EntitySets.events)
    .skip(0)
    .top(50)
    .inlineCount()
val result = eventService.executeQuery(query)
val count = result.inlineCount
// a list of 50 events will be returned
val events = Event.list(result.entityList)

// Dynamic API
val query = DataQuery()
    .from(eventSet)
    .skip(0)
    .top(50)
    .inlineCount()
val result = dataService.executeQuery(query)
val count = result.inlineCount
// a list of 50 events will be returned
val events = result.entityList

Server-Driven Paging

In server-driven paging, the server determines how much to send back for a request. It is possible to influence the server by specifying a page size preference. However, it is up to the server whether it will honor the setting. At the end of the server response is a nextLink URL for use by the client to fetch the next page of entities.

It is possible to get a count of the total number of entities via the InlineCount query option. However, the client can only read the next page via nextLink.

// Proxy Class
DataQuery pagedQuery = new DataQuery()
    .from(EntitySets.events)
    .page(10);
QueryResult result = eventService.executeQuery(pagedQuery);
List<Event> events = Event.list(result.getEntityList());
// Get the query based on the nextLink URI returned by the server
pagedQuery = result.getNextQuery();
// Ready to query the server again for the next page of entities

// Dynamic API
DataQuery pagedQuery = new DataQuery()
    .from(eventSet)
    .page(10);
QueryResult result = dataService.executeQuery(pagedQuery);
EntityValueList events = result.getEntityList();
// Get the query based on the nextLink URI returned by the server
pagedQuery = result.getNextQuery();
// Ready to query the server again for the next page of entities
// Proxy Class
var pagedQuery = DataQuery()
    .from(EntitySets.events)
    .page(10)
val result = eventService.executeQuery(pagedQuery)
val events = Event.list(result.entityList;
// Get the query based on the nextLink URI returned by the server
pagedQuery = result.nextQuery
// Ready to query the server again for the next page of entities

// Dynamic API
var pagedQuery = new DataQuery()
    .from(eventSet)
    .page(10)
val result = dataService.executeQuery(pagedQuery)
val events = result.entityList
// Get the query based on the nextLink URI returned by the server
pagedQuery = result.nextQuery
// Ready to query the server again for the next page of entities

Single Entity with Entity Key

The entity key is required to access a particular instance of the entity set. The getter method takes a DataQuery constructed with the entity key as the parameter.

// OData Query: /Events(1000L)

// Proxy Class
DataQuery query = new DataQuery().withKey(Event.key(1000L));
Event event = eventService.getEvent(query);

// Dynamic API
DataValue keyValue = LongValue.of(1000L);
// Note: composite key can be created using multiple with methods
// Assume Session requires a composite key of EventID and SessionID
// new EntityKey().with("EventID", keyValue).with("SessionID", idValue)
EntityKey eventKey = new EntityKey().with("EventID", keyValue);
DataQuery query = new DataQuery().from(eventSet).withKey(eventKey);
EntityValue event = dataService.executeQuery(query).getRequiredEntity();
// OData Query: /Events(1000L)

// Proxy Class
val query = DataQuery().withKey(Event.key(1000L))
val event = eventService.getEvent(query)

// Dynamic API
val keyValue = LongValue.of(1000L)
// Note: composite key can be created using multiple with methods
// Assume Session requires a composite key of EventID and SessionID
// new EntityKey().with("EventID", keyValue).with("SessionID", idValue)
val eventKey = EntityKey().with("EventID", keyValue)
val query = DataQuery().from(eventSet).withKey(eventKey)
val event = dataService.executeQuery(query).requiredEntity

If there is no event with the specified key, an exception will be thrown. This is due to the getRequiredEntity method used in the implementation of getEvent in the generated service class. With Dynamic API, it is possible to return null by using getOptionalEntity.

EntityValue event = dataService.executeQuery(query).getOptionalEntity();
val event = dataService.executeQuery(query).optionalEntity

Filter

You can create a filter to retrieve the subset of a collection that satisfies its criteria. An empty collection will be returned if there is no entity within the collection that satisfies the criteria. The withKey method of DataQuery essentially generates a filter using the entity key.

// OData Query: /Events?$filter=(Name eq 'SAP Tech Ed 2015')

// Proxy Class
DataQuery query = new DataQuery()
    .filter(Event.name.equal("SAP Tech Ed 2015"));
List<Event> events = eventService.getEvents(query);

// Dynamic API
DataQuery query = new DataQuery()
    .from(eventSet)
    .filter(eventNameProp.equal("SAP Tech Ed 2015"));
EntityValueList events = dataService.executeQuery(query).getEntityList();
// OData Query: /Events?$filter=(Name eq 'SAP Tech Ed 2015')

// Proxy Class
val query = DataQuery().filter(Event.name.equal("SAP Tech Ed 2015"))
val events = eventService.getEvents(query)

// Dynamic API
val query = DataQuery()
    .from(eventSet)
    .filter(eventNameProp.equal("SAP Tech Ed 2015"))
val events = dataService.executeQuery(query).entityList

For complex filtering criteria, consider using the QueryOperator within the filter.

// Find all sessions with registration >= 90%

// Proxy Class
DataQuery query = new DataQuery().filter(
    QueryOperator.multiply(Session.participants, IntValue.of(100))
        .divide(Session.maxParticipants)
        .greaterEqual(IntValue.of(90)));
List<Session> sessions = eventService.getSessions(query);

// Dynamic API
DataQuery query = new DataQuery()
    .from(sessionSet)
    .filter(
        QueryOperator.multiply(sessionParticipantsProp, IntValue.of(100))
            .divide(sessionMaxParticipantsProp)
            .greaterEqual(IntValue.of(90))
    );
EntityValueList sessions = dataService.executeQuery(query).getEntityList();
// Find all sessions with registration >= 90%

// Proxy Class
val query = DataQuery().filter(
    QueryOperator.multiply(Session.participants, IntValue.of(100))
        .divide(Session.maxParticipants)
        .greaterEqual(IntValue.of(90)))
val sessions = eventService.getSessions(query)

// Dynamic API
val query = DataQuery()
    .from(sessionSet)
    .filter(
        QueryOperator.multiply(sessionParticipantsProp, IntValue.of(100))
            .divide(sessionMaxParticipantsProp)
            .greaterEqual(IntValue.of(90))
    )
val sessions = dataService.executeQuery(query).entityList

Count

Count the number of entities in a collection, or the number of entities that satisfies a specific query. Note that the list of qualifying events is not returned, only the count.

// OData: /Events/$count

// Proxy Class
DataQuery query = new DataQuery().from(EntitySets.events).count();
long count = eventService.executeQuery(query).getCount();

// Dynamic API
DataQuery query = new DataQuery().from(eventSet).count();
long count = dataService.executeQuery(query).getCount();

// OData: /Events/$count&$filter=(Country eq 'USA')

// Proxy Class
DataQuery query = new DataQuery().from(EntitySets.events)
    .filter(QueryOperator.equal(Event.country, StringValue.of('USA')))
    .count();
long count = eventService.executeQuery(query).getCount();

// Dynamic API
DataQuery query = new DataQuery().from(eventSet)
    .filter(QueryOperator.equal(eventCountryProp, StringValue.of('USA')))
    .count();
long count = dataService.executeQuery(query).getCount();
// OData: /Events/$count

// Proxy Class
val query = DataQuery().from(EntitySets.events).count()
val count = eventService.executeQuery(query).count

// Dynamic API
val query = DataQuery().from(eventSet).count()
val count = dataService.executeQuery(query).count

// OData: /Events/$count&$filter=(Country eq 'USA')

// Proxy Class
val query = DataQuery().from(EntitySets.events)
        .filter(QueryOperator.equal(Event.country, StringValue.of('USA')))
        .count()
val count = eventService.executeQuery(query).count

// Dynamic API
val query = DataQuery().from(eventSet)
    .filter(QueryOperator.equal(eventCountryProp, StringValue.of('USA')))
    .count()
val count = dataService.executeQuery(query).count

Expand

To expand, specify a navigation property to retrieve associated entities. For example, when we retrieve an event, we can have all the sessions belonging to the event be returned inline.

// OData: /Events?$filter=(Name eq 'SAP Tech Ed 2015')&$expand=Sessions

// Proxy Class
DataQuery query = new DataQuery()
    .filter(Event.name.equal("SAP Tech Ed 2015"))
    .expand(Event.sessions);
List<Event> events = eventService.getEvents(query);

// Dynamic API
Property eventSessionsProp = eventType.getProperty("Sessions");
DataQuery query = new DataQuery().from(eventSet)
    .filter(eventNameProp.equal("SAP Tech Ed 2015"))
    .expand(eventSessionsProp);
EntityValueList events = dataService.executeQuery(query).getEntityList();
// OData: /Events?$filter=(Name eq 'SAP Tech Ed 2015')&$expand=Sessions

// Proxy Class
val query = DataQuery()
    .filter(Event.name.equal("SAP Tech Ed 2015"))
    .expand(Event.sessions)
val events = eventService.getEvents(query)

// Dynamic API
val eventSessionsProp = eventType.getProperty("Sessions")
val query = DataQuery().from(eventSet)
    .filter(eventNameProp.equal("SAP Tech Ed 2015"))
    .expand(eventSessionsProp)
val events = dataService.executeQuery(query).entityList

You can expand with multiple properties by passing a comma-separated list of navigation properties to expand or invoking expand multiple times. The following code fragments demonstrate how to do this using proxy classes.

// OData: /Events?$filter=(Name eq 'SAP Tech Ed 2015')&$expand=Sessions,Features

// Proxy Class
DataQuery query = new DataQuery()
    .filter(Event.name.equal("SAP Tech Ed 2015"))
    .expand(Event.sessions, Event.features);
List<Event> events = eventService.getEvents(query);

DataQuery query = new DataQuery()
    .filter(Event.name.equal("SAP Tech Ed 2015"))
    .expand(Event.sessions)
    .expand(Event.features);
List<Event> events = eventService.getEvents(query);
// OData: /Events?$filter=(Name eq 'SAP Tech Ed 2015')&$expand=Sessions,Features

// Proxy Class
val query = DataQuery()
    .filter(Event.name.equal("SAP Tech Ed 2015"))
    .expand(Event.sessions, Event.features)
val events = eventService.getEvents(query)

val query = DataQuery()
    .filter(Event.name.equal("SAP Tech Ed 2015"))
    .expand(Event.sessions)
    .expand(Event.features)
val events = eventService.getEvents(query)

Multi-level expansion is also possible with the use of the expandWithQuery method. The following example shows how to retrieve an event with sessions returned inline. In addition, the track entity associated with each session instance will also be returned inline.

// OData: /Events(1000L)?$expand=Sessions/Track

// Proxy Class
DataQuery query = new DataQuery()
    .withKey(Event.key(1000L))
    .expandWithQuery(
        Event.sessions,
        new DataQuery().expandWithQuery(Session.track, new DataQuery())
    );
List<Event> events = eventService.getEvents(query);

// Dynamic API
EntityKey eventKey = new EntityKey().with("EventID", LongValue.of(1000L));
DataQuery query = new DataQuery()
    .withKey(eventKey)
    .expandWithQuery(
        eventSessionsProp,
        new DataQuery().expandWithQuery(sessionTrackProp, new DataQuery())
    );
List<Event> events = eventService.getEvents(query);
// OData: /Events(1000L)?$expand=Sessions/Track

// Proxy Class
val query = DataQuery()
    .withKey(Event.key(1000L))
    .expandWithQuery(
        Event.sessions,
        DataQuery().expandWithQuery(Session.track, DataQuery())
    )
val events = eventService.getEvents(query)

// Dynamic API
val eventKey = EntityKey().with("EventID", LongValue.of(1000L))
val query = DataQuery()
    .withKey(eventKey)
    .expandWithQuery(
        eventSessionsProp,
        DataQuery().expandWithQuery(sessionTrackProp, DataQuery())
    )
val events = eventService.getEvents(query)

orderBy

Specify the orderBy system query option to sort the returned set of entities.

// OData: /Events?$orderby=Country asc,Name asc
// Sort by country in ascending order then by name in ascending order

// Proxy Class
DataQuery query = new DataQuery()
    .orderBy(Event.country, SortOrder.ASCENDING)
    .thenBy(Event.name, SortOrder.ASCENDING);
List<Event> events = eventService.getEvents(query);

// Dynamic API
DataQuery query = new DataQuery()
    .from(eventSet)
    .orderBy(eventCountryProp, SortOrder.ASCENDING)
    .thenBy(eventNameProp, SortOrder.ASCENDING);
EntityValueList events = dataService.executeQuery(query).getEntityList();
// OData: /Events?$orderby=Country asc,Name asc
// Sort by country in ascending order then by name in ascending order

// Proxy Class
val query = DataQuery()
    .orderBy(Event.country, SortOrder.ASCENDING)
    .thenBy(Event.name, SortOrder.ASCENDING)
val events = eventService.getEvents(query)

// Dynamic API
val query = DataQuery()
    .from(eventSet)
    .orderBy(eventCountryProp, SortOrder.ASCENDING)
    .thenBy(eventNameProp, SortOrder.ASCENDING)
val events = dataService.executeQuery(query).entityList

In some cases, you may want to sort by a property of the entity associated with the navigation property. For example, you may want to list the sessions sorted by the name of their associated track. To do so, create a DataPath as a parameter for the orderBy method.

// OData: /Sessions?$filter=(EventID eq 1000L)&$expand=Track&$orderby=Track/Name

// Proxy Class
// Create the path Track/Name
DataPath trackNamePath = Session.track.path(Track.name);
DataQuery sortQuery = new DataQuery()
    .filter(Session.eventID.equal(1000L))
    .expand(Session.track)
    .orderBy(trackNamePath);
List<Session> sessions = eventService.getSessions(query);

// Dynamic API
// Create the path Track/Name
DataPath trackNamePath = sessionTrackProp.path(trackNameProp);
DataQuery sortQuery = new DataQuery().from(setSession)
    .filter(sessionEventIDProp.equal(1000L))
    .expand(sessionTrackProp)
    .orderBy(trackNamePath);
EntityValueList sessions = dataService.executeQuery(query).getEntityList();
// OData: /Sessions?$filter=(EventID eq 1000L)&$expand=Track&$orderby=Track/Name

// Proxy Class
// Create the path Track/Name
val trackNamePath = Session.track.path(Track.name)
val sortQuery = DataQuery()
    .filter(Session.eventID.equal(1000L))
    .expand(Session.track)
    .orderBy(trackNamePath)
val sessions = eventService.getSessions(query)

// Dynamic API
// Create the path Track/Name
val trackNamePath = sessionTrackProp.path(trackNameProp)
val sortQuery = DataQuery().from(setSession)
    .filter(sessionEventIDProp.equal(1000L))
    .expand(sessionTrackProp)
    .orderBy(trackNamePath)
val sessions = dataService.executeQuery(query).entityList

Select

Use the select system query option to specify a subset of the structural properties to be returned with the query.

// OData: /Events?$select=Name,EventID,Country

// Proxy Class
DataQuery query = new DataQuery().select(Event.name, Event.eventID, Event.country);
List<Event> events = eventService.getEvents(query);

// Dynamic API
DataQuery query = new DataQuery()
    .from(eventSet)
    .select(eventNameProp, eventEventIDProp, eventCountryProp);
EntityValueList events = dataService.executeQuery(query).getEntityList();
// OData: /Events?$select=Name,EventID,Country

// Proxy Class
val query = DataQuery().select(Event.name, Event.eventID, Event.country)
val events = eventService.getEvents(query)

// Dynamic API
val query = DataQuery()
    .from(eventSet)
    .select(eventNameProp, eventEventIDProp, eventCountryProp)
val events = dataService.executeQuery(query).entityList

Exception will be thrown if non-selected properties are accessed.

loadEntity

Use loadEntity to retrieve an entity using its entity key or readLink. If both are set, entity key takes precedence. If neither is set, an exception will be thrown.

Using Entity Key

// Proxy Class
Event event = new Event();
event.setEventID(1000L);
eventService.loadEntity(event);

// Dynamic API
EntityValue event = EntityValue.ofType(eventType);
eventEventIDProp.setLong(event, 1000L);
dataService.loadEntity(event);
// Proxy Class
val event = Event()
event.eventID = 1000L
eventService.loadEntity(event)

// Dynamic API
val event = EntityValue.ofType(eventType)
eventEventIDProp.setLong(event, 1000L)
dataService.loadEntity(event)

Using ReadLink

// Proxy Class
Event event = new Event();
event.setReadLink("...");
eventService.loadEntity(event);

// Dynamic API
EntityValue event = EntityValue.ofType(eventType);
event.setReadLink("...");
dataService.loadEntity(event);
// Proxy Class
val event = Event()
event.readLink = "..."
eventService.loadEntity(event)

// Dynamic API
val event = EntityValue.ofType(eventType)
event.readLink = "..."
dataService.loadEntity(event)

When using proxy class, calling the default constructor will create a new event with each property set to its default value. Therefore, the following code will likely result in an "entity not found" exception due to the EventID being set to its default - zero.

Event event = new Event();
// Likely result in an entity not found exception because EventID is 0L
eventService.loadEntity(event);

// Create a new event without defaults for any property
event = new Event(false);
// An exception will be thrown because no identification is provided
eventService.loadEntity(event);
var event = Event()
// Likely result in an entity not found exception because EventID is 0L
eventService.loadEntity(event)

// Create a new event without defaults for any property
event = Event(false)
// An exception will be thrown because no identification is provided
eventService.loadEntity(event)

You can use LoadEntity to lazy load properties, especially navigation properties for an entity that has already been retrieved. For example, when the list of features of an event is to be retrieved and the details of the event is to be shown, avoiding the use of the potentially expensive expand query option in the initial query.

// Proxy Class
// An event is retrieved via a query, and its navigation properties are not selected
Event event = ...;
// Specify that the navigation properties, Features and Theme, are to be loaded
DataQuery expandQuery = new DataQuery().expand(Event.features, Event.theme);
eventService.loadEntity(event, expandQuery);

// Dynamic API
// An event is retrieved via a query, and its navigation properties are not selected
EntityValue event = ...;
// Specify that the navigation properties, Features and Theme, are to be loaded
DataQuery expandQuery = new DataQuery().expand(eventFeaturesProp, eventThemeProp);
dataService.loadEntity(event, expandQuery);
// Proxy Class
// An event is retrieved via a query, and its navigation properties are not selected
val event = ...
// Specify that the navigation properties, Features and Theme, are to be loaded
val expandQuery = DataQuery().expand(Event.features, Event.theme)
eventService.loadEntity(event, expandQuery)

// Dynamic API
// An event is retrieved via a query, and its navigation properties are not selected
val event = ...
// Specify that the navigation properties, Features and Theme, are to be loaded
val expandQuery = DataQuery().expand(eventFeaturesProp, eventThemeProp)
dataService.loadEntity(event, expandQuery)

loadProperty

While we can use loadEntity to retrieve properties of an entity, complex retrieval conditions can only be retrieved using loadProperty. In the example below, we are loading the top 50 sessions belonging to the event sorted by the name of the track associated with each session.

// Proxy Class
// An event is retrieved via a query
Event event = ...;
// Create a path to represent the name of the track associated with the session
DataPath path = Session.track.path(Track.name);
// Define the retrieval criteria for sessions associated with this particular event
DataQuery sortQuery = new DataQuery().expand(Session.track).top(50).orderBy(path);
eventService.loadProperty(Event.sessions, event, sortQuery);

// Dynamic API
// An event is retrieved via a query
EntityValue event = ...;
// Create a path to represent the name of the track associated with the session
DataPath path = sessionTrackProp.path(trackNameProp);
// Define the retrieval criteria for sessions associated with this particular event
DataQuery sortQuery = new DataQuery().expand(sessionTrackProp).top(50).orderBy(path);
dataService.loadProperty(eventSessionsProp, event, sortQuery);
// Proxy Class
// An event is retrieved via a query
val event = ...
// Create a path to represent the name of the track associated with the session
val path = Session.track.path(Track.name)
// Define the retrieval criteria for sessions associated with this particular event
val sortQuery = DataQuery().expand(Session.track).top(50).orderBy(path)
eventService.loadProperty(Event.sessions, event, sortQuery)

// Dynamic API
// An event is retrieved via a query
val event = ...
// Create a path to represent the name of the track associated with the session
val path = sessionTrackProp.path(trackNameProp)
// Define the retrieval criteria for sessions associated with this particular event
val sortQuery = DataQuery().expand(sessionTrackProp).top(50).orderBy(path)
dataService.loadProperty(eventSessionsProp, event, sortQuery)

If the EventID foreign key is exposed in Session entity type, we can do the same with a DataQuery against the Session collection.

// Proxy Class
DataPath path = Session.track.path(Track.name);
DataQuery query = new DataQuery().filter(Session.eventID.equal(1000L))
    .expand(Session.track)
    .top(50)
    .orderBy(path);
List<Session> sessions = eventService.getSessions(query);

// Dynamic API
DataPath path = sessionTrackProp.path(trackNameProp);
DataQuery query = new DataQuery().filter(sessionEventIDProp.equal(1000L))
    .expand(sessionTrackProp)
    .top(50)
    .orderBy(path);
EntityValueList sessions = dataService.executeQuery(query).getEntityList();
// Proxy Class
val path = Session.track.path(Track.name)
val query = DataQuery().filter(Session.eventID.equal(1000L))
    .expand(Session.track)
    .top(50)
    .orderBy(path)
val sessions = eventService.getSessions(query)

// Dynamic API
val path = sessionTrackProp.path(trackNameProp)
val query = new DataQuery().filter(sessionEventIDProp.equal(1000L))
    .expand(sessionTrackProp)
    .top(50)
    .orderBy(path)
val sessions = dataService.executeQuery(query).entityList

Media Download

OData provides two specific media metadata: media entity (think of a media entity as the metadata describing the binary data in the stream), and entity with stream properties. There are different strategies used to access online and offline media resources.

Offline

Offline only supports OData version 2 currently, and version 2 doesn’t support stream properties.

  1. Suppose FILE_DOWNLOADSET is an entityset, when you use something like:

    //OfflineDataDefiningQuery(String name, String query, boolean automaticallyRetrievesStreams)
    new OfflineDataDefiningQuery("FILE_DOWNLOADSET", "FILE_DOWNLOADSET", true);
    
    //OfflineDataDefiningQuery(String name, String query, boolean automaticallyRetrievesStreams)
    OfflineDataDefiningQuery("FILE_DOWNLOADSET", "FILE_DOWNLOADSET", true)
    

    Offline will first try to get all entities of the entity set by performing a GET /FILE_DOWNLOADSET request. And for each FILE_DOWNLOAD inside FILE_DOWNLOADSET, Offline will follow its media_src link to download media, if any: GET FILE_DOWNLOADSET(1)/$value.

    If you want to download an individual stream:

    //OfflineDataDefiningQuery(String name, String query, boolean automaticallyRetrievesStreams)
    new OfflineDataDefiningQuery("FILE_DOWNLOADSET", "FILE_DOWNLOADSET(1)", true);
    
    //OfflineDataDefiningQuery(String name, String query, boolean automaticallyRetrievesStreams)
    OfflineDataDefiningQuery("FILE_DOWNLOADSET", "FILE_DOWNLOADSET(1)", true)
    

    Offline will still need to call GET FILE_DOWNLOADSET(1) to get entity properties and metadata. And then follow the media_src link to download FILE_DOWNLOADSET(1)/$value.

    If, in that defining query, you set automaticallyRetrievesStreams to be true, it will also pull down the media stream for each media entity. At that point, you can access the stream locally by using the media stream's read link. If your automaticallyRetrievesStreams is set to false, it will still pull down the media entities (i.e. the metadata) but not the streams, so if you try to access the stream locally, it won't find anything. In that scenario, you would need to request() that the stream for individual media entities be downloaded by adding defining requests on a per-media entity basis, and then downloading/refreshing, at which point the stream can be accessed locally.

    Note

    However, we suggest you set automaticallyRetrievesStreams to be true, as the back-end service may not implement GET requests for individual entities.

  2. After the media entity is pulled down along with its media stream, use:

    DataService.downloadMediaAsync(entity, successHandler, failureHandler);
    
    DataService.downloadMediaAsync(entity, successHandler, failureHandler)
    

    to load the media stream from the media entity. successHandler is called when the entity is successfully loaded and failureHandler is called if a failure occurred during loading.

  3. If the entity is successfully loaded, its media stream can be decoded using BitmapFactory. Usually, this is implemented in successHandler. Here is a simple example:

    DataService.downloadMediaAsync(mediaEntity, media -> {
            Drawable image = new BitmapDrawable(Resources, BitmapFactory.decodeByteArray(media, 0, media.length));
        }, error -> {
            LOGGER.debug("Error encountered during load of media resource", error);
        });
    
    DataService.downloadMediaAsync(mediaEntity, { media ->
            val image = BitmapDrawable(Resources, BitmapFactory.decodeByteArray(media, 0, media.size))
        }, { error ->
            LOGGER.debug("Error encountered during load of media resource", error)
        })
    

    In the example above, the media stream is decoded by BitmapFactory and then passed to BitmapDrawable to generate an image. Otherwise, an error message will be generated.

Additional information can be found: Offline OData Media Resources

Online

In this scenario, you don't have to download entity sets in advance. Data is downloaded by the application using the OData service base URL. So, the only concern is how to access the media resource.

  1. Because the way to access media data of a media entity and an entity with stream properties is different, we first have to check each entity as to whether it is a media entity or an entity with stream properties. Suppose EntityValue is the entity that you want to handle, use

    EntityValue.getEntityType().isMedia();
    
    EntityValue.entityType.isMedia
    

    to check if it is a media entity. Use the code below to check whether it has stream properties:

    EntityValue.getEntityType().getStreamProperties().length() > 0
    
    EntityValue.entityType.streamProperties.length() > 0
    

    If either of the above checks returns True, we can move to next step.

  2. Unlike the Offline scenario, we need the media resource URL as well as the OData base URL to pull down the media stream for each entity.

    • Media entity

      For media entities, use the following to get the media resource URL.

      String mediaLinkURL = EntityValue.getMediaStream().getReadLink();
      
      val mediaLinkURL = EntityValue.mediaStream.readLink
      
    • Entity with stream properties

      An entity could have zero or more stream properties. In that case, you may have to iterate the PropertyList. The following example demonstrates how to get (the first) one stream property's URL.

      PropertyList ResourceProperties = EntityValue.getEntityType().getStreamProperties();
      String mediaLinkURL = ResourceProperties.first().getStreamLink(EntityValue).getReadLink();
      
      val ResourceProperties = EntityValue.entityType.streamProperties()
      val mediaLinkURL = ResourceProperties.first().getStreamLink(EntityValue).readLink
      

    Note

    Media resource URL could be null.

    After obtaining the media resource URL, pass it, combined with OData base URL, to some kind of RequestManager to load. In the following example, Glide.with() is a RequestManager.

    Glide.with(currentActivity).load(ODATA_URL + mediaLinkURL);
    
    Glide.with(currentActivity).load(ODATA_URL + mediaLinkURL)
    

Custom Query

Developers who are familiar with the OData protocol can specify the OData requests directly. However, the correctness of the requests and using the QueryResult appropriately are also the responsibility of the developers. In addition, the query options described previously e.g. filter, expand, etc., are ignored for a custom query.

// Proxy Class
DataQuery query = new DataQuery()
    .withKey(Event.key(1000L))
    .expand(Event.features);
// Load the event and associated features specified by the key
Event event = eventService.getEvent(query);
// Get the readLink for the event returned
String readLink = event.getReadLink();

// Create custom query to expand on features associated with event
// This query does the same as the previously query
query = new DataQuery()
    .withURL(readLink + "?$expand=" + Event.features.getName());
event = eventService.getEvent(query);

// Dynamic API
DataValue keyValue = LongValue.of(1000L);
EntityKey eventKey = new EntityKey().with("EventID", keyValue);
DataQuery query = new DataQuery().from(eventSet)
    .withKey(eventKey)
    .expand(eventFeaturesProp);
// Load the event and associated features specified by the key
EntityValue event = dataService.executeQuery(query).getRequiredEntity();
// Get the readLink for the event returned
String readLink = event.getReadLink();

// Create custom query to expand on features associated with event
// This query does the same as the previously query
query = new DataQuery()
    .withURL(readLink + "?$expand=" + eventFeaturesProp.getName());
event = dataService.executeQuery(query).getRequiredEntity();
// Proxy Class
var query = DataQuery().withKey(Event.key(1000L)).expand(Event.features)
// Load the event and associated features specified by the key
var event = eventService.getEvent(query)
// Get the readLink for the event returned
val readLink = event.readLink

// Create custom query to expand on features associated with event
// This query does the same as the previously query
query = DataQuery()
    .withURL(readLink + "?$expand=" + Event.features.getName())
event = eventService.getEvent(query)

// Dynamic API
val keyValue = LongValue.of(1000L)
val eventKey = EntityKey().with("EventID", keyValue)
var query = DataQuery().from(eventSet)
    .withKey(eventKey)
    .expand(eventFeaturesProp)
// Load the event and associated features specified by the key
val event = dataService.executeQuery(query).requiredEntity
// Get the readLink for the event returned
val readLink = event.readLink

// Create custom query to expand on features associated with event
// This query does the same as the previously query
query = new DataQuery()
    .withURL(readLink + "?$expand=" + eventFeaturesProp.getName())
event = dataService.executeQuery(query).requiredEntity

CUD Operations

The required parameter is an entity of the entity type operated on. Update is usually performed on an entity retrieved from the provider. However, all three operations can use a new entity as well.

CreateEntity

// Proxy Class
Event event = new Event();
// Using type specific setters to set the property value
event.setName("A New Event");
event.setType(("Public");
event.setDescription("Another event...");
// Set start date time to be a local date time: 7/4/2018 08:15:00
event.setStartDateTime(LocalDateTime.of(2018, 7, 4, 8, 15, 0));
...
eventService.createEntity(event);

// Dynamic API
// Create an EntityValue of the entity type of Event
EntityValue event = EntityValue.ofType(eventType);
eventNameProp.setString(event, "A New Event");
eventTypeProp.setString(event, "Public");
eventDescriptionProp.setString(event, "Another event...");
// Set start date time to be a local date time: 7/4/2018 08:15:00
eventStartDateTimeProp.setValue(event, LocalDateTime.of(2018, 7, 4, 8, 15, 0));
...
dataService.createEntity(event);
// Proxy Class
val event = Event()
// Using type specific setters to set the property value
event.name = "A New Event"
event.type = "Public"
event.description = "Another event..."
// Set start date time to be a local date time: 7/4/2018 08:15:00
event.startDateTime = LocalDateTime.of(2018, 7, 4, 8, 15, 0)
...
eventService.createEntity(event)

// Dynamic API
// Create an EntityValue of the entity type of Event
val event = EntityValue.ofType(eventType)
eventNameProp.setString(event, "A New Event")
eventTypeProp.setString(event, "Public")
eventDescriptionProp.setString(event, "Another event...")
// Set start date time to be a local date time: 7/4/2018 08:15:00
eventStartDateTimeProp.setValue(event, LocalDateTime.of(2018, 7, 4, 8, 15, 0))
...
dataService.createEntity(event)

UpdateEntity

The typical flow for update is:

  1. Retrieve the entity
  2. Examine/Display properties to be changed
  3. Set properties to their new values

  4. Invoke the updateEntity method, passing in the entity

By default, the library uses merge and only sends changed properties to the OData service.

// Proxy Class
// Read Event entity from provider
Event event = …;
event.setCountry("Canada");
eventService.updateEntity(event);

// Dynamic API
// Read event entity from provider
EntityValue event = …;
eventCountryProp.setString(event, "Canada");
dataService.updateEntity(event);
// Proxy Class
// Read Event entity from provider
val event = …
event.country = "Canada"
eventService.updateEntity(event)

// Dynamic API
// Read event entity from provider
val event = …
eventCountryProp.setString(event, "Canada")
dataService.updateEntity(event)

If there is no need to review existing values, change the properties as follows:

  1. Construct a new entity
  2. Set either primary key or editLink
  3. Invoke the rememberOld method, which saves the current value of all properties
  4. Set properties to their new values
  5. Invoke the UpdateEntity method, passing in the new entity
// Proxy Class
Event event = new Event();
// Set the entity key of the event we want to update
event.setEventID(1000L);
// sets old value to current value. Note that we need old and new to determine
// what has changed
event.rememberOld();
// update the property
event.setCountry("Canada");
eventService.updateEntity(event);

// Dynamic API
EntityValue event = EntityValue.ofType(eventType);
// Set the entity key of the event we want to update
eventEventIDProp.setLong(event, 1000L);
// sets old value to current value. Note that we need old and new to determine
// what has changed
event.rememberOld();
eventCountryProp.setString(event, "Canada");
dataService.updateEntity(event);
// Proxy Class
val event = Event()
// Set the entity key of the event we want to update
event.eventID = 1000L
// sets old value to current value. Note that we need old and new to determine
// what has changed
event.rememberOld()
// update the property
event.country = "Canada"
eventService.updateEntity(event)

// Dynamic API
val event = EntityValue.ofType(eventType)
// Set the entity key of the event we want to update
eventEventIDProp.setLong(event, 1000L)
// sets old value to current value. Note that we need old and new to determine
// what has changed
event.rememberOld()
eventCountryProp.setString(event, "Canada")
dataService.updateEntity(event)

If you want to use the replace semantic instead of merge for a particular update, pass RequestOptions as the second parameter. Because the entire entity is replaced, be sure to first read the existing entity before changing any properties.

// Proxy Class
RequestOptions updateOptions = new RequestOptions();
updateOptions.setUpdateMode(UpdateMode.REPLACE);
// Important: Read the entity in first
Event event = ...;
// Change properties...
event.setName("Test Event");
// Use a variant of updateEntity that supports the passing in of
// http headers and request options
eventService.updateEntity(event, new HttpHeaders(), updateOptions);
// Proxy Class
val updateOptions = RequestOptions()
updateOptions.updateMode = UpdateMode.REPLACE
// Important: Read the entity in first
val event = ...
// Change properties...
event.name = "Test Event"
// Use a variant of updateEntity that supports the passing in of
// http headers and request options
eventService.updateEntity(event, new HttpHeaders(), updateOptions)

If the OData service only supports HTTP PUT, turn off the merge option for the OData online/offline provider:

provider.getServiceOptions().setSupportsPatch(false);
provider.getServiceOptions().setSupportsPatch(false)

deleteEntity

// Proxy Class
Event event = new Event();
// Set the entity key of the event we want to delete
event.setEventID(1000L);
eventService.deleteEntity(event);

// Dynamic API
EntityValue entityValue = EntityValue.ofType(eventType);
// Set the entity key of the event we want to delete
eventEventIDProp.setLong(entityValue, 1000L);
dataService.deleteEntity(entityValue);
// Proxy Class
val event = Event()
// Set the entity key of the event we want to delete
event.eventID = 1000L
eventService.deleteEntity(event)

// Dynamic API
val entityValue = EntityValue.ofType(eventType)
// Set the entity key of the event we want to delete
eventEventID.setLong(entityValue, 1000L)
dataService.deleteEntity(entityValue)

If you want to delete multiple entities, you can use the deleteByQuery method. Because there is no OData equivalent, Dynamic API first executes the query to retrieve the list of entities to be deleted, and then uses batch processing and ChangeSet to apply all the deletes in one request.

Note

Batch processing and ChangeSet are described in later sections.

// Proxy Class
DataQuery query = new DataQuery()
    .from(eventSet)
    .filter(Event.country.equal("USA"));
eventService.deleteByQuery(query);

// Dynamic API
DataQuery query = new DataQuery()
    .from(eventSet)
    .filter(eventCountryProp.equal("USA"));
dataService.deleteByQuery(query);
// Proxy Class
val query = DataQuery()
    .from(eventSet)
    .filter(Event.country.equal("USA"))
eventService.deleteByQuery(query)

// Dynamic API
val query = DataQuery()
    .from(eventSet)
    .filter(eventCountryProp.equal("USA"))
dataService.deleteByQuery(query)

Relations Between Entities

There are multiple ways to establish relationships between entities in OData:

  1. Establish or maintain a relationship between existing entities
  2. Create a new entity and establish a relationship with an existing entity
  3. Create new entities and establish relationships between them

Note

In some data models, the foreign keys are exposed as a property, so you can establish relations by setting the property to the entity key value of the related entity. However, with many-to-many relations, we rely on one of the ways described below.

Existing Entities

Use link commands to create, delete, or modify relations between existing entities via the navigation property. Note that for offline, bidirectional many-to-many relations require multiple actions. Please refer to the offline documentation for additional information.

Establish a relation between two entities. The navigation property can have a to-one or to-many cardinality .

// Proxy Class
// Load the event to which we want to add a feature association
Event event = …;
// Load the feature that we want the event to be associated with
Feature feature = …;
// Link feature to event
// Note Event.features is the navigation property from event to features
eventService.createLink(event, Event.features, feature);

// Dynamic API
// Load the event to which we want to add a feature association
EntityValue event = …;
// Load the feature that we want the event to be associated with
EntityValue feature = …;
// Link feature to event
dataService.createLink(event, eventFeaturesProp, feature);
// Note that there is no difference between proxy class and Dynamic API in Kotlin
// as it is able to determine the data type automatically
// Proxy Class
// Load the event to which we want to add a feature association
val event = …
// Load the feature that we want the event to be associated with
val feature = …
// Link feature to event
// Note Event.features is the navigation property from event to features
eventService.createLink(event, Event.features, feature)

// Dynamic API
// Load the event to which we want to add a feature association
val event = …
// Load the feature that we want the event to be associated with
val feature = …
// Link feature to event
dataService.createLink(event, eventFeaturesProp, feature)

Delete a relation between two entities. The navigation property can have to-one or to-many cardinality.

// Proxy Class
eventService.deleteLink(event, Event.features, feature);

// Dynamic API
dataService.deleteLink(event, eventFeaturesProp, feature);
// Proxy Class
eventService.deleteLink(event, Event.features, feature)

// Dynamic API
dataService.deleteLink(event, eventFeaturesProp, feature)

If one of the entities is deleted via deleteEntity, any existing relations with other entities will be deleted as well.

This method can only be used for navigation properties having a cardinality of one. Using it on a to-many navigation property will result in an exception. It is always possible to use deleteLink and createLink to accomplish the same. In the example below, each event can be associated with no or one theme.

// Proxy Class
eventService.updateLink(event, Event.Theme, anotherTheme);

// Dynamic API
dataService.updateLink(event, eventThemeProp, anotherTheme);
// Proxy Class
eventService.updateLink(event, Event.Theme, anotherTheme)

// Dynamic API
dataService.updateLink(event, eventThemeProp, anotherTheme)

Create New Entity and Associate with an Existing Entity

There are two ways to create a new entity and associate it with an existing entity in OData. While they are functionally the same, an OData service may support only one.

bindEntity

Creates the new entity against its own entity set. The new entity is bound to an existing entity, with which the relationship is to be established.

// OData: POST /Sessions

// Proxy Class
// Create a session
Session session = new Session(false);
// set properties...
// Locate the event to associate with
Event event = …;
// Bind the new session to the specified event
session.bindEntity(event, Session.event);
eventService.createEntity(session);

// Dynamic API
// Create a session
EntityValue session = EntityValue.ofType(sessionType);
// Set properties...
// Locate event to be associated with
EntityValue event = ...;
// Bind the new session to the specified event
session.bindEntity(event, sessionEventProp);
dataService.createEntity(session);
// OData: POST /Sessions

// Proxy Class
// Create a session
val session = Session(false)
// set properties...
// Locate the event to associate with
val event = …
// Bind the new session to the specified event
session.bindEntity(event, Session.event)
eventService.createEntity(session)

// Dynamic API
// Create a session
val session = EntityValue.ofType(sessionType)
// Set properties...
// Locate event to be associated with
val event = ...
// Bind the new session to the specified event
session.bindEntity(event, sessionEventProp)
dataService.createEntity(session)

createRelatedEntity creates the new entity against the navigation property of the entity with which the relationship is to be established. The difference between this and bindEntity is that this operation is against the entity set of the entity to be associated with.

// OData: POST /Events(1000L)/Sessions

// Proxy Class
// Create a session
Session session = new Session(false);
// set properties...
// Locate the event to associate with
Event event = ...;
// Bind the new session to the specified event
eventService.createRelatedEntity(session, event, Event.sessions);

// Dynamic API
// Create a session
EntityValue session = EntityValue.ofType(sessionType);
// Set properties...
// Locate the event to associate with
EntityValue event = ...;
// Bind the new session to the specified event
dataService.createRelatedEntity(session, event, eventSessionsProp);
// OData: POST /Events(1000L)/Sessions

// Proxy Class
// Create a session
val session = Session(false)
// set properties...
// Locate the event to associate with
Event event = ...
// Bind the new session to the specified event
eventService.createRelatedEntity(session, event, Event.sessions)

// Dynamic API
// Create a session
val session = EntityValue.ofType(sessionType)
// Set properties...
// Locate the event to associate with
val event = ...
// Bind the new session to the specified event
dataService.createRelatedEntity(session, event, eventSessionsProp)

Create New Entities with Associations

You can create new entities with associations using a deep insert or batch processing. Batch processing is discussed in a subsequent section.

Deep Insert

Deep insert refers to the creation of an entity that includes related entities. The operation is atomic and either all entities are created, or none is. It is most often used to create hierarchical data, such as a sales order with line items, but can also be used for non-containment relations.

// Proxy Class
// Create new event and set its properties
Event event = ...;

// Create a session and set its properties
Session session1 = ...;
// Create a second session and set its properties
Session session2 = ...;

// Create Session List
List<Session> sessions = new ArrayList<>();
sessions.add(session1);
sessions.add(session2);
event.setSessions(sessions);
// Create new event with associated sessions
eventService.createEntity(event);

// Dynamic API
// Create new event and set its properties
EntityValue event = EntityValue.ofType(eventType);
...

// Create a session and set its properties
EntityValue session1 = EntityValue.ofType(sessionType);
...
// Create a second session and set its properties
EntityValue session2 = EntityValue.ofType(sessionType);
...

// Create Session List
EntityValueList sessions = new EntityValueList();
sessions.add(session1);
sessions.add(session2);
eventSessionsProps.setEntityList(event, sessions);
// Create new event with associated sessions
dataService.createEntity(event);
// Proxy Class
// Create new event and set its properties
val event = ...

// Create a session and set its properties
val session1 = ...
// Create a second session and set its properties
val session2 =...

// Create Session List
val sessions = ArrayList<Session>()
sessions.add(session1)
sessions.add(session2)
event.sessions = sessions
// Create new event with associated sessions
eventService.createEntity(event)

// Dynamic API
// Create new event and set its properties
val event = EntityValue.ofType(eventType)
...

// Create a session and set its properties
val session1 = EntityValue.ofType(sessionType)
...
// Create a second session and set its properties
val session2 = EntityValue.ofType(sessionType)
...

// Create Session List
val sessions = EntityValueList()
sessions.add(session1)
sessions.add(session2)
eventSessionsProps.setEntityList(event, sessions)
// Create new event with associated sessions
dataService.createEntity(event)

Service Operations

The generated service class provides a corresponding method for each service operation in the metadata document. The preferred way is to invoke service operations via these generated methods. With Dynamic API, use executeQuery and DataMethodCall to invoke service operations.

// Proxy Class
Long count = eventService.getSessionAttendeesCount(session.getSessionID());

// Dynamic API
EntityValue session = ...;
DataMethod dataMethod = dataService.getDataMethod("getSessionAttendeesCount");
Long sessionID = sessionSessionIDProp.getLong(session);
DataMethodCall methodCall = DataMethodCall.apply(
    dataMethod,
    new ParameterList(1).with("SessionInstanceID", LongValue.of(sessionInstanceID)));
DataQuery query = new DataQuery();
query.setMethodCall(methodCall);
// Use DataService executeQuery
Long count = LongValue.toNullable(dataService.executeQuery(query).getResult());
// Proxy Class
val count = eventService.getSessionAttendeesCount(session.getSessionID())

// Dynamic API
val session = ...
val dataMethod = dataService.getDataMethod("getSessionAttendeesCount")
val sessionID = sessionSessionIDProp.getLong(session)
val methodCall = DataMethodCall.apply(
    dataMethod,
    ParameterList(1).with("SessionInstanceID", LongValue.of(sessionInstanceID)))
val query = DataQuery()
query.setMethodCall(methodCall)
// Use DataService executeQuery
val count = LongValue.toNullable(dataService.executeQuery(query).result)

If the service operation returns a list of entities, you can use getEntityList instead of getResult.

Alternately, use the convenience method executeMethod to simplify invocation.

// Use DataService executeMethod
DataMethod dataMethod = dataService.getDataMethod("getSessionAttendeesCount");
Long count = LongValue.toNullable(
    dataService.executeMethod(
        dataMethod,
        new ParameterList(1).with("SessionInstanceID", LongValue.of(sessionInstanceID))
    ));
// Use DataService executeMethod
val dataMethod = dataService.getDataMethod("getSessionAttendeesCount")
val count = LongValue.toNullable(
    dataService.executeMethod(
        dataMethod,
        ParameterList(1).with("SessionInstanceID", LongValue.of(sessionInstanceID))
    ))

Batch Processing

OData allows you to submit an ordered series of query operations and/or change sets. To leverage this capability, use the ChangeSet and RequestBatch classes. Note that all requests within a batch are guaranteed to be executed in the order they are listed. The usage of these classes is the same regardless of whether you use proxy class or Dynamic API, although generated artifacts can simplify the construction of data queries and modification requests.

The status code returned from a batch is always 202. No exception is thrown by processBatch should any of the queries or change sets within it fail. It is the developer's responsibility to check the status of queries or change sets via getStatus.

Data Query

The data query added to the batch must specify the entity set that it is querying against.

// Prepare queries
DataQuery query1 = new DataQuery().from(sessionSet);
DataQuery query2 = new DataQuery().from(eventSet).withKey(Event.key(999L));
DataQuery query3 = new DataQuery().from(eventSet).orderBy(Event.name);
// Create a new batch request
RequestBatch batch = new RequestBatch();
// Add the data queries to the batch
batch.addQuery(query1);
batch.addQuery(query2);
batch.addQuery(query3);
// Execute batch
dataService.processBatch(batch);
// Get results for each data query
QueryResult result1 = batch.getQueryResult(query1);
QueryResult result2 = batch.getQueryResult(query2);
QueryResult result3 = batch.getQueryResult(query3);

if (result1.getStatus() == 200) {
    // Convert returned EntityValueList to a strongly typed List
    List<Session> sessions = Session.list(result1.getEntityList());
}
if (result2.getStatus() == 200) {
    // Convert returned EntityValueList to a strongly typed List
    List<Event> events = Event.list(result2.getEntityList());
}
if (result3.getStatus() == 200) {
    // Convert returned EntityValueList to a strongly typed List
    List<Event> orderedEvents = Event.list(result3.getEntityList());
}
// Prepare queries
val query1 = DataQuery().from(sessionSet)
val query2 = DataQuery().from(eventSet).withKey(Event.key(999L))
val query3 = DataQuery().from(eventSet).orderBy(Event.name)
// Create a new batch request
val batch = RequestBatch()
// Add the data queries to the batch
batch.addQuery(query1)
batch.addQuery(query2)
batch.addQuery(query3)
// Execute batch
dataService.processBatch(batch)
// Get results for each data query
val result1 = batch.getQueryResult(query1)
val result2 = batch.getQueryResult(query2)
val result3 = batch.getQueryResult(query3)

if (result1.status == 200) {
    // Convert returned EntityValueList to a strongly typed List
    val sessions = Session.list(result1.entityList)
}
if (result2.status == 200) {
    // Convert returned EntityValueList to a strongly typed List
    val events = Event.list(result2.entityList)
}
if (result3.status == 200) {
    // Convert returned EntityValueList to a strongly typed List
    val orderedEvents = Event.list(result3.entityList)
}

If an error was encountered during execution of the query, use QueryResult to get more information.

int status = result1.getStatus();
if (status != 200) {
    // Get the exception returned by the provider
    DataServiceException dsEx = result1.getError();
    ErrorResponse response = dsEx.getResponse();
    // Service-defined error code, serving as a sub-status for the HTTP error code
    // specified in the response
    String serverStatusCode = response.getCode();
    // Language dependent error message
    String serverMessage = response.getMessage();
    // Target of the particular error e.g. the name of the property in error
    String target = response.getTarget();
    // Server-defined and should only be used in development for OData
    String innerDetails = response.getInnerDetails();
    // OData allows for server to return a list of error responses
    // This is totally server dependent
    ErrorResponseList erl = response.getDetails();
    ...
}
val status = result1.status
if (status != 200) {
    // Get the exception returned by the provider
    val dsEx = result1.error
    val response = dsEx.response
    // Service-defined error code, serving as a sub-status for the HTTP error code
    // specified in the response
    val serverStatusCode = response.code
    // Language dependent error message
    val serverMessage = response.message
    // Target of the particular error e.g. the name of the property in error
    val target = response.target
    // Server-defined and should only be used in development for OData
    val innerDetails = response.innerDetails
    // OData allows for server to return a list of error responses
    // This is totally server dependent
    val erl = response.details
    ...
}

ChangeSet

A change set is an atomic unit of work that is made up of an unordered group of one or more CUD operations. Note that change sets cannot contain retrieve requests and cannot be nested. The status of a change set is only relevant when a request has failed, in which case the status of the failed request is returned.

The following example shows a change set and a query enclosed in a batch request.

RequestBatch batch = new RequestBatch();
ChangeSet updateChangeSet = new ChangeSet();

// Update 2 events in change set
event1.setLocation("The Hague");
updateChangeSet.updateEntity(event1);
event2.setBanner("http://abc.com/banner");
updateChangeSet.updateEntity(event2);
// Add change set to batch
batch.addChanges(updateChangeSet);
// Add query to batch
DataQuery query = new DataQuery().from(eventSet);
batch.addQuery(query);
// Process batch
eventService.processBatch(batch);

// Retrieve result from query
QueryResult result = batch.getQueryResult(query);
int queryStatus = result.getStatus();
List<Event> events = Event.list(result.getEntityList());

// Check response status for change set
int changeSetStatus = updateChangeSet.getStatus();
// Note: batch.getChangeSet(0) is updateChangeSet
// In a batch, everything is executed in order and
// we can retrieve change sets in the order they are added to the batch
if (changeSetStatus >= 400) {
    // Get the exception returned by the provider
    DataServiceException dsEx = updateChangeSet.getError();
    // Get error response
    ErrorResponse response = dsEx.getResponse();
    ...
}
val batch = RequestBatch()
val updateChangeSet = ChangeSet()

// Update 2 events in change set
event1.location = "The Hague"
updateChangeSet.updateEntity(event1)
event2.banner = "http://abc.com/banner"
updateChangeSet.updateEntity(event2)
// Add change set to batch
batch.addChanges(updateChangeSet)
// Add query to batch
val query = DataQuery().from(eventSet)
batch.addQuery(query)
// Process batch
eventService.processBatch(batch)

// Retrieve result from query
val result = batch.getQueryResult(query)
val queryStatus = result.status
val events = Event.list(result.entityList)

// Check response status for change set
val changeSetStatus = updateChangeSet.status
// Note: batch.getChangeSet(0) is updateChangeSet
// In a batch, everything is executed in order and
// we can retrieve change sets in the order they are added to the batch
if (changeSetStatus >= 400) {
    // Get the exception returned by the provider
    val dsEx = updateChangeSet.error
    // Get error response
    val response = dsEx.response
    ...
}

Using Batch to Create Entities and Associations

This is an alternative to performing a deep insert, as described earlier, and is done via content-ID referencing. While OData stipulates that requests within a change set are unordered, using a content-ID reference within the change set should allow the server to execute requests in the proper order.

// Proxy Class
// Create a new event and set its properties
Event event = ...;
// Create 2 new sessions belonging to the new event
Session session1 = ...;
Session session2 = ...;
ChangeSet changeSet = new ChangeSet();
changeSet.createEntity(event);
changeSet.createRelatedEntity(session1, event, Event.sessions);
changeSet.createRelatedEntity(session2, event, Event.sessions);
// The method applyChanges will create a new batch to contain the change set
eventService.applyChanges(changeSet);

// Dynamic API
// Create a new event and set its properties
EntityValue event = EntityValue.ofType(eventType);
...
// Create 2 new sessions belonging to the new event
EntityValue session1 = EntityValue.ofType(sessionType);
...
EntityValue session2 = EntityValue.ofType(sessionType);
...
ChangeSet changeSet = new ChangeSet();
changeSet.createEntity(event);
changeSet.createRelatedEntity(session1, event, eventSessionsProp);
changeSet.createRelatedEntity(session2, event, eventSessionsProp);
// The method applyChanges will create a new batch to contain the change set
dataService.applyChanges(changeSet);
// Proxy Class
// Create a new event and set its properties
val event = ...
// Create 2 new sessions belonging to the new event
val session1 = ...
val session2 = ...
val changeSet = ChangeSet()
changeSet.createEntity(event)
changeSet.createRelatedEntity(session1, event, Event.sessions)
changeSet.createRelatedEntity(session2, event, Event.sessions)
// The method applyChanges will create a new batch to contain the change set
eventService.applyChanges(changeSet)

// Dynamic API
// Create a new event and set its properties
val event = EntityValue.ofType(eventType)
...
// Create 2 new sessions belonging to the new event
val session1 = EntityValue.ofType(sessionType)
...
val session2 = EntityValue.ofType(sessionType)
...
val changeSet = ChangeSet()
changeSet.createEntity(event)
changeSet.createRelatedEntity(session1, event, eventSessionsProp)
changeSet.createRelatedEntity(session2, event, eventSessionsProp)
// The method applyChanges will create a new batch to contain the change set
dataService.applyChanges(changeSet)

Serialization

During the Android application development process, developers often have to send Java class objects from one activity to another activity or save some as state information. Use JSON serialization or Android Parcelable to accomplish this.

OData API supports serializing Java objects as JSON string or Parcelable.

JSON Serialization

// Proxy Class
// Serialization to JSON String
List<Event> events = ...;
String jsonString = ToJSON.entityList(events);
bundle.putString(BUNDLE_KEY, jsonString);

// Deserialization from JSON String
// Note that FromJSON.entityList returns a DataQuery wrapping the JSON string as the result
// When getEvents is invoked, there will not be any network I/O
List<Event> restoredEvents =
    eventService.getEvents(FromJSON.entityList(bundle.getString(BUNDLE_KEY)));

// Dynamic API
// Serialization to JSON String
EntityValueList events = ...;
String jsonString = ToJSON.entityList(events);
bundle.putString(BUNDLE_KEY, jsonString);

// Deserialization from JSON String
// Since the DataQuery returned from FromJSON.entityList does not contain from method
// which the query is against, the from method is required in order for
// deserialization to know to what entity type it is deserializing to
EntityValueList restoredEvents =
    dataService.executeQuery(
            FromJSON.entityList(bundle.getString(BUNDLE_KEY)).from(eventSet)
    ).getEntityList();
// Proxy Class
// Serialization to JSON String
val events = ...
val jsonString = ToJSON.entityList(events)
bundle.putString(BUNDLE_KEY, jsonString)

// Deserialization from JSON String
// Note that FromJSON.entityList returns a DataQuery wrapping the JSON string as the result
// When getEvents is invoked, there will not be any network I/O
val restoredEvents = eventService.getEvents(
    FromJSON.entityList(bundle.getString(BUNDLE_KEY)))

// Dynamic API
// Serialization to JSON String
val events = ...
val jsonString = ToJSON.entityList(events)
bundle.putString(BUNDLE_KEY, jsonString)

// Deserialization from JSON String
// Since the DataQuery returned from FromJSON.entityList does not contain from method
// which the query is against, the from method is required in order for
// deserialization to know to what entity type it is deserializing to
val restoredEvents =
    dataService.executeQuery(
        FromJSON.entityList(bundle.getString(BUNDLE_KEY)).from(eventSet)
    ).entityList

Android Parcelable

Generated proxy classes have built-in support for Android Parcelable.

// Using Parcelable
// Proxy Class
List<Event> events = ...;
bundle.putParcelableArrayList(BUNDLE_KEY,
    (ArrayList<? extends Parcelable>)events);

// Convert value from bundle back to proxy class
List<Event> restoredEvents = bundle.getParcelableArrayList(BUNDLE_KEY);
// Using Parcelable
// Proxy Class
val events = ...
bundle.putParcelableArrayList(BUNDLE_KEY,
    (ArrayList<? extends Parcelable>)events)

// Convert value from bundle back to proxy class
val restoredEvents = bundle.getParcelableArrayList(BUNDLE_KEY);

Exception Handling

Because exceptions may occur for queries or CUD operations, you should put them inside a try/catch statement. If the OData provider returns a response with an HTTP error status code, a DataServiceException is thrown. You can then examine the server response to determine how the error should be handled.

// Query for a non-existent event
DataQuery query = new DataQuery().withKey(Event.key(99999L));
try {
    List<Event> events = eventService.getEvents(query);
} catch (DataServiceException ex) {
    // HTTP status code
    int status = ex.getStatus();
    // Examine the message sent back by server
    ErrorResponse response = ex.getResponse();
    String serverErrorMessage = response.getMessage();
} catch (Exception unex) {
    // unexpected exception handling
}
// Query for a non-existent event
val query = DataQuery().withKey(Event.key(99999L))
try {
    val events = eventService.getEvents(query)
} catch (ex: DataServiceException) {
    // HTTP status code
    val status = ex.status
    // Examine the message sent back by server
    val response = ex.response
    val serverErrorMessage = response.message
} catch (unex: Exception) {
    // unexpected exception handling
}

The OData specification defines the error response from the server. This response consists of a code that serves as a substatus to the HTTP status code and a message. The OData component parses the response and makes it available to the application through the getMessage and getCode methods of the ErrorResponse class. See the preceding example for more details.

For exceptions occurring within a batch request, the HTTP status code can be retrieved from the QueryResult or ChangeSet. If there is an error, use the getError method from either class to retrieve the corresponding DataServiceException. With the DataServiceException, you can get the ErrorResponse and use it to get the code and the message from the server. See the earlier section on batch processing for additional code samples.

It is important to understand that in the offline case, the request or batch is executed against the local store. Execution status only indicates success or failure from the point of view of the local store. When the pending requests are uploaded to synchronize the local store with the OData service, they may fail. Resolution of such errors can be complex. Please refer to offline error handling topic for more details.

// For query within a batch
QueryResult result = batch.getQueryResult(query);
int httpStatusCode = result.getStatus();
DataServiceException ex = result.getError();
ErrorResponse response = ex.getResponse();
String serverCode = response.getCode();
String serverMessage = response.getMessage();

// For ChangeSet within a batch
int httpStatusCode = updateChangeSet.getStatus();
DataServiceException ex = updateChangeSet.getError();
ErrorResponse response = ex.getResponse();
String serverCode = response.getCode();
String serverMessage = response.getMessage();
// For query within a batch
val result = batch.getQueryResult(query);
var httpStatusCode = result.status
var ex = result.error
var response = ex.response
var serverCode = response.code
var serverMessage = response.message

// For ChangeSet within a batch
httpStatusCode = updateChangeSet.status
ex = updateChangeSet.error
response = ex.response
serverCode = response.code
serverMessage = response.message

Last update: February 29, 2024