Перейти к основному содержимому

Введение в OData с Olingo

· 7 мин. чтения

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 версии 2, которые поддерживают прямую интеграцию с 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, нам нужно добавить пару банок 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-ядерный
  • ядро процессора olingo-odata2-jpa
  • olingo-odata2-jpa-процессор-ссылка

Нам нужны эти исключения в этом списке, потому что 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, чтобы визуализировать, как все это происходит:

./f631f984650cb854408c4ef2d0bf7331.png

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

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

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

Следующим шагом является регистрация нашей ServiceFactory в среде выполнения Olingo и регистрация точки входа Olingo в среде выполнения JAX-RS. Мы сделаем это внутри производного класса 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 ODataApplication представляет собой обычный класс приложения JAX-RS , который регистрирует несколько поставщиков с помощью стандартного обратного вызова getClasses() .

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

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

@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

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

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

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

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

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

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

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

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

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

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

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

5. Вывод

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

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

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