Introdução ao OData com o Olingo

Introdução ao OData com o Olingo

1. Introdução

Este tutorial é uma continuação do nossoOData Protocol Guide, onde exploramos os princípios básicos do protocoloOData.

Now, we’ll see how to implement a simple OData service using the Apache Olingo library.

Esta biblioteca fornece uma estrutura para expor dados usando o protocolo OData, permitindo assim acesso fácil e baseado em padrões a informações que, de outra forma, seriam bloqueadas em bancos de dados internos.

2. O que é o Olingo?

Olingo is one of the “featured” OData implementations available for the Java environment - o outro sendoSDL OData Framework. É mantido pela Apache Foundation e é composto por três módulos principais:

  • Java V2 - bibliotecas de cliente e servidor com suporte para OData V2

  • Java V4 - bibliotecas de servidor com suporte a OData V4

  • Javascript V4 - Javascript, biblioteca somente cliente com suporte a OData V4

In this article, we’ll cover only the server-side V2 Java libraries, which support direct integration with JPA. O serviço resultante suporta operações CRUD e outros recursos do protocolo OData, incluindo pedidos, paginação e filtragem.

O Olingo V4, por outro lado, lida apenas com os aspectos de nível inferior do protocolo, como negociação de tipo de conteúdo e análise de URL. Isso significa que caberá a nós, desenvolvedores, codificar todos os detalhes essenciais sobre coisas como geração de metadados, geração de consultas de back-end com base em parâmetros de URL, etc.

Quanto à biblioteca cliente JavaScript, estamos deixando-a de fora por enquanto porque, como OData é um protocolo baseado em HTTP, podemos usar qualquer biblioteca REST para acessá-lo.

3. Um serviço Olingo Java V2

Vamos criar um serviço OData simples com os doisEntitySets que usamos no próprioour brief introduction to the protocol. No fundo, o Olingo V2 é simplesmente um conjunto de recursos JAX-RS e, como tal, precisamos fornecer a infraestrutura necessária para usá-lo. Ou seja, precisamos de uma implementação JAX-RS e um contêiner de servlet compatível.

Para este exemplo,we’ve opted to use Spring Boot - pois fornece uma maneira rápida de criar um ambiente adequado para hospedar nosso serviço. Também usaremos o adaptador JPA da Olingo, que "fala" diretamente com uma ordemEntityManager in fornecida pelo usuário para reunir todos os dados necessários para criar osEntityDataModel. do OData

Embora não seja um requisito estrito, a inclusão do adaptador JPA simplifica bastante a tarefa de criar nosso serviço.

Além das dependências padrão do Spring Boot, precisamos adicionar alguns jars 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
        
    

A versão mais recente dessas bibliotecas está disponível no repositório central do Maven:

Precisamos dessas exclusões nesta lista porque o Olingo tem dependências no EclipseLink como seu provedor de JPA e também usa uma versão JAX-RS diferente da Spring Boot.

3.1. Classes de domínio

A primeira etapa para implementar um serviço OData baseado em JPA com o Olingo é criar nossas entidades de domínio. Neste exemplo simples, criaremos apenas duas classes -CarMakereCarModel - com uma única relação um-para-muitos:

@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. Implementação deODataJPAServiceFactory

The key component we need to provide to Olingo in order to serve data from a JPA domain is a concrete implementation of an abstract class called ODataJPAServiceFactory. Esta classe deve estenderODataServiceFactorye funciona como um adaptador entre JPA e OData. Chamaremos esta fábrica deCarsODataJPAServiceFactory, após o tópico principal de nosso domínio:

@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 chama o métodoinitializeJPAContext() se esta classe para obter um novoODataJPAContext  usado para lidar com cada solicitação OData. Aqui, usamos o métodogetODataJPAContext() da classe base para obter uma instância “simples”, que então fazemos algumas personalizações.

Este processo é um tanto complicado, então vamos desenhar uma sequência UML para visualizar como tudo isso acontece:

image

Observe que estamos usando intencionalmentesetEntityManager() em vez desetEntityManagerFactory(). Podemos obter um do Spring, mas, se passarmos para o Olingo, ele entrará em conflito com a maneira como o Spring Boot lida com seu ciclo de vida - especialmente quando lidar com transações.

Por esse motivo, vamos recorrer para passar uma instânciaEntityManager já existente e informá-la que seu ciclo de vida é gerenciado externamente. A instânciaEntityManager injetada vem de um atributo disponível na solicitação atual. Veremos mais tarde como definir esse atributo.

3.3. Registro de Recursos em Jersey

A próxima etapa é registrar nossoServiceFactory com o tempo de execução do Olingo e registrar o ponto de entrada do Olingo com o tempo de execução JAX-RS. Faremos isso dentro de uma classe derivadaResourceConfig, onde também definimos o caminho OData para nosso serviço como/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
}

OODataApplication fornecido pelo Olingo é uma classe JAX-RSApplication regular que registra alguns provedores usando o retorno de chamada padrão * getClasses () *.

Podemos usar todas as classes, excetoODataRootLocator no estado em que se encontram. Este em particular é responsável por instanciar nossaODataJPAServiceFactory implificação usando o métodonewInstance() de Java. Mas, como queremos que o Spring gerencie para nós, precisamos substituí-lo por um localizador personalizado.

Este localizador é um recurso JAX-RS muito simples que estendeODataRootLocator do estoque de Olingo e retorna nossoServiceFactory gerenciado por Spring quando necessário:

@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. FiltroEntityManager

A última peça restante para nosso serviço OData éEntityManagerFilter. This filter injects an EntityManager in the current request, so it is available to the ServiceFactory. É uma classe JAX-RS@Provider simples que implementa ambas as interfacesContainerRequestFilter andContainerResponseFilter, para que possa lidar adequadamente com as transações:

@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();
    }
}

O primeiro métodofilter(), chamado no início de uma solicitação de recurso, usa oEntityManagerFactory fornecido para criar uma nova instânciaEntityManager, que é então colocada sob um atributo para que possa ser recuperada posteriormente peloServiceFactory. Também ignoramos as solicitações GET, pois não devem ter efeitos colaterais e, portanto, não precisamos de uma transação.

O segundo métodofilter()  é chamado após Olingo terminar de processar a solicitação. Aqui também verificamos o método de solicitação e confirmamos a transação, se necessário.

3.5. Teste

Vamos testar nossa implementação usando comandoscurl simples. A primeira coisa que podemos fazer é obter o documento de serviços$metadata:

curl http://localhost:8080/odata/$metadata

Como esperado, o documento contém dois tipos -CarMakereCarModel - e uma associação. Agora, vamos brincar um pouco mais com nosso serviço, recuperando coleções e entidades de nível superior:

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

Agora, vamos testar uma consulta simples que retorna todos osCarMakers onde seu nome começa com 'B':

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

Uma lista mais completa de URLs de exemplo está disponível em nossoOData Protocol Guide article.

5. Conclusão

Neste artigo, vimos como criar um serviço OData simples apoiado por um domínio JPA usando Olingo V2.

No momento da redação deste artigo, havia umopen issue on Olingo’s JIRA rastreando os trabalhos em um módulo JPA para V4, mas o último comentário data de 2016. Há também um adaptador JPA de código aberto de terceiroshosted at SAP’s GitHub repository que, embora não tenha sido lançado, parece ser mais completo de recursos neste ponto do que o da Olingo.

Como de costume, todo o código deste artigo está disponívelover on GitHub.