Intro to OData with Olingo – 使用Olingo的OData介绍

最后修改: 2019年 5月 23日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.绪论

This tutorial is a follow-up to our OData Protocol Guide, where we’ve explored the basics of the OData protocol.

本教程是我们的OData协议指南的后续教程,我们已经探讨了OData协议的基本知识。

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

现在,我们将看到如何使用Apache Olingo库实现一个简单的OData服务

This library provides a framework to expose data using the OData protocol, thus allowing easy, standards-based access to information that would otherwise be locked away in internal databases.

这个库提供了一个框架,使用OData协议暴露数据,从而允许轻松地、基于标准的访问信息,否则这些信息将被锁在内部数据库中。

2. What Is Olingo?

2.什么是Olingo?

Olingo is one of the “featured” OData implementations available for the Java environment – the other being the SDL OData Framework. It is maintained by the Apache Foundation and is comprised of three main modules:

Olingo是适用于Java环境的 “特色 “OData实现之一 – 另一个是SDL OData框架。它由Apache基金会维护,由三个主要模块组成。

  • Java V2 – client and server libraries supporting OData V2
  • Java V4 – server libraries supporting OData V4
  • Javascript V4 – Javascript, client-only library supporting OData V4

In this article, we’ll cover only the server-side V2 Java libraries, which support direct integration with JPA. The resulting service supports CRUD operations and other OData protocol features, including ordering, paging and filtering.

在这篇文章中,我们将只涉及服务器端的V2 Java库,它支持与JPA直接集成。由此产生的服务支持CRUD操作和其他OData协议特性,包括排序、分页和过滤。

Olingo V4, on the other hand, only handles the lower-level aspects of the protocol, such as content-type negotiation and URL parsing. This means that it’ll be up to us, developers, to code all nitty-gritty details regarding things like metadata generation, generating back-end queries based on URL parameters, etc.

另一方面,Olingo V4只处理协议的低级方面,如内容类型协商和URL解析。这意味着,所有关于元数据生成、基于URL参数生成后端查询等方面的琐碎细节,都将由我们这些开发者来编码。

As for the JavaScript client library, we’re leaving it out for now because, since OData is an HTTP-based protocol, we can use any REST library to access it.

至于JavaScript客户端库,我们暂时不考虑它,因为OData是一个基于HTTP的协议,我们可以使用任何REST库来访问它。

3. An Olingo Java V2 Service

3.一个Olingo Java V2服务

Let’s create a simple OData service with the two EntitySets that we’ve used in our brief introduction to the protocol itself. At its core, Olingo V2 is simply a set of JAX-RS resources and, as such, we need to provide the required infrastructure in order to use it. Namely, we need a JAX-RS implementation and a compatible servlet container.

让我们用我们在协议的简要介绍本身中使用的两个EntitySets创建一个简单的OData服务。Olingo V2 的核心是一组 JAX-RS 资源,因此,我们需要提供所需的基础设施,以便使用它。也就是说,我们需要一个JAX-RS实现和一个兼容的Servlet容器。

For this example, we’ve opted to use Spring Boot – as it provides a quick way to create a suitable environment to host our service. We’ll also use Olingo’s JPA adapter, which “talks” directly to a user-supplied EntityManager in order to gather all data needed to create the OData’s EntityDataModel.

在本例中,我们选择使用Spring Boot–因为它提供了一种快速创建合适环境来托管我们的服务的方法。我们还将使用 Olingo 的 JPA 适配器,它直接与用户提供的 EntityManager 进行 “对话”,以便收集创建 OData 的 EntityDataModel 所需的所有数据。

While not a strict requirement, including the JPA adapter greatly simplifies the task of creating our service.

虽然不是一个严格的要求,但包括JPA适配器大大简化了创建服务的任务。

Besides standard Spring Boot dependencies, we need to add a couple of Olingo’s jars:

除了标准的Spring Boot依赖,我们还需要添加一些Olingo的jars。

<dependency>
    <groupId>org.apache.olingo</groupId>
    <artifactId>olingo-odata2-core</artifactId>
    <version>2.0.11</version>
    <exclusions>
        <exclusion>
            <groupId>javax.ws.rs</groupId>
            <artifactId>javax.ws.rs-api</artifactId>
         </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.olingo</groupId>
    <artifactId>olingo-odata2-jpa-processor-core</artifactId>
    <version>2.0.11</version>
</dependency>
<dependency>
    <groupId>org.apache.olingo</groupId>
    <artifactId>olingo-odata2-jpa-processor-ref</artifactId>
    <version>2.0.11</version>
    <exclusions>
        <exclusion>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
        </exclusion>
    </exclusions>
</dependency>

The latest version of those libraries is available at Maven’s Central repository:

这些库的最新版本可在Maven的中央仓库找到。

We need those exclusions in this list because Olingo has dependencies on EclipseLink as its JPA provider and also uses a different JAX-RS version than Spring Boot.

我们在这个列表中需要这些排除法,因为Olingo依赖EclipseLink作为其JPA提供者,而且还使用了与Spring Boot不同的JAX-RS版本。

3.1. Domain Classes

3.1.领域类

The first step to implement a JPA-based OData service with Olingo is to create our domain entities. In this simple example, we’ll create just two classes – CarMaker and CarModel – with a single one-to-many relationship:

使用Olingo实现基于JPA的OData服务的第一步是创建我们的领域实体。在这个简单的例子中,我们将只创建两个类–CarMakerCarModel–具有单一的一对多关系。

@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<CarModel> 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 Implementation

3.2.ODataJPAServiceFactory实现

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. This class should extend ODataServiceFactory and works as an adapter between JPA and OData. We’ll name this factory CarsODataJPAServiceFactory, after the main topic for our domain:

为了从JPA域中提供数据,我们需要向Olingo提供的关键组件是一个名为ODataJPAServiceFactory的抽象类的具体实现。该类应扩展ODataServiceFactory,并作为JPA和OData之间的适配器。我们将这个工厂命名为CarsODataJPAServiceFactory,以我们领域的主要话题命名。

@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 calls the initializeJPAContext() method if this class to get a new ODataJPAContext  used to handle every OData request. Here, we use the getODataJPAContext() method from the base classe to get a “plain” instance which we then do some customization.

Olingo调用该类的initializeJPAContext()方法,以获得一个新的ODataJPAContext,用于处理每个OData请求。在这里,我们使用基类中的getODataJPAContext()方法来获得一个 “普通 “实例,然后进行一些定制。

This process is somewhat convoluted, so let’s draw a UML sequence to visualize how all this happens:

这个过程有些曲折,所以让我们画一个UML序列来可视化这一切如何发生。

Olingo Request Processing

Note that we’re intentionally using setEntityManager() instead of setEntityManagerFactory(). We could get one from Spring but, if we pass it to Olingo, it’ll conflict with the way that Spring Boot handles its lifecycle – especially when dealing with transactions.

请注意,我们有意使用setEntityManager() ,而不是setEntityManagerFactory()。我们可以从Spring获得一个,但如果我们将其传递给Olingo,就会与Spring Boot处理其生命周期的方式相冲突–尤其是在处理事务时。

For this reason, we’ll resort to pass an already existing EntityManager instance and inform it that its lifecycle its externally managed. The injected EntityManager instance comes from an attribute available at the current request. We’ll later see how to set this attribute.

出于这个原因,我们将借助于传递一个已经存在的EntityManager实例,并通知它其生命周期是由外部管理的。注入的EntityManager实例来自于当前请求中的一个可用属性。我们稍后将看到如何设置这个属性。

3.3. Jersey Resource Registration

3.3.泽西岛资源注册

The next step is to register our ServiceFactory with Olingo’s runtime and register Olingo’s entry point with the JAX-RS runtime. We’ll do it inside a ResourceConfig derived class, where we also define the OData path for our service to be /odata:

下一步是向Olingo的运行时注册我们的ServiceFactory,并向JAX-RS运行时注册Olingo的入口点。我们将在ResourceConfig派生类中进行注册,在这里我们还将为我们的服务定义OData路径为/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
}

Olingo’s provided ODataApplication is a regular JAX-RS Application class that registers a few providers using the standard callback getClasses()

Olingo 提供的 ODataApplication 是一个普通的 JAX-RS Application 类,它使用标准回调 getClasses() 注册了一些提供者。

We can use all but the ODataRootLocator class as-is. This particular one is responsible for instantiating our ODataJPAServiceFactory implementation using Java’s newInstance() method. But, since we want Spring to manage it for us, we need to replace it by a custom locator.

除了ODataRootLocator类,我们可以按原样使用所有的类。这个特别的类负责使用Java的ODataJPAServiceFactory实现newInstance() 方法来实例化我们的ODataJPAServiceFactory。但是,由于我们希望Spring为我们管理它,我们需要用一个自定义的定位器来代替它。

This locator is a very simple JAX-RS resource that extends Olingo’s stock ODataRootLocator and it returns our Spring-managed ServiceFactory when needed:

这个定位器是一个非常简单的JAX-RS资源,它扩展了Olingo的库存ODataRootLocator,它在需要时返回我们Spring管理的ServiceFactory

@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

3.4 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:

我们的OData服务剩下的最后一块是EntityManagerFilter这个过滤器在当前请求中注入一个EntityManager,因此它对ServiceFactory可用。这是一个简单的 JAX-RS @Provider 类,它同时实现了 ContainerRequestFilterContainerResponseFilter 接口,因此它可以正确处理事务。

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

第一个filter()方法在资源请求开始时被调用,使用所提供的EntityManagerFactory来创建一个新的EntityManager实例,然后将其放在一个属性下,以便以后可以由ServiceFactory恢复。我们也跳过GET请求,因为不应该有任何副作用,所以我们不需要事务。

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.

第二个filter() 方法在Olingo处理完请求后被调用。在这里,我们也会检查请求方法,如果需要的话,也会提交事务。

3.5. Testing

3.5.测试

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

让我们使用简单的curl命令来测试我们的实现。首先我们可以做的是获得服务$metadata文档。

curl http://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:

正如预期的那样,该文档包含两种类型–CarMakerCarModel–以及一个关联现在,让我们再玩一下我们的服务,检索顶级的集合和实体。

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

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

现在,让我们测试一个简单的查询,返回所有名字以’B’开头的CarMakers

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

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

在我们的OData协议指南文章中,有一个更完整的URL示例列表。

5. Conclusion

5.总结

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

在这篇文章中,我们已经看到了如何使用Olingo V2创建一个由JPA域支持的简单OData服务。

As of this writing, there is an open issue on Olingo’s JIRA tracking the works on a JPA module for V4, but the last comment dates back to 2016. There’s also a third-party open-source JPA adapter hosted at SAP’s GitHub repository which, although unreleased, seems to be more feature-complete at this point than Olingo’s one.

截至目前,在Olingo的JIRA上有一个开放问题,跟踪V4的JPA模块的工作,但最后的评论可以追溯到2016年。还有一个第三方开源 JPA 适配器托管在 SAP 的 GitHub 仓库,虽然尚未发布,但在这一点上似乎比 Olingo 的适配器功能更完整。

As usual, all code for this article is available over on GitHub.

像往常一样,本文的所有代码都可以在GitHub上找到over