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

1. Въведение

Този урок е продължение на нашето Ръководство за протокол OData, където разгледахме основите на протокола OData.

Сега ще видим как да приложим проста услуга OData, използвайки библиотеката Apache Olingo .

Тази библиотека осигурява рамка за излагане на данни, използвайки протокола OData, като по този начин позволява лесен, базиран на стандарти достъп до информация, която иначе би била заключена във вътрешните бази данни.

2. Какво е Olingo?

Olingo е една от „включените“ реализации на OData, налични за Java среда - другата е SDL OData Framework. Той се поддържа от Фондация Apache и се състои от три основни модула:

  • Java V2 - клиентски и сървърни библиотеки, поддържащи OData V2
  • Java V4 - сървърни библиотеки, поддържащи OData V4
  • Javascript V4 - Javascript, само клиентска библиотека, поддържаща OData V4

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

Olingo V4, от друга страна, обработва само аспектите на протокола от по-ниско ниво, като договаряне от типа съдържание и синтактичен анализ на URL. Това означава, че от нас, разработчиците, ще зависи кодирането на всички подробности относно нещата като генериране на метаданни, генериране на back-end заявки въз основа на параметри на 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:

 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   

Най-новата версия на тези библиотеки е достъпна в Централното хранилище на Maven:

  • olingo-odata2-ядро
  • olingo-odata2-jpa-процесор-ядро
  • olingo-odata2-jpa-процесор-реф

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

3.1. Класове на домейни

Първата стъпка за внедряване на JPA-базирана услуга OData с 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 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 :

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

Предоставеният от Oalao ODataApplication е обикновен клас на приложение JAX-RS, който регистрира няколко доставчици, използвайки стандартния getClasses за обратно извикване () .

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

Този локатор е много прост ресурс на JAX-RS, който разширява запаса ODataRootLocator на Olingo и връща нашата пролетно управлявана 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

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.

Към момента на написването има отворен проблем за JIRA на Olingo, проследяващ работата на JPA модул за V4, но последният коментар датира от 2016 г. Има и JPA адаптер с отворен код на трети страни, хостван в хранилището на SAP GitHub, който, макар и неиздаван, на този етап изглежда по-пълен с функции, отколкото този на Олинго.

Както обикновено, целият код за тази статия е достъпен в GitHub.