Introduzione a OData con Olingo

1. Introduzione

Questo tutorial è il seguito della nostra Guida al protocollo OData, in cui abbiamo esplorato le basi del protocollo OData.

Ora vedremo come implementare un semplice servizio OData utilizzando la libreria Apache Olingo .

Questa libreria fornisce un framework per esporre i dati utilizzando il protocollo OData, consentendo così un accesso semplice e basato su standard alle informazioni che altrimenti sarebbero bloccate nei database interni.

2. Che cos'è Olingo?

Olingo è una delle implementazioni OData "in primo piano" disponibili per l'ambiente Java, l'altra è SDL OData Framework. È gestito dalla Apache Foundation ed è composto da tre moduli principali:

  • Java V2 : librerie client e server che supportano OData V2
  • Java V4 : librerie server che supportano OData V4
  • Javascript V4 - Javascript, libreria solo client che supporta OData V4

In questo articolo, tratteremo solo le librerie Java V2 lato server, che supportano l'integrazione diretta con JPA . Il servizio risultante supporta le operazioni CRUD e altre funzionalità del protocollo OData, inclusi l'ordinamento, il paging e il filtro.

Olingo V4, d'altra parte, gestisce solo gli aspetti di livello inferiore del protocollo, come la negoziazione del tipo di contenuto e l'analisi dell'URL. Ciò significa che spetterà a noi, sviluppatori, codificare tutti i dettagli essenziali per quanto riguarda cose come la generazione di metadati, la generazione di query di back-end basate su parametri URL, ecc.

Per quanto riguarda la libreria client JavaScript, per ora la tralasciamo perché, poiché OData è un protocollo basato su HTTP, possiamo utilizzare qualsiasi libreria REST per accedervi.

3. Un servizio Java V2 Olingo

Creiamo un semplice servizio OData con i due EntitySet che abbiamo utilizzato nella nostra breve introduzione al protocollo stesso. Fondamentalmente, Olingo V2 è semplicemente un insieme di risorse JAX-RS e, come tale, dobbiamo fornire l'infrastruttura richiesta per utilizzarlo. Vale a dire, abbiamo bisogno di un'implementazione JAX-RS e di un servlet container compatibile.

Per questo esempio, abbiamo scelto di utilizzare Spring Boot , poiché fornisce un modo rapido per creare un ambiente adatto per ospitare il nostro servizio. Useremo anche l'adattatore JPA di Olingo, che "dialoga" direttamente con un EntityManager fornito dall'utente per raccogliere tutti i dati necessari per creare EntityDataModel di OData .

Sebbene non sia un requisito rigoroso, includere l'adattatore JPA semplifica notevolmente l'attività di creazione del nostro servizio.

Oltre alle dipendenze standard di Spring Boot, dobbiamo aggiungere un paio di vasi di Olingo:

 org.apache.olingo olingo-odata2-core 2.0.11   javax.ws.rs javax.ws.rs-api     org.apache.olingo olingo-odata2-jpa-processor-core 2.0.11   org.apache.olingo olingo-odata2-jpa-processor-ref 2.0.11   org.eclipse.persistence eclipselink   

L'ultima versione di queste librerie è disponibile nel repository centrale di Maven:

  • olingo-odata2-core
  • olingo-odata2-jpa-processor-core
  • olingo-odata2-jpa-processor-ref

Abbiamo bisogno di queste esclusioni in questo elenco perché Olingo ha dipendenze da EclipseLink come provider JPA e utilizza anche una versione JAX-RS diversa da Spring Boot.

3.1. Classi di dominio

Il primo passo per implementare un servizio OData basato su JPA con Olingo è creare le nostre entità di dominio. In questo semplice esempio, creeremo solo due classi, CarMaker e CarModel , con un'unica relazione uno-a-molti:

@Entity @Table(name="car_maker") public class CarMaker { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @NotNull private String name; @OneToMany(mappedBy="maker",orphanRemoval = true,cascade=CascadeType.ALL) private List models; // ... getters, setters and hashcode omitted } @Entity @Table(name="car_model") public class CarModel { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; @NotNull private String name; @NotNull private Integer year; @NotNull private String sku; @ManyToOne(optional=false,fetch=FetchType.LAZY) @JoinColumn(name="maker_fk") private CarMaker maker; // ... getters, setters and hashcode omitted }

3.2. Implementazione ODataJPAServiceFactory

Il componente chiave che dobbiamo fornire a Olingo per fornire dati da un dominio JPA è un'implementazione concreta di una classe astratta chiamata ODataJPAServiceFactory. Questa classe dovrebbe estendere ODataServiceFactory e funziona come un adattatore tra JPA e OData. Chiameremo questa fabbrica CarsODataJPAServiceFactory , dopo l'argomento principale del nostro dominio:

@Component public class CarsODataJPAServiceFactory extends ODataJPAServiceFactory { // other methods omitted... @Override public ODataJPAContext initializeODataJPAContext() throws ODataJPARuntimeException { ODataJPAContext ctx = getODataJPAContext(); ODataContext octx = ctx.getODataContext(); HttpServletRequest request = (HttpServletRequest) octx.getParameter( ODataContext.HTTP_SERVLET_REQUEST_OBJECT); EntityManager em = (EntityManager) request .getAttribute(EntityManagerFilter.EM_REQUEST_ATTRIBUTE); ctx.setEntityManager(em); ctx.setPersistenceUnitName("default"); ctx.setContainerManaged(true); return ctx; } } 

Olingo chiama il metodo initializeJPAContext () se questa classe ottiene un nuovo ODataJPAContext utilizzato per gestire ogni richiesta OData. Qui, usiamo il metodo getODataJPAContext () dalla classe base per ottenere un'istanza "semplice" che poi faremo un po 'di personalizzazione.

Questo processo è alquanto complicato, quindi disegniamo una sequenza UML per visualizzare come accade tutto ciò:

Nota che stiamo intenzionalmente utilizzando setEntityManager () invece di setEntityManagerFactory (). Potremmo averne uno da Spring ma, se lo passiamo a Olingo, entrerà in conflitto con il modo in cui Spring Boot gestisce il suo ciclo di vita, specialmente quando si tratta di transazioni.

Per questo motivo, ricorreremo a passare un'istanza EntityManager già esistente e la informeremo che il suo ciclo di vita è gestito esternamente. L' istanza EntityManager inserita proviene da un attributo disponibile alla richiesta corrente. Vedremo in seguito come impostare questo attributo.

3.3. Registrazione delle risorse di Jersey

Il passo successivo è registrare la nostra ServiceFactory con il runtime di Olingo e registrare il punto di ingresso di Olingo con il runtime JAX-RS. Lo faremo all'interno di una classe derivata ResourceConfig , dove definiamo anche il percorso OData per il nostro servizio in / odata :

@Component @ApplicationPath("/odata") public class JerseyConfig extends ResourceConfig { public JerseyConfig(CarsODataJPAServiceFactory serviceFactory, EntityManagerFactory emf) { ODataApplication app = new ODataApplication(); app .getClasses() .forEach( c -> { if ( !ODataRootLocator.class.isAssignableFrom(c)) { register(c); } }); register(new CarsRootLocator(serviceFactory)); register(new EntityManagerFilter(emf)); } // ... other methods omitted }

ODataApplication fornita da Olingo è una normale classe di applicazioni JAX-RS che registra alcuni provider utilizzando la callback standard getClasses () .

Possiamo usare tutto tranne la classe ODataRootLocator così com'è. Questo particolare è responsabile dell'istanza della nostra implementazione ODataJPAServiceFactory utilizzando il metodo newInstance () di Java . Tuttavia, poiché vogliamo che Spring lo gestisca per noi, dobbiamo sostituirlo con un localizzatore personalizzato.

Questo localizzatore è una risorsa JAX-RS molto semplice che estende l'ODataRootLocator stock di Olingo e restituisce la nostra ServiceFactory gestita da Spring quando necessario:

@Path("/") public class CarsRootLocator extends ODataRootLocator { private CarsODataJPAServiceFactory serviceFactory; public CarsRootLocator(CarsODataJPAServiceFactory serviceFactory) { this.serviceFactory = serviceFactory; } @Override public ODataServiceFactory getServiceFactory() { return this.serviceFactory; } } 

3.4. Filtro EntityManager

The last remaining piece for our OData service the EntityManagerFilter. This filter injects an EntityManager in the current request, so it is available to the ServiceFactory. It's a simple JAX-RS @Provider class that implements both ContainerRequestFilter and ContainerResponseFilter interfaces, so it can properly handle transactions:

@Provider public static class EntityManagerFilter implements ContainerRequestFilter, ContainerResponseFilter { public static final String EM_REQUEST_ATTRIBUTE = EntityManagerFilter.class.getName() + "_ENTITY_MANAGER"; private final EntityManagerFactory emf; @Context private HttpServletRequest httpRequest; public EntityManagerFilter(EntityManagerFactory emf) { this.emf = emf; } @Override public void filter(ContainerRequestContext ctx) throws IOException { EntityManager em = this.emf.createEntityManager(); httpRequest.setAttribute(EM_REQUEST_ATTRIBUTE, em); if (!"GET".equalsIgnoreCase(ctx.getMethod())) { em.getTransaction().begin(); } } @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { EntityManager em = (EntityManager) httpRequest.getAttribute(EM_REQUEST_ATTRIBUTE); if (!"GET".equalsIgnoreCase(requestContext.getMethod())) { EntityTransaction t = em.getTransaction(); if (t.isActive() && !t.getRollbackOnly()) { t.commit(); } } em.close(); } } 

The first filter() method, called at the start of a resource request, uses the provided EntityManagerFactory to create a new EntityManager instance, which is then put under an attribute so it can later be recovered by the ServiceFactory. We also skip GET requests since should not have any side effects, and so we won't need a transaction.

The second filter() method is called after Olingo has finished processing the request. Here we also check the request method, too, and commit the transaction if required.

3.5. Testing

Let's test our implementation using simple curl commands. The first this we can do is get the services $metadata document:

curl //localhost:8080/odata/$metadata

As expected, the document contains two types – CarMaker and CarModel – and an association. Now, let's play a bit more with our service, retrieving top-level collections and entities:

curl //localhost:8080/odata/CarMakers curl //localhost:8080/odata/CarModels curl //localhost:8080/odata/CarMakers(1) curl //localhost:8080/odata/CarModels(1) curl //localhost:8080/odata/CarModels(1)/CarMakerDetails 

Now, let's test a simple query returning all CarMakers where its name starts with ‘B':

curl //localhost:8080/odata/CarMakers?$filter=startswith(Name,'B') 

A more complete list of example URLs is available at our OData Protocol Guide article.

5. Conclusion

In this article, we've seen how to create a simple OData service backed by a JPA domain using Olingo V2.

Al momento della stesura di questo articolo, c'è un problema aperto sul JIRA di Olingo che tiene traccia dei lavori su un modulo JPA per V4, ma l'ultimo commento risale al 2016. C'è anche un adattatore JPA open source di terze parti ospitato nel repository GitHub di SAP che, sebbene inedito, sembra essere più completo di funzionalità a questo punto di quello di Olingo.

Come al solito, tutto il codice per questo articolo è disponibile su GitHub.