Guida al protocollo OData

1. Introduzione

In questo tutorial, esploreremo OData, un protocollo standard che consente un facile accesso ai set di dati utilizzando un'API RESTFul.

2. Cos'è OData ?

OData è uno standard OASIS e ISO / IEC per l'accesso ai dati utilizzando un'API RESTful. In quanto tale, consente a un consumatore di scoprire e navigare attraverso i set di dati utilizzando chiamate HTTP standard.

Ad esempio, possiamo accedere a uno dei servizi OData disponibili pubblicamente con un semplice curl one-liner :

curl -s //services.odata.org/V2/Northwind/Northwind.svc/Regions   Regions //services.odata.org/V2/Northwind/Northwind.svc/Regions ... rest of xml response omitted

Al momento della stesura di questo documento, il protocollo OData è alla sua quarta versione - 4.01 per essere più precisi. OData V4 ha raggiunto il livello standard OASIS nel 2014, ma ha una storia più lunga. Possiamo risalire alle sue radici in un progetto Microsoft chiamato Astoria, che è stato ribattezzato ADO.Net Data Services nel 2007. Il post originale del blog che annuncia questo progetto è ancora disponibile nel blog OData di Microsoft.

Avere un protocollo basato su standard per accedere al set di dati offre alcuni vantaggi rispetto alle API standard come JDBC o ODBC. In qualità di consumatore a livello di utente finale, possiamo utilizzare strumenti comuni come Excel per recuperare i dati da qualsiasi provider compatibile. La programmazione è inoltre facilitata da un gran numero di librerie client REST disponibili.

In qualità di fornitori, l'adozione di OData ha anche dei vantaggi: una volta creato un servizio compatibile, possiamo concentrarci sulla fornitura di preziosi set di dati, che gli utenti finali possono utilizzare utilizzando gli strumenti di loro scelta. Poiché si tratta di un protocollo basato su HTTP, possiamo anche sfruttare aspetti come meccanismi di sicurezza, monitoraggio e registrazione.

Queste caratteristiche hanno reso OData una scelta popolare da parte delle agenzie governative durante l'implementazione di servizi di dati pubblici, come possiamo verificare dando un'occhiata a questa directory.

3. Concetti OData

Al centro del protocollo OData c'è il concetto di Entity Data Model, o in breve EDM. L'EDM descrive i dati esposti da un fornitore OData attraverso un documento di metadati contenente una serie di meta-entità:

  • Tipo di entità e sue proprietà (ad esempio Persona , Cliente , Ordine , ecc.) E chiavi
  • Relazioni tra entità
  • Tipi complessi utilizzati per descrivere i tipi strutturati incorporati nelle entità (ad esempio, un tipo di indirizzo che fa parte di un tipo di cliente )
  • Set di entità, che aggregano entità di un determinato tipo

La specifica impone che questo documento di metadati sia disponibile nella posizione standard $ metadata nell'URL radice utilizzato per accedere al servizio. Ad esempio, se abbiamo un servizio OData disponibile su //example.org/odata.svc/ , il suo documento di metadati sarà disponibile su //example.org/odata.svc/$metadata .

Il documento restituito contiene un mucchio di XML che descrivono gli schemi supportati da questo server:

   ... schema elements omitted  

Analizziamo questo documento nelle sue sezioni principali.

L'elemento di primo livello, può avere un solo figlio, il elemento .La cosa importante da notare qui è l'URI dello spazio dei nomi poiché ci consente di identificare quale versione di OData utilizza il server. In questo caso, lo spazio dei nomi indica che abbiamo un server OData V2, che utilizza gli identificatori di Microsoft.

Un elemento DataServices può avere uno o più elementi Schema , ciascuno dei quali descrive un set di dati disponibile. Poiché una descrizione completa degli elementi disponibili in uno schema va oltre lo scopo di questo articolo, ci concentreremo su quelli più importanti: EntityTypes, Associazioni ed EntitySets .

3.1. EntityType Element

Questo elemento definisce le proprietà disponibili di una data entità, inclusa la sua chiave primaria. Può anche contenere informazioni sulle relazioni con altri tipi di schemi e, guardando un esempio - un CarMaker - saremo in grado di vedere che non è molto diverso dalle descrizioni trovate in altre tecnologie ORM, come JPA:

Qui, il nostro CarMaker ha solo due proprietà - Id e Name - e un'associazione a un altro EntityType . L' elemento ub Key definisce la chiave primaria dell'entità come la sua proprietà Id e ogni elemento Property contiene dati sulla proprietà di un'entità come il suo nome, tipo o nullability.

Una NavigationProperty è un tipo speciale di proprietà che descrive un "punto di accesso" a un'entità correlata.

3.2. Elemento di associazione

Un elemento Association descrive un'associazione tra due entità, che include la molteplicità su ciascuna estremità e facoltativamente un vincolo di integrità referenziale:

In questo caso, l' elemento Association definisce una relazione uno-a-molti tra un CarModel e le entità CarMaker , dove il primo funge da parte dipendente.

3.3. EntitySet Element

Il concetto di schema finale che esploreremo è l' elemento EntitySet , che rappresenta una raccolta di entità di un determinato tipo. Sebbene sia facile pensarli come analoghi a un tavolo - e in molti casi, sono proprio questo - un'analogia migliore è quella di una vista. Il motivo è che possiamo avere più elementi EntitySet per lo stesso EntityType , ognuno dei quali rappresenta un diverso sottoinsieme dei dati disponibili.

L' elemento EntityContainer , che è un elemento dello schema di primo livello, raggruppa tutti gli EntitySet disponibili :

Nel nostro semplice esempio, abbiamo solo due EntitySet , ma potremmo anche aggiungere visualizzazioni aggiuntive, come ForeignCarMakers o HistoricCarMakers .

4. URL e metodi OData

Per accedere ai dati esposti da un servizio OData, utilizziamo i normali verbi HTTP:

  • GET restituisce una o più entità
  • POST aggiunge una nuova entità a un set di entità esistente
  • PUT sostituisce una data entità
  • PATCH sostituisce proprietà specifiche di una data entità
  • DELETE rimuove una determinata entità

Tutte queste operazioni richiedono un percorso di risorse su cui agire. Il percorso della risorsa può definire un insieme di entità, un'entità o anche una proprietà all'interno di un'entità.

Let's take a look on an example URL used to access our previous OData service:

//example.org/odata/CarMakers 

The first part of this URL, starting with the protocol up to the odata/ path segment, is known as the service root URL and is the same for all resource paths of this service. Since the service root is always the same, we'll replace it in the following URL samples by an ellipsis (“…”).

CarMakers, in this case, refers to one of the declared EntitySets in the service metadata. We can use a regular browser to access this URL, which should then return a document containing all existing entities of this type:

  //localhost:8080/odata/CarMakers CarMakers 2019-04-06T17:51:33.588-03:00      //localhost:8080/odata/CarMakers(1L) CarMakers 2019-04-06T17:51:33.589-03:00      1 Special Motors    ... other entries omitted 

The returned document contains an entry element for each CarMaker instance.

Let's take a closer look at what information we have available to us:

  • id: a link to this specific entity
  • title/author/updated: metadata about this entry
  • link elements: Links used to point to a resource used to edit the entity (rel=”edit”) or to related entities. In this case, we have a link that takes us to the set of CarModel entities associated with this particular CarMaker.
  • content: property values of CarModel entity

An important point to notice here is the use of the key-value pair to identify a particular entity within an entity set. In our example, the key is numeric so a resource path like CarMaker(1L) refers to the entity with a primary key value equal to 1 – the “L” here just denotes a long value and could be omitted.

5. Query Options

We can pass query options to a resource URL in order to modify a number of aspects of the returned data, such as to limit the size of the returned set or its ordering. The OData spec defines a rich set of options, but here we'll focus on the most common ones.

As a general rule, query options can be combined with each other, thus allowing clients to easily implement common functionalities such as paging, filtering and ordering result lists.

5.1. $top and $skip

We can navigate through a large dataset using the $top an $skip query options:

.../CarMakers?$top=10&$skip=10 

$top tells the service that we want only the first 10 records of the CarMakers entity set. A $skip, which is applied before the $top, tells the server to skip the first 10 records.

It's usually useful to know the size of a given Entity Set and, for this purpose, we can use the $count sub-resource:

.../CarMakers/$count 

This resource produces a text/plain document containing the size of the corresponding set. Here, we must pay attention to the specific OData version supported by a provider. While OData V2 supports $count as a sub-resource from a collection, V4 allows it to be used as a query parameter. In this case, $count is a Boolean, so we need to change the URL accordingly:

.../CarMakers?$count=true 

5.2. $filter

We use the $filter query option to limit the returned entities from a given Entity Set to those matching given criteria. The value for the $filter is a logical expression that supports basic operators, grouping and a number of useful functions. For instance, let's build a query that returns all CarMaker instances where its Name attribute starts with the letter ‘B':

.../CarMakers?$filter=startswith(Name,'B') 

Now, let's combine a few logical operators to search for CarModels of a particular Year and Maker:

.../CarModels?$filter=Year eq 2008 and CarMakerDetails/Name eq 'BWM' 

Here, we've used the equality operator eq to specify values for the properties. We can also see how to use properties from a related entity in the expression.

5.3. $expand

By default, an OData query does not return data for related entities, which is usually OK. We can use the $expand query option to request that data from a given related entity be included inline with the main content.

Using our sample domain, let's build an URL that returns data from a given model and its maker, thus avoiding an additional round-trip to the server:

.../CarModels(1L)?$expand=CarMakerDetails 

The returned document now includes the CarMaker data as part of the related entity:

  //example.org/odata/CarModels(1L) CarModels 2019-04-07T11:33:38.467-03:00      //example.org/odata/CarMakers(1L) CarMakers 2019-04-07T11:33:38.492-03:00      1 Special Motors        1 1 Muze SM001 2018   

5.4. $select

We use the $select query option to inform the OData service that it should only return the values for the given properties. This is useful in scenarios where our entities have a large number of properties, but we're only interested in some of them.

Let's use this option in a query that returns only the Name and Sku properties:

.../CarModels(1L)?$select=Name,Sku 

The resulting document now has only the requested properties:

... xml omitted   Muze SM001   ... xml omitted

We can also see that even related entities were omitted. In order to include them, we'd need to include the name of the relation in the $select option.

5.5. $orderBy

The $orderBy option works pretty much as its SQL counterpart. We use it to specify the order in which we want the server to return a given set of entities. In its simpler form, its value is just a list of property names from the selected entity, optionally informing the order direction:

.../CarModels?$orderBy=Name asc,Sku desc 

This query will result in a list of CarModels ordered by their names and SKUs, in ascending and descending directions, respectively.

An important detail here is the case used with the direction part of a given property: while the spec mandates that server must support any combination of upper- and lower-case letters for the keywords asc and desc, it also mandates that client use only lowercase.

5.6. $format

This option defines the data representation format that the server should use, which takes precedence over any HTTP content-negotiation header, such as Accept. Its value must be a full MIME-Type or a format-specific short form.

For instance, we can use json as an abbreviation for application/json:

.../CarModels?$format=json 

This URL instructs our service to return data using JSON format, instead of XML, as we've seen before. When this option is not present, the server will use the value of the Accept header, if present. When neither is available, the server is free to choose any representation – usually XML or JSON.

Per quanto riguarda JSON in particolare, è fondamentalmente privo di schemi. Tuttavia, OData 4.01 definisce uno schema JSON anche per gli endpoint dei metadati. Ciò significa che ora possiamo scrivere client che possono sbarazzarsi completamente dell'elaborazione XML se scelgono di farlo.

6. Conclusione

In questa breve introduzione a OData, abbiamo trattato la sua semantica di base e come eseguire una semplice navigazione nei set di dati. Il nostro articolo di follow-up continuerà da dove eravamo rimasti e andrà direttamente nella libreria Olingo. Vedremo quindi come implementare servizi di esempio utilizzando questa libreria.

Esempi di codice, come sempre, sono disponibili su GitHub.