Quarkus - Extension for Spring Data API

While users are encouraged to use Hibernate ORM with Panache for Relational Database access, Quarkus provides a compatibility layer forSpring Data JPA repositories in the form of the spring-data-jpa extension.

Prerequisites

To complete this guide, you need:

  • less than 15 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.5.3+

Solution

We recommend that you follow the instructions in the next sections and create the application step by step.However, you can go right to the completed example.

Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git, or download an archive.

The solution is located in the spring-data-jpa-quickstart directory.

Creating the Maven project

First, we need a new project. Create a new project with the following command:

  1. mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=spring-data-jpa-quickstart \
  4. -DclassName="org.acme.spring.data.jpa.FruitResource" \
  5. -Dpath="/greeting" \
  6. -Dextensions="spring-data-jpa,resteasy-jsonb,quarkus-jdbc-postgresql"
  7. cd spring-data-jpa-quickstart

This command generates a Maven project with a REST endpoint and imports the spring-data-jpa extension.

Define the Entity

Throughout the course of this guide, the following JPA Entity will be used:

  1. package org.acme.spring.data.jpa;
  2. import javax.persistence.Entity;
  3. import javax.persistence.GeneratedValue;
  4. import javax.persistence.Id;
  5. @Entity
  6. public class Fruit {
  7. @Id
  8. @GeneratedValue
  9. private Long id;
  10. private String name;
  11. private String color;
  12. public Fruit() {
  13. }
  14. public Fruit(String name, String color) {
  15. this.name = name;
  16. this.color = color;
  17. }
  18. public Long getId() {
  19. return id;
  20. }
  21. public void setId(Long id) {
  22. this.id = id;
  23. }
  24. public String getName() {
  25. return name;
  26. }
  27. public void setName(String name) {
  28. this.name = name;
  29. }
  30. public String getColor() {
  31. return color;
  32. }
  33. public void setColor(String color) {
  34. this.color = color;
  35. }
  36. }

Configure database access properties

Add the following properties to application.properties to configure access to a local PostgreSQL instance.

  1. quarkus.datasource.url=jdbc:postgresql:quarkus_test
  2. quarkus.datasource.driver=org.postgresql.Driver
  3. quarkus.datasource.username=quarkus_test
  4. quarkus.datasource.password=quarkus_test
  5. quarkus.datasource.max-size=8
  6. quarkus.datasource.min-size=2
  7. quarkus.hibernate-orm.database.generation=drop-and-create

This configuration assumes that PostgreSQL will be running locally.

A very easy way to accomplish that is by using the following Docker command:

  1. docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name quarkus_test -e POSTGRES_USER=quarkus_test -e POSTGRES_PASSWORD=quarkus_test -e POSTGRES_DB=quarkus_test -p 5432:5432 postgres:11.5

If you plan on using a different setup, please change your application.properties accordingly.

Prepare the data

To make it easier to showcase some capabilities of Spring Data JPA on Quarkus, some test data should be inserted into the databaseby adding the following content to a new file named src/main/resources/import.sql:

  1. INSERT INTO fruit(id, name, color) VALUES (1, 'Cherry', 'Red');
  2. INSERT INTO fruit(id, name, color) VALUES (2, 'Apple', 'Red');
  3. INSERT INTO fruit(id, name, color) VALUES (3, 'Banana', 'Yellow');
  4. INSERT INTO fruit(id, name, color) VALUES (4, 'Avocado', 'Green');

Hibernate ORM will execute these queries on application startup.

Define the repository

It is now time to define the repository that will be used to access Fruit.In a typical Spring Data fashion create a repository like so:

  1. package org.acme.spring.data.jpa;
  2. import org.springframework.data.repository.CrudRepository;
  3. import java.util.List;
  4. public interface FruitRepository extends CrudRepository<Fruit, Long> {
  5. List<Fruit> findByColor(String color);
  6. }

The FruitRepository above extends Spring Data’s org.springframework.data.repository.CrudRepository which means that all of the latter’s methods areavailable to FruitRepository.Additionally findByColor is defined whose purpose is to return all Fruit entities that match the specified color.

Update the JAX-RS resource

With the repository in place, the next order of business is to update the JAX-RS resource that will use the FruitRepository. Open FruitResource and change its contents to:

  1. package org.acme.spring.data.jpa;
  2. import javax.ws.rs.DELETE;
  3. import javax.ws.rs.GET;
  4. import javax.ws.rs.POST;
  5. import javax.ws.rs.PUT;
  6. import javax.ws.rs.Path;
  7. import javax.ws.rs.Produces;
  8. import org.jboss.resteasy.annotations.jaxrs.PathParam;
  9. import java.util.List;
  10. import java.util.Optional;
  11. @Path("/fruits")
  12. public class FruitResource {
  13. private final FruitRepository fruitRepository;
  14. public FruitResource(FruitRepository fruitRepository) {
  15. this.fruitRepository = fruitRepository;
  16. }
  17. @GET
  18. @Produces("application/json")
  19. public Iterable<Fruit> findAll() {
  20. return fruitRepository.findAll();
  21. }
  22. @DELETE
  23. @Path("{id}")
  24. public void delete(@PathParam long id) {
  25. fruitRepository.deleteById(id);
  26. }
  27. @POST
  28. @Path("/name/{name}/color/{color}")
  29. @Produces("application/json")
  30. public Fruit create(@PathParam String name, @PathParam String color) {
  31. return fruitRepository.save(new Fruit(name, color));
  32. }
  33. @PUT
  34. @Path("/id/{id}/color/{color}")
  35. @Produces("application/json")
  36. public Fruit changeColor(@PathParam Long id, @PathParam String color) {
  37. Optional<Fruit> optional = fruitRepository.findById(id);
  38. if (optional.isPresent()) {
  39. Fruit fruit = optional.get();
  40. fruit.setColor(color);
  41. return fruitRepository.save(fruit);
  42. }
  43. throw new IllegalArgumentException("No Fruit with id " + id + " exists");
  44. }
  45. @GET
  46. @Path("/color/{color}")
  47. @Produces("application/json")
  48. public List<Fruit> findByColor(@PathParam String color) {
  49. return fruitRepository.findByColor(color);
  50. }
  51. }

FruitResource now provides a few REST endpoints that can be used to perform CRUD operation on Fruit.

Note on Spring Web

The JAX-RS resource can also be substituted with a Spring Web controller as Quarkus supports REST endpoint definition using Spring controllers.See the Spring Web guide for more details.

Update the test

To test the capabilities of FruitRepository proceed to update the content of FruitResourceTest to:

  1. package org.acme.spring.data.jpa;
  2. import io.quarkus.test.junit.QuarkusTest;
  3. import org.junit.jupiter.api.Test;
  4. import static io.restassured.RestAssured.given;
  5. import static org.hamcrest.CoreMatchers.containsString;
  6. import static org.hamcrest.CoreMatchers.is;
  7. import static org.hamcrest.CoreMatchers.notNullValue;
  8. import static org.hamcrest.core.IsNot.not;
  9. @QuarkusTest
  10. class FruitResourceTest {
  11. @Test
  12. void testListAllFruits() {
  13. //List all, should have all 3 fruits the database has initially:
  14. given()
  15. .when().get("/fruits")
  16. .then()
  17. .statusCode(200)
  18. .body(
  19. containsString("Cherry"),
  20. containsString("Apple"),
  21. containsString("Banana")
  22. );
  23. //Delete the Cherry:
  24. given()
  25. .when().delete("/fruits/1")
  26. .then()
  27. .statusCode(204)
  28. ;
  29. //List all, cherry should be missing now:
  30. given()
  31. .when().get("/fruits")
  32. .then()
  33. .statusCode(200)
  34. .body(
  35. not(containsString("Cherry")),
  36. containsString("Apple"),
  37. containsString("Banana")
  38. );
  39. //Create a new Fruit
  40. String idOfNewFruit = given()
  41. .when().post("/fruits/name/Orange/color/Orange")
  42. .then()
  43. .statusCode(200)
  44. .body(containsString("Orange"))
  45. .body("id", notNullValue())
  46. .extract().body().jsonPath().getString("id");
  47. //List all, Orange should be present now:
  48. given()
  49. .when().get("/fruits")
  50. .then()
  51. .statusCode(200)
  52. .body(
  53. not(containsString("Cherry")),
  54. containsString("Apple"),
  55. containsString("Orange")
  56. );
  57. }
  58. @Test
  59. void testFindByColor() {
  60. //Find by color that no fruit has
  61. given()
  62. .when().get("/fruits/color/Black")
  63. .then()
  64. .statusCode(200)
  65. .body("size()", is(0));
  66. //Find by color that multiple fruits have
  67. given()
  68. .when().get("/fruits/color/Red")
  69. .then()
  70. .statusCode(200)
  71. .body(
  72. containsString("Apple"),
  73. containsString("Strawberry")
  74. );
  75. //Find by color that matches
  76. given()
  77. .when().get("/fruits/color/Green")
  78. .then()
  79. .statusCode(200)
  80. .body("size()", is(1))
  81. .body(containsString("Avocado"));
  82. //Update color of Avocado
  83. given()
  84. .when().put("/fruits/id/4/color/Black")
  85. .then()
  86. .statusCode(200)
  87. .body(containsString("Black"));
  88. //Find by color that Avocado now has
  89. given()
  90. .when().get("/fruits/color/Black")
  91. .then()
  92. .statusCode(200)
  93. .body("size()", is(1))
  94. .body(
  95. containsString("Black"),
  96. containsString("Avocado")
  97. );
  98. }
  99. }

The test can be easily run by issuing: ./mvnw test

Package and run the application

Quarkus dev mode works with the defined repositories just like with any other Quarkus extension, greatly enhancing your productivity during the dev cycle.The application can be started in dev mode as usual using:

  1. ./mvnw compile quarkus:dev

Run the application as a native binary

You can of course create a native executable following the instructions of the this guide.

Supported Spring Data JPA functionalities

Quarkus currently supports a subset of Spring Data JPA’s features, namely the most useful and most commonly used features.

An important part of this support is that all repository generation is done at build time thus ensuring that all supported features work correctly in native mode.Moreover, developers know at build time whether or not their repository method names can be converted to proper JPQL queries.This also means that if a method name indicates that a field should be used that is not part of the Entity, developers will getthe relevant error at build time.

What is supported

The following sections described the most important supported features of Spring Data JPA.

Automatic repository implementation generation

Interfaces that extend any of the following Spring Data repositories are automatically implemented:

  • org.springframework.data.repository.Repository

  • org.springframework.data.repository.CrudRepository

  • org.springframework.data.repository.PagingAndSortingRepository

  • org.springframework.data.jpa.repository.JpaRepository

The generated repositories are also registered as beans so they can be injected into any other bean.Furthermore the methods that update the database are automatically annotated with @Transactional.

Fine tuning of repository definition

This allows user defined repository interfaces to cherry-pick methods from any of the supported Spring Data repository interfaces without having to extend those interfaces.This is particularly useful when for example a repository needs to use some methods from CrudRepository but it’s undesirable to expose the full list of methods of said interface.

Assume for example that a PersonRepository that shouldn’t extend CrudRepository but would like to use save and findById methods which are defined in said interface.In such a case, PersonRepository would look like so:

  1. package org.acme.spring.data.jpa;
  2. import org.springframework.data.repository.Repository;
  3. public class PersonRepository extends Repository<Person, Long> {
  4. Person save(Person entity);
  5. Optional<Person> findById(Person entity);
  6. }

Customizing individual repositories using repository fragments

Repositories can be enriched with additional functionality or override the default implementation of methods of the supported Spring Data repositories.This is best shown with an example.

A repository fragment is defined as so:

  1. public interface PersonFragment {
  2. // custom findAll
  3. List<Person> findAll();
  4. void makeNameUpperCase(Person person);
  5. }

The implementation of that fragment looks like this:

  1. import java.util.List;
  2. import io.quarkus.hibernate.orm.panache.runtime.JpaOperations;
  3. public class PersonFragmentImpl implements PersonFragment {
  4. @Override
  5. public List<Person> findAll() {
  6. // do something here
  7. return (List<Person>) JpaOperations.findAll(Person.class).list();
  8. }
  9. @Override
  10. public void makeNameUpperCase(Person person) {
  11. person.setName(person.getName().toUpperCase());
  12. }
  13. }

Then the actual PersonRepository interface to be used would look like:

  1. public interface PersonRepository extends JpaRepository<Person, Long>, PersonFragment {
  2. }

Derived query methods

Methods of repository interfaces that follow the Spring Data conventions can be automatically implemented (unless they fall into one of the unsupported cases listed later on).This means that methods like the following will all work:

  1. public interface PersonRepository extends CrudRepository<Person, Long> {
  2. List<Person> findByName(String name);
  3. Person findByNameBySsn(String ssn);
  4. Optional<Person> findByNameBySsnIgnoreCase(String ssn);
  5. boolean existsBookByYearOfBirthBetween(Integer start, Integer end);
  6. List<Person> findByName(String name, Sort sort);
  7. Page<Person> findByNameOrderByJoined(String name, Pageable pageable);
  8. List<Person> findByNameOrderByAge(String name);
  9. List<Person> findByNameOrderByAgeDesc(String name, Pageable pageable);
  10. List<Person> findByAgeBetweenAndNameIsNotNull(int lowerAgeBound, int upperAgeBound);
  11. List<Person> findByAgeGreaterThanEqualOrderByAgeAsc(int age);
  12. List<Person> queryByJoinedIsAfter(Date date);
  13. Collection<Person> readByActiveTrueOrderByAgeDesc();
  14. Long countByActiveNot(boolean active);
  15. List<Person> findTop3ByActive(boolean active, Sort sort);
  16. Stream<Person> findPersonByNameAndSurnameAllIgnoreCase(String name, String surname);
  17. }

User defined queries

User supplied queries contained in the @Query annotation. For example things like the following all work:

  1. public interface MovieRepository extends CrudRepository<Movie, Long> {
  2. Movie findFirstByOrderByDurationDesc();
  3. @Query("select m from Movie m where m.rating = ?1")
  4. Iterator<Movie> findByRating(String rating);
  5. @Query("from Movie where title = ?1")
  6. Movie findByTitle(String title);
  7. @Query("select m from Movie m where m.duration > :duration and m.rating = :rating")
  8. List<Movie> withRatingAndDurationLargerThan(@Param("duration") int duration, @Param("rating") String rating);
  9. @Query("from Movie where title like concat('%', ?1, '%')")
  10. List<Object[]> someFieldsWithTitleLike(String title, Sort sort);
  11. @Modifying
  12. @Query("delete from Movie where rating = :rating")
  13. void deleteByRating(@Param("rating") String rating);
  14. @Modifying
  15. @Query("delete from Movie where title like concat('%', ?1, '%')")
  16. Long deleteByTitleLike(String title);
  17. @Modifying
  18. @Query("update Movie m set m.rating = :newName where m.rating = :oldName")
  19. int changeRatingToNewName(@Param("newName") String newName, @Param("oldName") String oldName);
  20. @Modifying
  21. @Query("update Movie set rating = null where title =?1")
  22. void setRatingToNullForTitle(String title);
  23. @Query("from Movie order by length(title)")
  24. Slice<Movie> orderByTitleLength(Pageable pageable);
  25. }

All methods that are annotated with @Modifying will automatically be annotated with @Transactional.

More examples

An extensive list of examples can be seen in the integration tests directory which is located inside the Quarkus source code.

What is currently unsupported

  • Methods of the org.springframework.data.repository.query.QueryByExampleExecutor interface - if any of these are invoked, a Runtime exception will be thrown.

  • QueryDSL support. No attempt will be made to generate implementations of any of the QueryDSL related repositories.

  • Customizing the base repository for all repository interfaces in the code base.

In Spring Data JPA this is done by registering a class that extends org.springframework.data.jpa.repository.support.SimpleJpaRepository however in Quarkus this classis not used at all (since all the necessary plumbing is done at build time). Similar support might be added to Quarkus in the future.

  • Using java.util.concurrent.Future and classes that extend it as return types of repository methods.

  • Native and named queries when using @Query

The Quarkus team is exploring various alternatives to bridging the gap between the JPA and Reactive worlds.

Important Technical Note

Please note that the Spring support in Quarkus does not start a Spring Application Context nor are any Spring infrastructure classes run.Spring classes and annotations are only used for reading metadata and / or are used as user code method return types or parameter types.

More Spring guides

Quarkus support has more Spring compatibility features. See the following guides for more details: