«1. Введение

Это руководство является продолжением нашего руководства по протоколу OData, в котором мы изучили основы протокола OData.

Теперь мы посмотрим, как реализовать простую службу OData с помощью библиотеки Apache Olingo.

Эта библиотека предоставляет платформу для предоставления данных с использованием протокола OData, что обеспечивает простой, основанный на стандартах доступ к информации, которая в противном случае была бы заблокирована во внутренних базах данных.

2. Что такое Олинго?

Olingo — одна из «популярных» реализаций OData, доступных для среды Java, вторая — SDL OData Framework. Он поддерживается Apache Foundation и состоит из трех основных модулей:

    Java V2 — клиентские и серверные библиотеки, поддерживающие OData V2 Java V4 — серверные библиотеки, поддерживающие OData V4 Javascript V4 — Javascript, только для клиента библиотека, поддерживающая OData V4

В этой статье мы рассмотрим только серверные библиотеки Java V2, которые поддерживают прямую интеграцию с JPA. Полученная служба поддерживает операции CRUD и другие функции протокола OData, включая упорядочивание, разбиение по страницам и фильтрацию.

Olingo V4, с другой стороны, обрабатывает только низкоуровневые аспекты протокола, такие как согласование типа контента и анализ URL. Это означает, что нам, разработчикам, предстоит кодировать все мельчайшие детали, касающиеся таких вещей, как генерация метаданных, генерация внутренних запросов на основе параметров URL и т. д.

Что касается клиентской библиотеки JavaScript, мы пока опустим его, потому что, поскольку OData — это протокол на основе HTTP, мы можем использовать любую библиотеку REST для доступа к нему.

3. Служба Olingo Java V2

Давайте создадим простую службу OData с двумя наборами EntitySet, которые мы использовали в нашем кратком введении в сам протокол. По своей сути Olingo V2 — это просто набор ресурсов JAX-RS, поэтому для его использования нам необходимо предоставить необходимую инфраструктуру. А именно, нам нужна реализация JAX-RS и совместимый контейнер сервлетов.

В этом примере мы решили использовать Spring Boot, поскольку он обеспечивает быстрый способ создания подходящей среды для размещения нашего сервиса. Мы также будем использовать JPA-адаптер Olingo, который «общается» напрямую с EntityManager, предоставленным пользователем, для сбора всех данных, необходимых для создания EntityDataModel OData.

Хотя это и не является строгим требованием, включение адаптера JPA значительно упрощает задачу создания нашего сервиса.

Помимо стандартных зависимостей Spring Boot, нам нужно добавить пару jar-файлов Olingo:

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

Последняя версия этих библиотек доступна в центральном репозитории Maven:

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

Нам нужны эти исключения в этом списке, потому что Olingo зависит от EclipseLink как поставщика JPA, а также использует другую версию JAX-RS, чем Spring Boot.

3.1. Классы предметной области

Первым шагом для реализации службы OData на основе JPA с Olingo является создание наших сущностей предметной области. В этом простом примере мы создадим всего два класса — CarMaker и CarModel — с одним отношением «один ко многим»:

@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

Ключевым компонентом, который нам нужно предоставить Olingo для обслуживания данных из домена JPA, является конкретная реализация абстрактного класса под названием 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 вызывает метод initializeJPAContext(), если этот класс получает новый ODataJPAContext, используемый для обработки каждого запроса OData. Здесь мы используем метод getODataJPAContext() из базового класса, чтобы получить «простой» экземпляр, который мы затем настраиваем.

Этот процесс несколько запутан, поэтому давайте нарисуем последовательность UML, чтобы визуализировать, как все это происходит:

«Обратите внимание, что мы намеренно используем setEntityManager() вместо setEntityManagerFactory(). Мы могли бы получить его из Spring, но если мы передадим его в Olingo, это будет противоречить тому, как Spring Boot обрабатывает свой жизненный цикл, особенно при работе с транзакциями.

По этой причине мы прибегнем к передаче уже существующего экземпляра EntityManager и сообщим ему, что его жизненный цикл управляется извне. Внедренный экземпляр EntityManager поступает из атрибута, доступного в текущем запросе. Позже мы увидим, как установить этот атрибут.

3.3. Регистрация ресурсов Джерси

Следующим шагом является регистрация нашей ServiceFactory в среде выполнения Olingo и регистрация точки входа Olingo в среде выполнения JAX-RS. Мы сделаем это внутри производного класса ResourceConfig, где мы также определим путь OData для нашей службы как /odata: стандартный обратный вызов getClasses().

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

Мы можем использовать все, кроме класса ODataRootLocator, как есть. Этот конкретный объект отвечает за создание экземпляра нашей реализации ODataJPAServiceFactory с использованием метода Java newInstance(). Но, поскольку мы хотим, чтобы Spring управлял им за нас, нам нужно заменить его настраиваемым локатором.

Этот локатор представляет собой очень простой ресурс JAX-RS, который расширяет стандартный ODataRootLocator Olingo и при необходимости возвращает нашу ServiceFactory, управляемую Spring:

3.4. EntityManager Filter

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

    @Override
    public ODataServiceFactory getServiceFactory() {
       return this.serviceFactory;
    } 
}

Последняя оставшаяся часть нашего сервиса OData — EntityManagerFilter. Этот фильтр внедряет EntityManager в текущий запрос, поэтому он доступен для ServiceFactory. Это простой класс JAX-RS @Provider, который реализует интерфейсы ContainerRequestFilter и ContainerResponseFilter, поэтому он может правильно обрабатывать транзакции:

Первый метод filter(), вызываемый в начале запроса ресурсов, использует предоставленную EntityManagerFactory. для создания нового экземпляра EntityManager, который затем помещается в атрибут, чтобы впоследствии его можно было восстановить с помощью ServiceFactory. Мы также пропускаем GET-запросы, поскольку они не должны иметь побочных эффектов, и поэтому нам не потребуется транзакция.

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

Второй метод filter() вызывается после того, как Olingo закончит обработку запроса. Здесь мы также проверяем метод запроса и при необходимости фиксируем транзакцию.

3.5. Тестирование

Давайте протестируем нашу реализацию с помощью простых команд curl. Первое, что мы можем сделать, это получить документ services $metadata:

Как и ожидалось, документ содержит два типа — CarMaker и CarModel — и ассоциацию. Теперь давайте еще немного поиграем с нашим сервисом, извлекая коллекции и объекты верхнего уровня:

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

Теперь давайте протестируем простой запрос, возвращающий все CarMakers, имя которых начинается с «B»:

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

~ ~~ Более полный список примеров URL доступен в нашей статье «Руководство по протоколу OData».

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

5. Заключение

В этой статье мы рассмотрели, как создать простую службу OData, поддерживаемую доменом JPA, с помощью Olingo V2.

На момент написания этой статьи существует открытая проблема с JIRA Olingo, отслеживающей работы над модулем JPA для V4, но последний комментарий датируется 2016 годом. Также существует сторонний адаптер JPA с открытым исходным кодом, размещенный на GitHub SAP. репозиторий, который, хотя и не выпущен, на данный момент кажется более полнофункциональным, чем репозиторий Olingo.

Как обычно, весь код для этой статьи доступен на GitHub.

«