Einführung in OData mit Olingo

1. Einleitung

Dieses Tutorial ist eine Fortsetzung unseres OData-Protokollhandbuchs, in dem wir die Grundlagen des OData-Protokolls untersucht haben.

Nun werden wir sehen, wie ein einfacher OData-Dienst mithilfe der Apache Olingo-Bibliothek implementiert wird .

Diese Bibliothek bietet ein Framework zum Offenlegen von Daten mithilfe des OData-Protokolls und ermöglicht so einen einfachen, standardbasierten Zugriff auf Informationen, die andernfalls in internen Datenbanken gesperrt wären.

2. Was ist Olingo?

Olingo ist eine der "vorgestellten" OData-Implementierungen, die für die Java-Umgebung verfügbar sind - die andere ist das SDL OData Framework. Es wird von der Apache Foundation verwaltet und besteht aus drei Hauptmodulen:

  • Java V2 - Client- und Serverbibliotheken, die OData V2 unterstützen
  • Java V4 - Serverbibliotheken, die OData V4 unterstützen
  • Javascript V4 - Javascript, Nur-Client-Bibliothek, die OData V4 unterstützt

In diesem Artikel werden nur die serverseitigen V2-Java-Bibliotheken behandelt, die die direkte Integration in JPA unterstützen . Der resultierende Dienst unterstützt CRUD-Operationen und andere OData-Protokollfunktionen, einschließlich Bestellung, Paging und Filterung.

Olingo V4 hingegen behandelt nur die untergeordneten Aspekte des Protokolls, z. B. die Aushandlung von Inhaltstypen und das Parsen von URLs. Dies bedeutet, dass es an uns Entwicklern liegt, alle wichtigen Details in Bezug auf Dinge wie die Generierung von Metadaten, das Generieren von Back-End-Abfragen basierend auf URL-Parametern usw. zu codieren.

Die JavaScript-Clientbibliothek wird vorerst weggelassen, da OData ein HTTP-basiertes Protokoll ist und wir mit jeder REST-Bibliothek darauf zugreifen können.

3. Ein Olingo Java V2-Dienst

Erstellen wir einen einfachen OData-Service mit den beiden EntitySets , die wir in unserer kurzen Einführung in das Protokoll selbst verwendet haben. Olingo V2 besteht im Kern lediglich aus einer Reihe von JAX-RS-Ressourcen. Daher müssen wir die erforderliche Infrastruktur bereitstellen, um sie nutzen zu können. Wir benötigen nämlich eine JAX-RS-Implementierung und einen kompatiblen Servlet-Container.

In diesem Beispiel haben wir uns für Spring Boot entschieden, da dies eine schnelle Möglichkeit bietet, eine geeignete Umgebung für das Hosting unseres Dienstes zu erstellen. Wir werden auch den JPA-Adapter von Olingo verwenden, der direkt mit einem vom Benutzer bereitgestellten EntityManagerkommuniziert“ , um alle Daten zu erfassen, die zum Erstellen des EntityDataModel des OData erforderlich sind.

Obwohl dies keine strenge Anforderung ist, vereinfacht das Einschließen des JPA-Adapters die Erstellung unseres Dienstes erheblich.

Neben den Standardabhängigkeiten für Spring Boot müssen wir einige Olingo-Gläser hinzufügen:

 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   

Die neueste Version dieser Bibliotheken ist im zentralen Repository von Maven verfügbar:

  • olingo-odata2-core
  • olingo-odata2-jpa-Prozessorkern
  • olingo-odata2-jpa-prozessor-ref

Wir benötigen diese Ausschlüsse in dieser Liste, da Olingo Abhängigkeiten von EclipseLink als JPA-Anbieter hat und auch eine andere JAX-RS-Version als Spring Boot verwendet.

3.1. Domänenklassen

Der erste Schritt zur Implementierung eines JPA-basierten OData-Dienstes mit Olingo besteht darin, unsere Domänenentitäten zu erstellen. In diesem einfachen Beispiel erstellen wir nur zwei Klassen - CarMaker und CarModel - mit einer einzigen Eins-zu-Viele-Beziehung:

@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. ODataJPAServiceFactory- Implementierung

Die Schlüsselkomponente, die wir Olingo zur Verfügung stellen müssen, um Daten aus einer JPA-Domäne bereitzustellen, ist eine konkrete Implementierung einer abstrakten Klasse namens ODataJPAServiceFactory. Diese Klasse sollte ODataServiceFactory erweitern und fungiert als Adapter zwischen JPA und OData. Wir werden diese Fabrik CarsODataJPAServiceFactory nach dem Hauptthema für unsere Domain benennen :

@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 ruft die Methode initializeJPAContext () auf , wenn diese Klasse einen neuen ODataJPAContext erhält, der zur Verarbeitung jeder OData-Anforderung verwendet wird. Hier verwenden wir die Methode getODataJPAContext () aus der Basisklasse, um eine "einfache" Instanz zu erhalten, die wir dann anpassen.

Dieser Prozess ist etwas kompliziert. Zeichnen wir also eine UML-Sequenz, um zu veranschaulichen, wie dies alles geschieht:

Beachten Sie, dass wir absichtlich setEntityManager () anstelle von setEntityManagerFactory () verwenden. Wir könnten eine von Spring bekommen, aber wenn wir sie an Olingo weitergeben, wird dies im Widerspruch zu der Art und Weise stehen, wie Spring Boot seinen Lebenszyklus handhabt - insbesondere bei Transaktionen.

Aus diesem Grund werden wir eine bereits vorhandene EntityManager- Instanz übergeben und sie darüber informieren, dass ihr Lebenszyklus extern verwaltet wird. Die injizierte EntityManager- Instanz stammt aus einem Attribut, das bei der aktuellen Anforderung verfügbar ist. Wir werden später sehen, wie dieses Attribut festgelegt wird.

3.3. Jersey Resource Registration

Der nächste Schritt besteht darin, unsere ServiceFactory bei Olingos Laufzeit zu registrieren und Olingos Einstiegspunkt bei der JAX-RS-Laufzeit zu registrieren. Wir werden dies in einer von ResourceConfig abgeleiteten Klasse tun , in der wir auch den OData-Pfad für unseren Service als / odata definieren :

@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 }

Die von Olingo bereitgestellte ODataApplication ist eine reguläre JAX-RS- Anwendungsklasse , die einige Anbieter mithilfe des Standard-Rückrufs getClasses () registriert .

Wir können alle außer der ODataRootLocator- Klasse unverändert verwenden . Dieser ist für die Instanziierung unserer ODataJPAServiceFactory- Implementierung mithilfe der newInstance () -Methode von Java verantwortlich . Da Spring es jedoch für uns verwalten soll, müssen wir es durch einen benutzerdefinierten Locator ersetzen.

Dieser Locator ist eine sehr einfache JAX-RS-Ressource, die Olingos ODataRootLocator erweitert und bei Bedarf unsere von Spring verwaltete ServiceFactory zurückgibt :

@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. EntityManager- Filter

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.

Zum jetzigen Zeitpunkt gibt es ein offenes Problem mit Olingos JIRA, das die Arbeiten an einem JPA-Modul für V4 verfolgt. Der letzte Kommentar stammt jedoch aus dem Jahr 2016. Es gibt auch einen Open-Source-JPA-Adapter eines Drittanbieters, der im GitHub-Repository von SAP gehostet wird. Obwohl unveröffentlicht, scheint es an dieser Stelle vollständiger zu sein als Olingos.

Wie üblich ist der gesamte Code für diesen Artikel auf GitHub verfügbar.