Skip to content

Additional Features

There are some additional Offline OData features.

A built-in function is provided be used in a $filter system query option to check for the existence of a related entity.

In the following example, Event has a relationship to GlobalTheme, and each event may or may not have a global theme.

You can use the following code to get all events that have an associated global theme:

1
2
3
4
DataQuery query = new 
DataQuery().filter(OfflineODataQueryFunction.entityExists(EventDetail.theme.toPath()));

events = eventService.getEventDetails(query);

To get all events without an associated global theme, use:

1
2
3
4
query = new
DataQuery().filter(OfflineODataQueryFunction.entityExists(EventDetail.theme.toPath()).not());

events = eventService.getEventDetails(query);

Prefer Header

Offline OData serves as an OData service that you can send request to. When sending a GET request to query data from the offline store, you can add a Prefer header.

Currently only odata.maxpagesize can be specified for the Prefer header:

1
odata.maxpagesize=<number greater than 0>

EventLog Entity Set

EventLog is a local-only entity set for the OfflineOData namespace that allows an offline application to view a log of past system events. This is a read-only entity set and any attempts to modify it will result in an error. Only the most recent 100 events are available from the log.

The following types of system events are logged:

  • Download Records a successful download. Such event has a Type of Download and the Details property contains the defining query subset used in the download, or null if it was a full download.

  • Upload Records a successful upload. Such event has a Type of Upload and the Details property is always null.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// A portion of `EventLog` entity type properties
Properties: [
    {
       Name: "ID",
       Type: "Edm.Int64",
       IsNullable: "false"
    },
    {
        Name: "Type",
        Type: "Edm.String",
        IsNullable: "false",
        MaxLength: "128"
    },
    {
        Name: "Time",
        Type: "Edm.DateTimeOffset",
        IsNullable: "true"
    },
    {
        Name: "Details",
        Type: "Edm.String",
        IsNullable: "true",
        MaxLength: "2048"
    },
    ...

Example queries that can be made against this entity set:

  • Retrieve the details of the last full download
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Set up entity set, entity type and properties for EventLog
EntitySet eventLogSet = eventService.getEntitySet("EventLog");
EntityType eventLogType = eventLogSet.getEntityType();

// Structural properties
Property idProp = eventLogType.getProperty("ID");
Property typeProp = eventLogType.getProperty("Type");
Property timeProp = eventLogType.getProperty("Time");
Property detailProp = eventLogType.getProperty("Details");

// Get last Full Download
DataQuery query = new DataQuery().from(eventLogSet)
                                 .filter(typeProp.equal("Download").and(detailProp.isNull()))
                                 .orderBy(timeProp, SortOrder.DESCENDING)
                                 .top(1);

EntityValueList eventLogs = eventService.executeQuery(query).getEntityList();
EntityValue eventLog = eventLogs.get(0);
long id = idProp.getLong(eventLog);
String type = typeProp.getString(eventLog);
DataValue timeValue = timeProp.getValue(eventLog);
if (timeValue != null) {
    GlobalDateTime time = GlobalDateTime.castRequired(timeValue);
    // Use the time
    ...
}
  • Retrieve the details of the last download for a particular defining query
1
2
3
4
5
6
7
query = new DataQuery().from(eventLogSet)    
                       .filter(typeProp.equal("Download")
                       .and(detailProp.isNull().or(detailProp.indexOf("DefQ1").greaterEqual(0))))
                       .orderBy(timeProp, SortOrder.DESCENDING)
                       .top(1);

eventLogs = eventService.executeQuery(query).getEntityList();
  • Retrieve the times of the last 10 uploads
1
2
3
4
5
6
7
query = new DataQuery().from(eventLogSet)
                       .select(timeProp)
                       .filter(typeProp.equal("Upload"))
                       .orderBy(timeProp, SortOrder.DESCENDING)
                       .top(10);

eventLogs = eventService.executeQuery(query).getEntityList();

Lambda Operators

Offline OData supports the two OData Version 4.0 lambda operators, any and all. Lambda operators allow an arbitrary Boolean expression to be evaluated on a related collection. That is, they allow the results of a query to be filtered based on a collection of related entities.

Both lambda operators are prefixed with a navigation property path that identifies a collection.

The arguments of a lambda operator are a lambda variable name followed by a colon (:) and a Boolean expression that uses the lambda variable name to refer to a property of the related entity type identified by the navigation path.

The any operator returns true if the Boolean expression evaluates to true for any member of the collection. The following example returns all events that have at least one session with a title that contains "OData":

1
2
3
4
DataPath d = DataPath.lambda("d");
DataQuery query = new DataQuery().from(EventServiceMetadata.EntitySets.events)
                                 .filter(Event.sessions.any(d, d.path(Session.title).contains("OData")));
events = eventService.getEventDetails(query);
The all operator returns true if the Boolean expression evaluates to true for all members of the collection. The following example returns all events that have session titles containing "OData":

1
2
3
query = new DataQuery().from(EventServiceMetadata.EntitySets.events)
        .filter(Event.sessions.all(d, d.path(Session.title).contains("OData")));
events = eventService.getEventDetails(query);

Please note that the all operator returns true if the collection is empty. For example, if an event has no any session, it will satisfy the query above (because none of the event's sessions violates the criteria).

Request Queue Status

The offline store provides the request queue status through the OfflineODataProvider.isRequestQueueEmpty() method. The method checks whether there are any pending requests stored in the request queue that have not yet been uploaded.

Request Queue Optimization

Request queue optimization is an algorithm that runs prior to an upload to reduce the number of requests that get sent to the back end while still maintaining the final state of the data as if all the requests the application made were sent as is.

The algorithm is turned off by default. To turn it on, call setEnableRequestQueueOptimization(true) on an OfflineODataParameters instance before opening an offline store, then open the offline store with the given parameters.

For example, say the application creates a new entity and then updates that entity several times before uploading. With the algorithm turned off, the create and all update requests will be sent to the back end in the next upload. However, if the algorithm was turned on, the create and update requests will be combined and only one create request will be sent in the next upload.

Consider another simple example. Say the application creates a new entity and then deletes that entity before doing the next upload. With the algorithm turned off, both the create and delete requests will be sent to the back end. However, if the algorithm was turned on, neither request will be sent.

Transaction Boundaries in Request Queue Optimization

The request queue optimization algorithm will maintain transaction (OData change set) boundaries. That is, requests within a transaction can be combined, and requests between the end of one transaction and start of the next transaction that reference the same entity can be combined, but the algorithm will not remove a request from within a transaction to combine with a request outside that transaction nor move a request from outside a transaction to combine with a request inside that transaction. In this context the transaction could have been created manually by creating a local change set or built as a result of enabling transaction builder (see Transaction Builder).

For example, consider the following sequence of requests:

  1. Create Customer 1 with Name="John"
  2. Batch #1:
    1. Change Set #1:
      1. Create Customer 2 with Name="Jan"
      2. Update Customer 2 with Name="Jane"
  3. Update Customer 1 with Name="John Doe"
  4. Update Customer 2 with Name="Jane Do"
  5. Update Customer 2 with Name="Jane Doe"

After the request queue optimization algorithm is run, the requests will be:

  1. Create Customer 1 with Name="John Doe"
  2. Batch #1:
    1. Change Set #1:
      1. Create Customer 2 with Name="Jane"
  3. Update Customer 2 with Name="Jane Doe"

Keeping Requests As Is

Certain requests may need to be sent as is. For example, imagine an entity which needs to move from one explicit state to another such as NEW, IN PROCESS, SOLUTION PROVIDED, CONFIRMED. For such requests, use an OfflineODataRequestOptions instance and call setUnmodifiableRequest(true) (see Unmodifiable Requests), then use the instance when executing the request. Such requests will be left as is (not combined with other requests) when the request queue optimization algorithm is run.

Undoing Local Creation

This is a functionality that deletes an entity that was created locally but not yet uploaded, that is, undoing the local creation. All update requests in between the create and delete requests are also removed.

The functionality is disabled by default. To enable it, call setEnableUndoLocalCreation(true) on an OfflineODataParameters instance before opening an offline store, then open the offline store with the given parameters.

This functionality is different than Request Queue Optimization. The request queue optimization algorithm respects transaction boundaries and unmodifiable requests (see Unmodifiable Requests). If undoing local creation is enabled, it does not matter whether the creates, updates and deletes are in different transactions or are unmodifiable; it will remove all requests.

Transaction Builder

Transaction builder, when being enabled, allows grouping requests into transactions (OData change sets) to be sent in the next upload that were not necessarily executed in a single change set initially. In other words, enabling this allows building large transactions for the next upload where all requests that need to go into the transactions are not known at one time.

Which requests go in which transactions is controlled by specifying a TransactionID instance for an OfflineODataRequestOptions instance when performing the requests with the options instance. Requests that contain the same transaction ID will be put into the same transaction (see exceptional cases in Additional Notes below).

The functionality is disabled by default. To enable it, call setEnableTransactionBuilder(true) on an OfflineODataParameters instance before opening an offline store, then open the offline store with the given parameters.

For example, consider the following sequence of requests:

  1. Create Customer 1 with a transaction ID "1"
  2. Create Order 1 with a transaction ID "1"
  3. Create Product 1 with NO transaction ID
  4. Create Customer 2 with a transaction ID "2"
  5. Update Customer 1 with a transaction ID "1"
  6. Update Customer 2 with a transaction ID "2"

Just prior to executing the upload, the transaction builder algorithm will run and these requests will result in the following requests being sent to the back end (in this example assume that Request Queue Optimization is turned off):

  1. Batch #1
    1. Change Set #1
      1. Create Customer 1
      2. Create Order 1
      3. Update Customer 1
  2. Create Product 1
  3. Batch #2
    1. Change Set #1
      1. Create Customer 2
      2. Update Customer 2

The sample below shows how to specify transaction ID for a request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Declare offline specific options
OfflineODataRequestOptions options = new OfflineODataRequestOptions();
options.setTransactionID(new TransactionID("1"));

// Declare a new Track entity
Track track = ...

// Set property values
...

// Create the new track with the given transaction ID
eventService.createEntity(track, HttpHeaders.empty, options);

Additional Notes

Transaction Builder can be used in combination with Request Queue Optimization.

To ensure referential integrity of related entities and to ensure all requests are valid OData requests, it is not always possible to group all requests with the same transaction ID into a single change set. Therefore, the transaction builder will group requests with the same transaction ID into as few change sets as possible.

In addition to referential integrity, unmodifiable requests can affect the transaction builder. Only one unmodifiable request per entity will be put into a single change set.

Determining Modified/Created Relatives

You can retrieve information about related entities without using $expand. Specifically, you can determine whether an entity has one or more relatives that have any modification requests (including creates) unsent in the request queue and/or has one or more relatives that have locally applied requests (either requests that are in the request queue and unsent or requests which have been uploaded but not yet downloaded).

Relatives with Unsent Requests

To retrieve information about whether or not entities have relatives with unsent requests, indicating pending changes, in the request queue, add the custom query option sap.computeHasRelativesWithPendingChanges. The value of this custom query option is a comma separated list of navigation paths (that is, the same syntax as $expand). Only navigation paths specified in the query option will be used when computing if an entity has relatives with pending changes. Note that the sap.comupteHasRelativesWithPendingChanges option does not filter which entities are returned; it only enables computation of a property of EntityValue that can be retrieved by calling the hasRelativesWithPendingChanges() method.

Relatives with Local Changes

To retrieve information about whether or not entities have relatives with local changes applied, add the custom query option sap.computeHasLocalRelatives. The value of this custom query option is a comma separated list of navigation paths (that is, the same syntax as $expand). Only navigation paths specified in the query option will be used when computing if an entity has relatives with local changes. Note that the sap.computeHasLocalRelatives option does not filter which entities are returned; it only enables computation of a property of EntityValue that can be retrieved by calling the hasLocalRelatives() method.

Sample

The example below demonstrates building a query to retrieve all customers and will return information about whether or not the customers have related orders, products, or employers with local changes applied and also return information about whether or not the customers have related orders, products, or employers that have pending changes:

1
2
3
DataQuery query = new DataQuery().from(Customers)
                                 .custom("sap.computeHasLocalRelatives", "Orders/Products,Employer")
                                 .custom("sap.computeHasRelativesWithPendingChanges", "Orders/Products,Employer");