Playing with OData

Last year I briefly dived into the world of SAP and its new Business Technology Platform (BTP) and tried to make sense of everything there. During that time I discovered OData is the communication protocol by choice and wondered about its relationship to traditional REST.

So during the course of this blog post we are going to have a look at OData, compare it to REST and look at examples of a rudimentary implementation based on the projects Apache Olingo and our good fellow Quarkus.

Before we can actually start, let us quickly recap what REST actually is.

Many of the following URLs used in examples need to be specially encoded when used directly on the shell with curl. We will omit that here to increase readability and obviously for brevity.

What is REST? &

Representational State Transfer (or REST) is an architecture style originated from the doctoral thesis by [Roy Fielding].

There is lots of good documentation about the general ideas, but the key takeaways for us are following six easy principles:

Uniform interface

Interfaces must be unique identifiable, self-descriptive and have a uniform representation

Client-Server

Separation of concern allows client and server to evolve independently

Stateless

Each request must contain all the necessary information to handle it

Cachable

A response can either be re-used (cached) or marked as non-reusable (non-cachable)

Layered System

Hierarchical components cannot see beyond their immediate layer

Code on demand (optional)

Client functionality can be extended by downloading and executing scripts or applets

Together they are the foundation of easy communication between web services with the well-known example Hypertext Transfer Protocol (or HTTP).

And what is OData? &

Open Data protocol (or OData) is a standardized application protocol for RESTful services. Initially been created by Microsoft in 2007, it was later included in the Microsoft Open Specification Promise and therefore opened for everyone else to use freely.

The protocol adheres to following principles and specifications:

  • OData MUST follow REST-principles UNLESS there is a good reason not to

  • OData Services MUST support ATOM encoding

  • OData services SHOULD support JSON encoding

Service documents &

A good starting point for interaction with every OData service is the atom service document at the root of it. It can be requested with a simple call with e.g. curl and includes a brief list of the names and feeds handled by the service:

$ curl -s http://localhost:8080/odata/ | jq .
{
  "@odata.context": "$metadata",
  "value": [
    {
      "name": "Todos",
      "url": "Todos"
    },
    {
      "name": "Tasks",
      "url": "Tasks"
    }
  ]
}

If you require more details the next best option is the metadata document, which contains a complete description of the feeds, types, properties and relationships exposed by this service.

In case you have wondered about the particular format of the previous document, the next own follows suit, but includes more objects and probably justifies some explanations in advance. The general format is called Entity Data Model (or EDM respectively EDMX if you insist on XML) and mainly consists of these types:

Entity types

Domain objects with a key, properties and relationships - like Todo

Complex types

Keyless value types belonging to an entity

Entity sets

Aggregate entities of a given type

The output can be pretty long, but a redacted version of the Todo entity looks like this in JSON:

$ curl -s http://localhost:8080/odata/$metadata | jq .
{
  "$Version": "4.01",
  "OData.Todo": {
    "Todo": {
      "$Kind": "EntityType",
      "$Key": [ (1)
        "ID"
      ],
      "ID": {
        "$Type": "Edm.Int32" (2)
      },
      "Title": {
        "$Type": "Edm.String"
      },
      "Description": {
        "$Type": "Edm.String"
      },
      "Tasks": { (3)
        "$Kind": "NavigationProperty", (4)
        "$Type": "OData.Todo.Task",
        "$Collection": true,
        "$Partner": "Todo",
        "$ContainsTarget": true
      }
    },
...
    "Container": { (5)
      "$Kind": "EntityContainer",
      "Todos": {
        "$Kind": "EntitySet",
        "$Type": "OData.Todo.Todo",
        "$NavigationPropertyBinding": {
          "Tasks": "Tasks"
        }
      },
...
    }
  }
}
1 Key property of the entity
2 Primitives and basic types
3 Tasks is an embedded entity type
4 Navigational properties allow access to related entities
5 The enclosing container that holds the sets and imports

Next up we are going to see how the service can actually be queries for data.

Query, Expand, Filter and Order &

OData provides by default a wide range of different ways to query for the actual data of the service.

Simple queries &

Listing all data can be archived by this easy call:

$ curl -s http://localhost:8080/odata/Todos | jq .
{
  "@odata.context": "$metadata#Todos",
  "value": [
    {
      "ID": 1,
      "Title": "Todo string",
      "Description": "Todo string"
    }
  ]
}

Querying for more specific data can be done either by key properties:

$ curl -s http://localhost:8080/odata/Todos(1) | jq .
{
  "@odata.context": "$metadata#Todos/$entity",
  "ID": 1,
  "Title": "Todo string",
  "Description": "Todo string"
}

Or more generally by all kind of properties directly via URL:

$ curl -s http://localhost:8080/odata/Todos(ID=1) | jq .
{
  "@odata.context": "$metadata#Todos/$entity",
  "ID": 1,
  "Title": "Todo string",
  "Description": "Todo string"
}

System queries &

System queries allow further control of the amount and order of the data and can be used in the used manner.

Counting the actual data can be done with $count:

$ curl -s http://localhost:8080/odata/Todos?$count=true | jq .
{
  "@odata.context": "$metadata#Todos",
  "@odata.count": 2, (1)
  "value": [
    {
      "ID": 1,
      "Title": "Todo string",
      "Description": "Todo string"
    },
    {
      "ID": 2,
      "Title": "Todo string",
      "Description": "Todo string"
    }
  ]
}
1 The count of items is included at the root level of the document

Further limiting the data can be done via $top and $skip:

$ curl -s http://localhost:8080/odata/Todos?$skip=1 | jq .
{
  "@odata.context": "$metadata#Todos",
  "value": [
    {
      "ID": 2,
      "Title": "Todo string",
      "Description": "Todo string"
    }
  ]
}
$ curl -s http://localhost:8080/odata/Todos?$top=1 | jq .
{
  "@odata.context": "$metadata#Todos",
  "value": [
    {
      "ID": 1,
      "Title": "Todo string",
      "Description": "Todo string"
    }
  ]
}

And limiting the number of actual properties can be done with $select:

$ curl -s http://localhost:8080/odata/Todos(ID=1)?$select=Title | jq .
{
  "@odata.context": "$metadata#Todos(ID,Title)/$entity",
  "@odata.id": "Todos(1)",
  "ID": 1, (1)
  "Title": "Todo string"
}
1 This doesn’t apply to key properties; they are always included

Our service document from above included a navigational property called Tasks and this can be used to also request related entities and expand them via $expand:

$ curl -s http://localhost:8080/odata/Todos(ID=1)?$expand=Tasks | jq .
{
  "@odata.context": "$metadata#Todos(Tasks())/$entity",
  "ID": 1,
  "Title": "Todo string",
  "Description": "Todo string",
  "Tasks": [
    {
      "ID": 1,
      "TodoID": 1,
      "Title": "Task string",
      "Description": "Task string"
    }
  ]
}

Ordering is also possible and works a bit like the order by clause of SQL:

$ curl -s http://localhost:8080/odata/Todos?$orderby=ID desc | jq . (1)
{
  "@odata.context": "$metadata#Todos",
  "value": [
    {
      "ID": 2,
      "Title": "test",
      "Description": "test"
    },
    {
      "ID": 1,
      "Title": "test",
      "Description": "test"
    }
  ]
}
This wasn’t an exhaustive list, there is many more to discover in the official documentation.

Arithmetic expressions &

Interestingly arithmetic expressions are also supported, so we can use operators like add, sub, mod, div and mul e.g. on ID:

$ curl -s http://localhost:8080/odata/Todos?$filter=ID mul 1 eq 1 | jq . (1)
{
  "@odata.context": "$metadata#Todos",
  "value": [
    {
      "ID": 1,
      "Title": "Todo string",
      "Description": "Todo string"
    }
  ]
}
1 I am quite sure someone has a valid use-case for this. (see xkcd 1127)

Complex queries &

And to conclude and make it a bit worse everything from above can be freely combined into beauties like this:

$ curl -s http://localhost:8080/odata/Todos?$filter=ID div 1 eq 1&$select=Title&$expand=Tasks($select=Title) | jq .
{
  "@odata.context": "$metadata#Todos(ID,Title,Tasks(ID,Title))",
  "value": [
    {
      "@odata.id": "Todos(1)",
      "ID": 1,
      "Title": "Todo string",
      "Tasks": [
        {
          "@odata.id": "Tasks(1)",
          "ID": 1,
          "Title": "Task string"
        }
      ]
    }
  ]
}

Rest of CRUD &

Lastly the missing CRUD operations can be used in a similar fashion as above and should not provoke anymore questions:

Create an entity &

$ curl -s -X POST --json '{"Title":"test", "Description":"test"}' http://localhost:8080/odata/Todos | jq .
{
  "@odata.context": "$metadata#Todos",
  "ID": 3,
  "Title": "test",
  "Description": "test"
}

Update an entity &

$ curl -v -X PUT --json '{"Title":"test3", "Description":"test3"}' http://localhost:8080/odata/Todos(3)
> PUT /odata/Todos(3) HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 204 No Content
...
$ curl -s http://localhost:8080/odata/Todos(3) | jq .
{
  "@odata.context": "$metadata#Todos",
  "ID": 3,
  "Title": "test3",
  "Description": "test3"
}

Delete an entity &

$ curl -v -X DELETE http://localhost:8080/odata/Todos(3)
> PUT /odata/Todos(3) HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 204 No Content
...
$ curl -s http://localhost:8080/odata/Todos(3) | jq .
{
  "error": {
    "code": null,
    "message": "Entity for requested key doesn't exist"
  }
}

Conclusion &

If we put aside the initial idea to compare an architectural style (REST) with an actual communication protocol (OData), it is probably save to say both can be used to query data from a service and interact with it in a CRUD manner.

By default and when properly implemented, OData allows a wide array of different ways to select and narrow down the amount of delivered data on a protocol level, without further ado of the requesting side via a defined interface.

On the other hand implementing the complete protocol e.g. based on Olingo is lots of work, if you won’t rely on something like olingo-jpa-processor-v4.

Still, implementing Olingo was quite funny and a perfect target for TDD due to the step-by-step tutorials and easy derivable tests there and I might consider it one day.

All examples can be found here: