REST Contract API and Data Access over JPA for Quarkus(JEE) or SpringBoot
REST Contract is a Java library targeted on creating generic REST Resource Services and Data Access Layers on top of JPA using CDI or Spring.
It develops REST Services starting from the CRUD pattern and extends it by adding frequently needed methods to each domain entity.
The idea behind it is that just by adding these generic services on each entity of your database you get a general 70 - 90% implementation of the services needed by the application, thus allowing the developer to focus just on the complex cases, in other words removing as much as "boilerplate code" as possible.
And that comes with already developed test units that bring 100% coverage to all provided service methods.
The library can be very easily extended to add more functionality by reusing and extending the provided components.
Initially designed for Quarkus it can also be used in any JEE( CDI / JPA) or SpringBoot / JPA compliant environment.
Let's start with a database table named Modell and the associated JPA Entity.
Let the entity implement the PrimaryKey and the SelfTransferObject interfaces :
@Data
@NoArgsConstructor
@Entity
public class Modell implements PrimaryKey<Long>, SelfTransferObject<Modell> {
@Id
@NotNull
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Update
private String name;
@Update(dynamic = false)
private String street;
@Update(dynamic = false)
private Integer number;
@EqualsAndHashCode.Exclude
private long age;
}Notice the used @Data annotation from Lombok.
Extend your resource service from AbstractResourceServiceImpl.
in Quarkus:
@Path("/modell")
public class ModellResourceService extends AbstractResourceServiceImpl<Modell, Modell, Long> {
}or SpringBoot:
@Getter
@RestController
@RequestMapping("/modell")
public class ModellResourceService extends AbstractResourceServiceImpl<Modell, Modell, Long> {
@Autowired
protected DataAccess<Modell, Long> dataAccess;
@Autowired
protected DataBinder<Modell, Modell, Long> dataBinder;
}and ... you're pretty much done. More Infos here or here.
For the Modell entity the following REST services are available :
- GET /modell/{id} - finds and returns the corresponding entity for the given path id.
- POST /modell/byId - finds and returns the corresponding entity for the given body id using POST.
- GET /modell/all/asList - returns all the entities for the given table.Filter Parameters and Pagination Parameters can be used to filter the list.
- GET /modell/byIds/{ids}/asList - finds and returns the corresponding entity for the given list of id's.
- POST /modell/byIds/asList - finds and returns the corresponding entity for the given list of id's in the body using POST.
- GET /modell/filter/{stringField}/equals/{value}/asList - finds all entities whose value in a specified string field is equal to the given value. Pagination Parameters can be applied.
- GET /modell/filter/{stringField}/like/{value}/asList - finds all entities whose value in a specified field is like the given path value. Pagination Parameters can be applied.
- GET /modell/filter/{stringField}/in/{values}/asList - finds all entities whose value in a specified field is in the given path values list. Pagination Parameters can be applied.
- GET /modell/autocomplete/{stringField}/like/{value}/asSortedSet - finds all values in a field whose value is like the given value.Autocomplete Parameters,Filter Parameters and Pagination Parameters can be applied.
- GET /modell/autocompleteIds/{stringField}/like/{value}/asList - finds all entities whose value in a field is like the given value, groups them, and returns for each a group of ids with the corresponding id.Autocomplete Parameters,* Filter Parameters* and Pagination Parameters can be used to filter the list.
- POST /modell/filter/content/equals/value/asList - finds all entities that equals a given body content object. * Pagination Parameters* can be applied.
- POST /modell/filter/content/in/values/asList - finds all entities that are in a given body content list of given values. Pagination Parameters can be applied.
- POST /modell/ - inserts a new entity in the database.
- POST /modell/list/asList - inserts a list of new entities in the database.
- PUT /modell/ - updates an existing entity by id.
- PUT /modell/list/asList - updates existing entities by id.
- DELETE /modell/{id}/ - deletes the entity for the given id.
- DELETE /modell/byIds - deletes all the entities for the given ids in the request body
- DELETE /modell/byIds/{ids} - deletes all the entities for the given ids.
Pagination Parameters
For every request returning a list items with a variable count two parameters can be used for pagination or simply to limit the result count.
- firstResult - position of the first result, numbered from 0. If unspecified, it defaults to 0 or it can be configured using the ResourceServiceConfig
- maxResults - maximum number of results to retrieve. If unspecified, it defaults to 256 or it can be configured using the ResourceServiceConfig Examples:
- GET /modell/all/asList?firstResult=0&maxResults=100 - return a maximum 100 results list.
- GET /modell/all/asList?firstResult=40&maxResults=10 - returns the page 5 out of a 10 per page sequence.
Filter Parameters
When using filter parameters the query can be appended with column based values to filter for. Also orderBy parameters can be added. Examples:
- GET /modell/all/asList?number=10 - returns all the models that have number 10
- GET /modell/all/asList?number=10&number=12 - returns all the models that have number 10 or 12
- GET /modell/all/asList?orderBy=number - returns all the models, ordered by number
- GET /modell/all/asList?orderBy=number desc - returns all the models, ordered by number descending order
Autocomplete Parameters
When using autocomplete type Queries two parameters come into action:
- cut - the minimum character input count for the query to produce results. If unspecified, it defaults to 3 or it can be configured using the ResourceServiceConfig
- maxResults - maximum number of results to retrieve. If unspecified, it defaults to 16 or it can be configured using the ResourceServiceConfig
What does the @Update annotation do ?
The Resource Service uses the entity as both DAO and DTO. Upon update though it is important to be able to configure which fields participate in the update process and how null values impact that. The @Update annotation marks the fields accordingly.
The Annotation can be used on every field or only once on the class.
When a field is annotated, it will be updated from the provided source during a PUT or POST operation.
When used on the class, all fields will be updated, except the ones annotated with @Update.excluded annotation.
If a field is not annotated (or excluded), it will not participate in the update process. That is for example the case for the id field and for our last field in the example (age).
A dynamic Update means that during the update process, the request contains only the fields that are to be changed. During the dynamic update the value of a field can not be set to null, so if a null value is received, it will be ignored. This is the default case using the @Update annotation, or @Update(dynamic = true)
If we decide for a field to not use dynamic update, this can be enforced with @Update(dynamic = false). For non-nullable fields this can lead though, that the field value is actually set to null, leading to a runtime-exception. So the actual value of the field must be always received in the Request.
As a best practice the non-dynamic update is generally recommended to be used on all the fields of the entity. Then the update source transfer object must be always complete.
One-to-one relations pointing to a child-entity can also participate in the update process. The @Update entity must be used on the child-entity field. The child entity must implement the required interfaces and its fields have to be marked for the update accordingly with the @Update annotation.
One-to-many relations and @ElementCollection can also participate in the update process and the previous rules apply. For non-entity collections and maps the update will be applied completely, the current content will be removed and the new content from the request will be inserted. For child-entity collections and maps the algorithm is more complex:
- the child-entities having the same id in the request and in the database will each be updated with its counterpart matching the id.
- the child-entities that are missing (do not have an id in the request) will be deleted.
- the child-entities having a new id (or no id for autogenerated ids) will be inserted.
- partial inserts or updates are not available. So is also the case for targeted deletes.
- for the algorithm to work correctly, all collections and maps must be initialized accordingly and nulls must be avoided.
- for dynamic updates if the field in the incoming request is null, then no changes will be done to the collection.
The best practice here is though to update these collection entities through a dedicated REST service belonging to the child-entity.
Every entity participating in the update process must implement the SelfTransferObject interface. The entity must also implement the PrimaryKey interface and provide a unique id field. If the primary key of the table is composed of several database columns, @EmbeddedId can be used like here.
Extending the DAO layer
In complex cases the Data Access of the entity must be extended, by adding the new data methods. Let's start by extending DataAccess.
in Quarkus:
@Dependent
public class ModellDataAccess extends DataAccess<Modell, Long> {
@Inject
public ModellDataAccess() {
super(Modell.class, Long.class);
}
public List<Modell> getAllModellsOver100() {
CriteriaBuilder criteriaBuilder = em().getCriteriaBuilder();
CriteriaQuery<Modell> query = criteriaBuilder.createQuery(type);
Root<Modell> entity = query.from(type);
return em().createQuery(query.select(entity)
.where(criteriaBuilder.greaterThan(entity.get("age"), 100L)))
.getResultList();
}
}or SpringBoot:
@Service
public class ModellDataAccess extends DataAccess<Modell, Long> {
public ModellDataAccess() {
super(Modell.class, Long.class);
}
public List<Modell> getAllModellsOver100() {
CriteriaBuilder criteriaBuilder = em().getCriteriaBuilder();
CriteriaQuery<Modell> query = criteriaBuilder.createQuery(type);
Root<Modell> entity = query.from(type);
return em().createQuery(query.select(entity)
.where(criteriaBuilder.greaterThan(entity.get("age"), 100L)))
.getResultList();
}
}The method getAllModellsOver100() uses the underlining em() method to access the available EntityManager and builds the query using the CriteriaBuilder.
Or if a JPQL approach is to be considered :
// ... //
public class ModellDataAccess extends DataAccess<Modell, Long> {
// ... //
public List<Modell> getAllModellsOver100() {
return em().createQuery(" select t from Modell where t.age > 100")
.getResultList();
}
}Now let's use this newly ModellDataAccess in our Resource Service.
in Quarkus:
@Getter
@Path("/modell")
public class ModellResourceService extends AbstractResourceServiceImpl<Modell, Long> {
@Inject
ModellDataAccess dataAccess;
/**
* Finds and returns all the models over 100
*
* @return the models list.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/over/100")
public List<Modell> getAllModellsOver100() {
return this.getDataAccess()
.getAllModellsOver100();
}
}or SpringBoot:
@Getter
@RestController
@RequestMapping("/modell")
public class ModellResourceService extends AbstractResourceServiceImpl<Modell, Modell, Long> {
@Autowired
protected ModellDataAccess dataAccess;
@Autowired
protected DataBinder<Modell, Modell, Long> dataBinder;
/**
* Finds and returns all the models over 100
*
* @return the models list.
*/
@GetMapping(path = "/over/100", produces = APPLICATION_JSON_VALUE)
public List<Modell> getOver100AsList() {
return getDataAccess().getAllModellsOver100();
}
}and we're ready to go:
- [GET] /modell/over/100 - Finds and returns all the models over 100
Please do notice the this.getDataAccess() method that gets overridden behind the scenes with Lombok
So far so good. But how can I be sure that the generated services do really work on my platform or with my entities ? Not to mention that there are already 17 methods in the service, and that goes for each entity.
Let's start by creating the TestUnit by extending AbstractResourceServiceImplTest.
in Quarkus:
@QuarkusTest
@Transactional
public class ModellResourceServiceTest extends AbstractResourceServiceImplTest<Modell, Long> {
static final String path = "/modell";
private static final String stringField = "stringVal";
private static final Producer<Modell> producer;
private static final List<Modell> insertData;
private static final List<Modell> updateData;
static {
producer = Producer.ofClass(Modell.class)
.withList(LinkedList::new)
.withMap(LinkedHashMap::new)
.withSize(Config.collectionSize);
insertData = producer.produceList();
updateData = producer.changeList(insertData);
}
public ModellResourceServiceTest() {
super(Modell.class, //
path, //
insertData, //
updateData, //
stringField,//
producer); //
}
}or Springboot:
@SpringBootTest(webEnvironment = DEFINED_PORT)
@Import(RestContractCoreTestPersistenceConfiguration.class)
public class ModellResourceServiceTest extends AbstractResourceServiceImplTest<Modell, Long> {
static final String path = "/modell";
private static final String stringField = "stringVal";
private static final Producer<Modell> producer;
private static final List<Modell> insertData;
private static final List<Modell> updateData;
static {
producer = Producer.ofClass(Modell.class)
.withList(LinkedList::new)
.withMap(LinkedHashMap::new)
.withSize(Config.collectionSize);
insertData = producer.produceList();
updateData = producer.changeList(insertData);
}
public ModellResourceServiceTest() {
super(Modell.class, //
path, //
insertData, //
updateData, //
stringField,//
producer); //
}
}Notice the use of the Producer class that generates automatically complete lists with instance objects for tests.
The test goes through all the provided methods :
Notice that the actual entities used in the test are omitted for simplicity from the example.
The ModellResourceServiceTest is a UnitTest where test methods can be further added :
@QuarkusTest
public class ModellResourceServiceTest extends AbstractResourceServiceImplTest<Modell, Long> {
public ModellResourceServiceTest() {
/// .....
}
@Test
@Order(1000)
void testGetAllModellsOver100() {
/// your favorite method gets tested here
}
}Notice the use of the @Order(1000) annotation, this will ensure the correct order of running.
My application grows steadily and every day I add new entities. It's time to present the resource services to my clients in a ready to code manner.
For Quarkus the smallrye-openapi dependency (io.quarkus:quarkus-smallrye-openapi) ensures the generation of the open API yaml and json file.
This can be further customized in the properties file.
quarkus.smallrye-openapi.store-schema-directory=openapi/api
quarkus.smallrye-openapi.open-api-version=3.0.3For Springboot the springdoc-openapi dependency (org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0) ensures the generation of the open API json file.
This can be further customized in the pom file
Then the org.openapitools:openapi-generator-maven-plugin:7.0.1 plugin will generate the classes for the front end.
Here is an example for Angular using Typescript.
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.10.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/openapi/api/openapi.json</inputSpec>
<generatorName>typescript-angular</generatorName>
<configOptions>
<sourceFolder>src/gen/java/main</sourceFolder>
</configOptions>
<output>${project.basedir}/openapi/generated</output>
<verbose>false</verbose>
<cleanupOutput>true</cleanupOutput>
</configuration>
</execution>
</executions>
</plugin>Here ist an example of the generated files:
А comprehensive example of using the library you can find in the Quarkus Example or Spring Example module.
The library works with Java 17+, Quarkus 3.6.7+, Spring Boot 3.0+, JPA 2+
Simply add io.github.agache41:quarkus-rest-contract:version or io.github.agache41:spring-rest-contract:version dependency to your project.
The current version today at sunset:
<version>1.0.0</version>The dependency for the main jar:
<dependency>
<groupId>io.github.agache41</groupId>
<artifactId>quarkus-rest-contract</artifactId>
<version>${version}</version>
</dependency>or
<dependency>
<groupId>io.github.agache41</groupId>
<artifactId>spring-rest-contract</artifactId>
<version>${version}</version>
</dependency>For the test context the tests-classified jar is needed:
<dependency>
<groupId>io.github.agache41</groupId>
<artifactId>quarkus-rest-contract</artifactId>
<version>${version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>or
<dependency>
<groupId>io.github.agache41</groupId>
<artifactId>spring-rest-contract</artifactId>
<version>${version}</version>
<classifier>tests</classifier>
<type>test-jar</type>
<scope>test</scope>
</dependency>- Easy to install, use and extend.
- Test coverage provided on the fly.
- Works with both Jackson and JSONB. No support yet for reactive mode.
- Tested with Quarkus 3.6.7 and Spring Boot 3.4.0
The library is packaged as a single jar. An auxiliary test classifier jar is provided for test context.
Execution dependencies consist in the needed packages from Jakarta and JbossLogging and are not transitive.
<dependencies>
<dependency>
<groupId>io.github.agache41</groupId>
<artifactId>rest-contract-core</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.5.3.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>Testing dependencies are listed here. Please note the Lombok is used only in the test context.
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.4.1.Final</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
</dependencies>
