Pengantar OData dengan Olingo

1. Perkenalan

Tutorial ini adalah tindak lanjut dari Panduan Protokol OData kami, di mana kami telah menjelajahi dasar-dasar protokol OData.

Sekarang, kita akan melihat bagaimana mengimplementasikan layanan OData sederhana menggunakan pustaka Apache Olingo .

Pustaka ini menyediakan kerangka kerja untuk mengekspos data menggunakan protokol OData, sehingga memungkinkan akses berbasis standar yang mudah ke informasi yang jika tidak akan dikunci dalam database internal.

2. Apakah Olingo Itu?

Olingo adalah salah satu implementasi OData “unggulan” yang tersedia untuk lingkungan Java - yang lainnya adalah Kerangka OData SDL. Itu dikelola oleh Apache Foundation dan terdiri dari tiga modul utama:

  • Java V2 - perpustakaan klien dan server yang mendukung OData V2
  • Java V4 - perpustakaan server yang mendukung OData V4
  • Javascript V4 - Javascript, perpustakaan khusus klien yang mendukung OData V4

Dalam artikel ini, kami hanya akan membahas pustaka Java V2 sisi server, yang mendukung integrasi langsung dengan JPA . Layanan yang dihasilkan mendukung operasi CRUD dan fitur protokol OData lainnya, termasuk pemesanan, paging, dan pemfilteran.

Olingo V4, di sisi lain, hanya menangani aspek tingkat yang lebih rendah dari protokol, seperti negosiasi jenis konten dan penguraian URL. Ini berarti terserah kepada kami, pengembang, untuk mengkodekan semua detail penting mengenai hal-hal seperti pembuatan metadata, membuat kueri back-end berdasarkan parameter URL, dll.

Adapun pustaka klien JavaScript, kami meninggalkannya untuk saat ini karena, karena OData adalah protokol berbasis HTTP, kami dapat menggunakan pustaka REST apa pun untuk mengaksesnya.

3. Layanan Olingo Java V2

Mari buat layanan OData sederhana dengan dua EntitySet yang telah kita gunakan dalam pengantar singkat kita ke protokol itu sendiri. Pada intinya, Olingo V2 hanyalah sekumpulan sumber daya JAX-RS dan, oleh karena itu, kami perlu menyediakan infrastruktur yang diperlukan untuk menggunakannya. Yaitu, kita membutuhkan implementasi JAX-RS dan wadah servlet yang kompatibel.

Untuk contoh ini, kami memilih untuk menggunakan Spring Boot - karena menyediakan cara cepat untuk membuat lingkungan yang sesuai untuk menghosting layanan kami. Kami juga akan menggunakan adaptor JPA Olingo, yang "berbicara" langsung ke EntityManager yang disediakan pengguna untuk mengumpulkan semua data yang diperlukan untuk membuat EntityDataModel OData .

Meskipun bukan persyaratan yang ketat, termasuk adaptor JPA sangat menyederhanakan tugas pembuatan layanan kami.

Selain dependensi Spring Boot standar, kita perlu menambahkan beberapa toples 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   

Versi terbaru dari pustaka tersebut tersedia di repositori Pusat Maven:

  • olingo-odata2-core
  • olingo-odata2-jpa-prosesor-core
  • olingo-odata2-jpa-prosesor-ref

Kami membutuhkan pengecualian tersebut dalam daftar ini karena Olingo memiliki ketergantungan pada EclipseLink sebagai penyedia JPA dan juga menggunakan versi JAX-RS yang berbeda dari Spring Boot.

3.1. Kelas Domain

Langkah pertama untuk mengimplementasikan layanan OData berbasis JPA dengan Olingo adalah membuat entitas domain kami. Dalam contoh sederhana ini, kita hanya akan membuat dua kelas - CarMaker dan CarModel - dengan hubungan satu-ke-banyak:

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

Komponen kunci yang perlu kita sediakan untuk Olingo untuk menyajikan data dari domain JPA adalah implementasi konkret dari kelas abstrak yang disebut ODataJPAServiceFactory. Kelas ini harus memperluas ODataServiceFactory dan berfungsi sebagai adaptor antara JPA dan OData. Kami akan menamai pabrik ini CarsODataJPAServiceFactory , setelah topik utama untuk domain kami:

@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 memanggil metode initializeJPAContext () jika kelas ini mendapatkan ODataJPAContext baru yang digunakan untuk menangani setiap permintaan OData. Di sini, kami menggunakan metode getODataJPAContext () dari basis classe untuk mendapatkan instance "biasa" yang kemudian kami lakukan beberapa penyesuaian.

Proses ini agak berbelit-belit, jadi mari kita menggambar urutan UML untuk memvisualisasikan bagaimana semua ini terjadi:

Perhatikan bahwa kami sengaja menggunakan setEntityManager () daripada setEntityManagerFactory (). Kami bisa mendapatkannya dari Spring tetapi, jika kami meneruskannya ke Olingo, itu akan bertentangan dengan cara Spring Boot menangani siklus hidupnya - terutama saat menangani transaksi.

Untuk alasan ini, kami akan menggunakan instance EntityManager yang sudah ada dan memberi tahu bahwa siklus prosesnya dikelola secara eksternal. Instance EntityManager yang dimasukkan berasal dari atribut yang tersedia pada permintaan saat ini. Nanti kita akan melihat bagaimana mengatur atribut ini.

3.3. Pendaftaran Sumber Daya Jersey

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:

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

Kita bisa menggunakan semua kecuali kelas ODataRootLocator sebagaimana adanya. Yang satu ini bertanggung jawab untuk membuat contoh implementasi ODataJPAServiceFactory menggunakan metode newInstance () Java . Tapi, karena kita ingin Spring mengelolanya untuk kita, kita perlu menggantinya dengan pencari lokasi khusus.

Locator ini adalah sumber daya JAX-RS yang sangat sederhana yang memperluas stok ODataRootLocator Olingo dan mengembalikan ServiceFactory yang dikelola Spring saat diperlukan:

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

Saat tulisan ini dibuat, ada masalah terbuka tentang JIRA Olingo yang melacak karya pada modul JPA untuk V4, tetapi komentar terakhir berasal dari 2016. Ada juga adaptor JPA sumber terbuka pihak ketiga yang dihosting di repositori GitHub SAP yang, meskipun belum dirilis, tampaknya fitur lebih lengkap pada saat ini daripada Olingo.

Seperti biasa, semua kode untuk artikel ini tersedia di GitHub.