Skip to content

Additional Features

Offline OData provides some additional features.

A built-in function is provided to the $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:

let query = DataQuery().filter(OfflineQueryFunction.entityExists(Event.theme.toPath()))
let events = try eventService.fetchEvents(matching: query)

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

let query = DataQuery().filter(OfflineQueryFunction.entityExists(Event.theme.toPath()).not())
let events = try eventService.fetchEvents(matching: query)

Prefer Header

Offline OData serves as an OData service that you can send a 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
2
3
```nohighlight
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, 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, has a Type of Upload, and the Details property is always null.

/// 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 using the dynamic API:

  • Retrieve the details of the last full download
/// Set up entity set, entity type and properties for `EventLog'
let eventLogSet: EntitySet = eventService.entitySet(withName: "EventLog")
let eventLogType: EntityType = eventLogSet.entityType

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

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

let eventLogs = try eventService.executeQuery(query).EntityList()
let eventLog = eventLogs[0]
let id = idProp.longValue(from: eventLog);
let type = typeProp.stringValue(from: eventLog)
let timeValue = timeProp.dataValue(from: eventLog)
if timeValue != nil {
    let time = GlobalDateTime.castRequired(timeValue)
    /// Use the time
    ...

}
  • Retrieve details of the last download for a particular defining query
let query = DataQuery().from(eventLogSet)
            .filter(typeProp.equal("Download")
            .and(detailProp.isNull().or(detailProp.indexOf("DefQ1").greaterEqual(0))))
            .orderBy(timeProp, SortOrder.descending)
            .top(1)

let eventLogs = try eventService.executeQuery(query).entityList()
  • Retrieve times of the last 10 uploads
let query = DataQuery().from(eventLogSet)
            .select(timeProp)
            .filter(typeProp.equal("Upload"))
            .orderBy(timeProp, SortOrder.descending)
            .top(10)

let eventLogs = try eventService.executeQuery(query).entityList()
1
Besides the dynamic API, `EventLog` is also available through a built-in proxy class `OfflineODataEvent`. The example below shows how to access `EventLog` with the proxy class:
/// Get EventLog entities. The type of the result is Array<OfflineODataEvent>
let eventLogs = try eventService.fetchEventLog()

/// Examine the events
for event in eventLogs {
    let eventId = event.id
    let eventType = event.eventType
    ...
}

Note that property names from the proxy class are slightly different from what you use in the dynamic API. For example, when using the dynamic API, you can access a property named ID. When using the proxy class, the property name is id.

Lambda Operators

Offline OData supports 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, lambda operators 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 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 whose title contains "OData":

let d = DataPath.lambda("d")
let query = DataQuery().from(EventServiceMetadata.EntitySets.events)
                        .filter(Event.sessions.any(d, d.path(Session.title).contains("OData")))
let events = try eventService.fetchEvents(matching: query)
1
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":
let query = DataQuery().from(EventServiceMetadata.EntitySets.events)
                        .filter(Event.sessions.all(d, d.path(Session.title).contains("OData")))
let events = try eventService.fetchEvents(matching: query)

Note

The all operator returns true if the collection is empty. For example, if an event has no session, it will satisfy the query above (because none of its sessions violates the criteria).

Request Queue Status

The offline store provides request queue status using the OfflineODataProvider.isRequestQueueIsEmpty() 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, set the enableRequestQueueOptimization property of an OfflineODataParameters instance to true 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.

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 requests 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 set the unmodifiableRequest property to true, 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 functionality supports the case of deleting an entity that was created locally but not yet uploaded undoing the local creation. All update requests in between the create and delete requests are also removed.

For example, say you have created a new entity (customer101) locally, updated it once, and then deleted it. After performing these operations, you invoke upload(). Below is the operation sequence:

  1. Request #1: Create customer101 locally
  2. Request #2: Update customer101
  3. Request #3: Delete customer101
  4. Invoke upload()

Note that before invoking upload(), customer101 is not available (since it has been deleted), but requests #1 to #3 are kept in the request queue.

If you've disabled undoing local creation, requests #1 to #3 will be sent to the back end when calling upload(). The entity customer101 will be created, updated, then deleted in the back end.

If the functionality is enabled, when calling upload(), the optimization removes requests #1 to #3 and they will therefore not be sent to the back end.

By default, this functionality is disabled. To enable it, set the enableUndoLocalCreation property of an OfflineODataParameters instance to true 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. 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 enabled, supports 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 initially not known.

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, set enableTransactionBuilder property of an OfflineODataParameters instance to true 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

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 the transaction ID for a request:

/// Declare offline specific request options
let options = OfflineODataRequestOptions()
options.transactionID = try TransactionID(stringLiteral: "1")

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

/// Set property values
...

/// Create the new track with the given transaction ID
try eventService.createEntity(track, options: 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.

Undoing Pending Changes

While you can make changes to existing entities and new entities locally, you can also undo the pending changes without uploading them by calling DataService.undoPendingChanges() for an EntityValue object. An existing entity will be restored to the original status as if no changes had been made. A new entity will be removed as if the entity had never been created.

For example, let's say you have an existing entity customer101. You can either:

  • Patch customer101 several times. Undoing the changes restores all property values.
  • Associate customer101 to some purchase orders. Undoing the changes removes the relationships.
  • Delete customer101. Undoing this change will restore the entity.

As another example, say you have created a new entity (customer102) locally. No matter what subsequent operations you apply to customer102, undoing the changes will remove this entity as if it had never been created.

Offline OData also provides flexible support for more complicated cases. For example, let's say you created a new entity (order102) that deep inserted a new related entity customer102 in one request. There can be different sequences of performing undo:

  • Undo changes for customer102 first, customer102 will be removed, and order102 remains (since you are not undoing changes for order102) but is not related to any customer (since customer102 has been removed). The original request for creating order102 and customer102 will be adjusted accordingly to produce the correct result.

    You can continue to undo changes for order102, which removes it.

  • Undo changes for order102 first, order102 will be removed, and customer102 remains (since you are not undoing changes for customer102) but is not related to any order (since order102 has been removed). The original request for creating order102 and customer102 is adjusted accordingly to produce the correct result.

    You can continue to undo changes for customer102, which removes it.

Undoing pending changes and undoing local creation are similar in terms of restoring the original status. The differences between are as follows:

  • Undoing local creation is an optimization for the back end to not send a POST request if a new entity created locally is deleted before it is uploaded. Undoing pending changes is not intended to be an optimization but rather allows correcting mistakes or changing your mind about what you did.
  • Undoing local creation only applies to new entities created locally. Undoing pending changes also applies to existing entities that have been downloaded from the back end.
  • For a new entity created locally, you can remove it by undoing pending changes for it without performing a deletion. However, you must perform a deletion on the entity if you want to apply undoing local creation in order to remove it.
  • Undoing local creation takes effect (removing affected requests from request queue) when you perform an upload. Undoing pending changes takes effect (removing affected requests from request queue) when you call the method.

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 the hasRelativesWithPendingChanges property of EntityValue.

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 the hasLocalRelatives property of EntityValue.

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:

let query = DataQuery().from(Customers)
                                    .custom("sap.computeHasLocalRelatives", "Orders/Products,Employer")
                                    .custom("sap.computeHasRelativesWithPendingChanges", "Orders/Products,Employer")

Last update: October 30, 2020