Introducción a OData con Olingo

1. Introducción

Este tutorial es un seguimiento de nuestra Guía de protocolo OData, donde hemos explorado los conceptos básicos del protocolo OData.

Ahora, veremos cómo implementar un servicio OData simple usando la biblioteca Apache Olingo .

Esta biblioteca proporciona un marco para exponer datos utilizando el protocolo OData, lo que permite un acceso fácil y basado en estándares a la información que de otro modo estaría guardada en bases de datos internas.

2. ¿Qué es Olingo?

Olingo es una de las implementaciones de OData "destacadas" disponibles para el entorno Java ; la otra es SDL OData Framework. Es mantenido por la Fundación Apache y se compone de tres módulos principales:

  • Java V2 : bibliotecas de cliente y servidor compatibles con OData V2
  • Java V4 : bibliotecas de servidor compatibles con OData V4
  • Javascript V4 : Javascript, biblioteca solo para clientes compatible con OData V4

En este artículo, cubriremos solo las bibliotecas de Java V2 del lado del servidor, que admiten la integración directa con JPA . El servicio resultante admite operaciones CRUD y otras características del protocolo OData, incluido el pedido, la paginación y el filtrado.

Olingo V4, por otro lado, solo maneja los aspectos de nivel inferior del protocolo, como la negociación del tipo de contenido y el análisis de URL. Esto significa que dependerá de nosotros, los desarrolladores, codificar todos los detalles esenciales con respecto a cosas como la generación de metadatos, la generación de consultas de back-end basadas en parámetros de URL, etc.

En cuanto a la biblioteca cliente de JavaScript, la dejamos fuera por ahora porque, dado que OData es un protocolo basado en HTTP, podemos usar cualquier biblioteca REST para acceder a ella.

3. Un servicio Olingo Java V2

Creemos un servicio OData simple con los dos EntitySet que hemos usado en nuestra breve introducción al protocolo en sí. En esencia, Olingo V2 es simplemente un conjunto de recursos JAX-RS y, como tal, necesitamos proporcionar la infraestructura necesaria para poder utilizarlo. Es decir, necesitamos una implementación JAX-RS y un contenedor de servlets compatible.

Para este ejemplo, hemos optado por utilizar Spring Boot , ya que proporciona una forma rápida de crear un entorno adecuado para alojar nuestro servicio. También usaremos el adaptador JPA de Olingo, que "habla" directamente con un EntityManager proporcionado por el usuario para recopilar todos los datos necesarios para crear el EntityDataModel de OData .

Si bien no es un requisito estricto, incluir el adaptador JPA simplifica enormemente la tarea de crear nuestro servicio.

Además de las dependencias estándar de Spring Boot, necesitamos agregar un par de frascos de 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   

La última versión de esas bibliotecas está disponible en el repositorio central de Maven:

  • olingo-odata2-core
  • olingo-odata2-jpa-procesador-core
  • olingo-odata2-jpa-procesador-ref

Necesitamos esas exclusiones en esta lista porque Olingo tiene dependencias de EclipseLink como su proveedor de JPA y también usa una versión de JAX-RS diferente a Spring Boot.

3.1. Clases de dominio

El primer paso para implementar un servicio OData basado en JPA con Olingo es crear nuestras entidades de dominio. En este ejemplo simple, crearemos solo dos clases, CarMaker y CarModel , con una única relación de uno a muchos:

@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. Implementación de ODataJPAServiceFactory

El componente clave que debemos proporcionar a Olingo para poder servir datos de un dominio JPA es una implementación concreta de una clase abstracta llamada ODataJPAServiceFactory. Esta clase debería extender ODataServiceFactory y funciona como un adaptador entre JPA y OData. Llamaremos a esta fábrica CarsODataJPAServiceFactory , después del tema principal de nuestro 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 llama al método initializeJPAContext () si esta clase obtiene un nuevo ODataJPAContext usado para manejar cada solicitud de OData. Aquí, usamos el método getODataJPAContext () de la clase base para obtener una instancia "simple" que luego hacemos algo de personalización.

Este proceso es algo complicado, así que dibujemos una secuencia UML para visualizar cómo sucede todo esto:

Tenga en cuenta que estamos usando intencionalmente setEntityManager () en lugar de setEntityManagerFactory (). Podríamos obtener uno de Spring pero, si se lo pasamos a Olingo, entrará en conflicto con la forma en que Spring Boot maneja su ciclo de vida, especialmente cuando se trata de transacciones.

Por esta razón, recurriremos a pasar una instancia de EntityManager ya existente y le informaremos que su ciclo de vida es administrado externamente. La instancia de EntityManager inyectada proviene de un atributo disponible en la solicitud actual. Más adelante veremos cómo configurar este atributo.

3.3. Registro de recursos de Jersey

El siguiente paso es registrar nuestra ServiceFactory con el tiempo de ejecución de Olingo y registrar el punto de entrada Olingo con el tiempo de ejecución JAX-RS. Lo haremos dentro de una clase derivada de ResourceConfig , donde también definimos la ruta de OData para que nuestro servicio sea / 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 }

La ODataApplication proporcionada por Olingo es una clase de aplicación JAX-RS regular que registra algunos proveedores utilizando la devolución de llamada estándar getClasses () .

Podemos usar todos menos la clase ODataRootLocator tal cual. Este en particular es responsable de crear una instancia de nuestra implementación ODataJPAServiceFactory usando el método newInstance () de Java . Pero, dado que queremos que Spring lo administre por nosotros, debemos reemplazarlo por un localizador personalizado.

Este localizador es un muy simple recurso JAX-RS que se extiende la acción de Olingo ODataRootLocator y vuelve nuestra primavera gestionados ServiceFactory cuando sea necesario:

@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 de escribir este artículo, existe un problema abierto sobre el seguimiento de JIRA de Olingo de los trabajos en un módulo JPA para V4, pero el último comentario se remonta a 2016. También hay un adaptador JPA de código abierto de terceros alojado en el repositorio GitHub de SAP que, aunque inédito, parece tener más funciones en este punto que el de Olingo.

Como de costumbre, todo el código de este artículo está disponible en GitHub.