1. Project Setup
Let’s start with a new Java Maven project created in your favorite IDE. Once you have a pom.xml
file, add a <dependencyManagement/>
section with "BOM" ("Bill of Materials") declarations for Spring Boot and Agrest:
<dependencyManagement>
<dependencies>
<!-- Defines the versions of Agrest modules -->
<dependency>
<groupId>io.agrest</groupId>
<artifactId>agrest-bom</artifactId>
<!-- Replace with a specific 5.x version -->
<version>${agrest.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!-- Defines the versions of Spring Boot and its dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Defines the version of Derby DB -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>10.14.2.0</version>
</dependency>
</dependencies>
</dependencyManagement>
This will allow the following <dependencies/>
section to include Agrest, Spring and other modules and not worry about their individual versions. Now let’s set up the <dependencies/>
section:
<dependencies>
<!-- Adds Agrest integration for JAX-RS / Jersey 2 -->
<dependency>
<groupId>io.agrest</groupId>
<artifactId>agrest-jaxrs2</artifactId>
</dependency>
<!-- Adds an Agrest DB backend based on Cayenne ORM -->
<dependency>
<groupId>io.agrest</groupId>
<artifactId>agrest-cayenne</artifactId>
</dependency>
<!-- Spring Boot + Jersey -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<!-- Simple embedded Java DB -->
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
</dependency>
</dependencies>
The first two dependencies are Agrest-related, others are SpringBoot and Derby.
2. ORM Mapping
Apache Cayenne is a powerful ORM, fully integrated with Agrest. To makes things easier for this tutorial, we will provide ready-to-use Cayenne ORM model files. And you will generate persistent Java classes from the model with the CayenneModeler GUI tool.
If you want to learn more about Cayenne, here is a link to get started. |
The following 2 XML files comprise Cayenne mapping project. Copy both of them to the src/main/resources
folder:
Model file 1: src/main/resources/cayenne-project.xml
<?xml version="1.0" encoding="utf-8"?>
<domain xmlns="http://cayenne.apache.org/schema/10/domain"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd"
project-version="10">
<map name="datamap"/>
<node name="datanode"
factory="org.apache.cayenne.configuration.server.XMLPoolingDataSourceFactory"
schema-update-strategy="org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy">
<map-ref name="datamap"/>
<data-source>
<driver value="org.apache.derby.jdbc.EmbeddedDriver"/>
<url value="jdbc:derby:target/db;create=true"/>
<connectionPool min="1" max="5"/>
<login/>
</data-source>
</node>
</domain>
Model file 2: src/main/resources/datamap.map.xml
<?xml version="1.0" encoding="utf-8"?>
<data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
project-version="10">
<property name="defaultPackage" value="io.agrest.tutorial.sb.persistence"/>
<property name="quoteSqlIdentifiers" value="true"/>
<db-entity name="author">
<db-attribute name="date_of_birth" type="DATE" isMandatory="true"/>
<db-attribute name="id" type="BIGINT" isPrimaryKey="true" isGenerated="true" isMandatory="true"/>
<db-attribute name="name" type="VARCHAR" isMandatory="true" length="200"/>
</db-entity>
<db-entity name="book">
<db-attribute name="author_id" type="BIGINT" isMandatory="true"/>
<db-attribute name="id" type="BIGINT" isPrimaryKey="true" isGenerated="true" isMandatory="true"/>
<db-attribute name="title" type="VARCHAR" isMandatory="true" length="200"/>
</db-entity>
<obj-entity name="Author" className="io.agrest.tutorial.sb.persistence.Author" dbEntityName="author">
<obj-attribute name="dateOfBirth" type="java.time.LocalDate" db-attribute-path="date_of_birth"/>
<obj-attribute name="name" type="java.lang.String" db-attribute-path="name"/>
</obj-entity>
<obj-entity name="Book" className="io.agrest.tutorial.sb.persistence.Book" dbEntityName="book">
<obj-attribute name="title" type="java.lang.String" db-attribute-path="title"/>
</obj-entity>
<db-relationship name="books" source="author" target="book" toMany="true">
<db-attribute-pair source="id" target="author_id"/>
</db-relationship>
<db-relationship name="author" source="book" target="author">
<db-attribute-pair source="author_id" target="id"/>
</db-relationship>
<obj-relationship name="books" source="Author" target="Book" deleteRule="Deny" db-relationship-path="books"/>
<obj-relationship name="author" source="Book" target="Author" deleteRule="Nullify" db-relationship-path="author"/>
<cgen xmlns="http://cayenne.apache.org/schema/10/cgen">
<destDir>../java</destDir>
<mode>entity</mode>
<template>templates/v4_1/subclass.vm</template>
<superTemplate>templates/v4_1/superclass.vm</superTemplate>
<template>templates/v4_1/subclass.vm</template>
<superTemplate>templates/v4_1/superclass.vm</superTemplate>
<embeddableTemplate>templates/v4_1/embeddable-subclass.vm</embeddableTemplate>
<embeddableSuperTemplate>templates/v4_1/embeddable-superclass.vm</embeddableSuperTemplate>
<queryTemplate>templates/v4_1/datamap-subclass.vm</queryTemplate>
<querySuperTemplate>templates/v4_1/datamap-superclass.vm</querySuperTemplate>
<outputPattern>*.java</outputPattern>
<makePairs>true</makePairs>
<usePkgPath>true</usePkgPath>
<overwrite>false</overwrite>
<createPropertyNames>false</createPropertyNames>
<createPKProperties>false</createPKProperties>
<client>false</client>
</cgen>
</data-map>
Now let’s generate persistent Java classes from the ORM model:
-
Download CayenneModeler GUI and open it on your computer. For this tutorial you will need the latest version of the Modeler
4.2
. -
Open
cayenne-project.xml
in the Modeler. -
Expand the project tree on the left, navigating to
project
>datamap
, and selectClass Generation
tab on the right:
-
Make sure the
Output Directory
field points to yoursrc/main/java
folder, and then click theGenerate
button. -
If everything went well, you should see 4 Java classes generated under
src/main/java/io/agrest/tutorial/sb/persistence
, that represent our persistent entities:-
auto/_Author.java
-
auto/_Book.java
-
Author.java
-
Book.java
-
3. REST Endpoints
3.1. POST: Create data
Let’s write a couple of POST endpoints to allow users to create Authors and Books. We’ll start by creating Java classes AuthorApi
and BookApi
, placing a few JAX-RS annotations on them, and injecting JAX-RS Configuration
object:
@Path("author")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AuthorApi {
@Context
private Configuration config;
}
@Path("book")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class BookApi {
@Context
private Configuration config;
}
Now let’s create POST methods in both classes. In AuthorApi
:
@POST
public DataResponse<Author> create(@Context UriInfo uri, String data) {
return AgJaxrs.create(Author.class, config) (1)
.clientParams(uri.getQueryParameters()) (2)
.syncAndSelect(data); (3)
}
1 | Start a builder for "create" operation. config parameter passed to create(..) , contains Agrest runtime |
2 | Pass URL parameters. This will control the shape of the response |
3 | Execute create operation and return created Author to the client |
In BookApi
@POST
public SimpleResponse create(String data) {
return AgJaxrs.create(Book.class, config).sync(data);
}
Notice the difference between Author and Book POST methods. In the former case, after creating an object Agrest returns DataResponse<Author>
, allowing the client to inspect the result of the operation (e.g. obtain the Author’s ID generated by the server). In the latter case, it returns SimpleResponse
that does not contain any data.
3.2. REST App Class
Now that we have a few endpoint, how do we integrate them in our app? Let’s create a main class of our SpringBoot application called AgrestApp
with some bootstrap code:
@ApplicationPath("/api")
@SpringBootApplication
public class AgrestApp extends ResourceConfig { (1)
public static void main(String[] args) {
SpringApplication application = new SpringApplication(AgrestApp.class);
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}
public AgrestApp() {
(2)
ServerRuntime cayenneRuntime = ServerRuntime
.builder()
.addConfig("cayenne-project.xml")
.build();
(3)
AgRuntime agRuntime = AgRuntime
.builder()
.module(AgCayenneModule.build(cayenneRuntime))
.build();
(4)
register(AgJaxrsFeature.build(agRuntime));
(5)
register(AuthorApi.class);
register(BookApi.class);
}
}
1 | Our app class extends ResourceConfig provided by Jersey (our JAX-RS provider). Configuration happens in the constructor below. ResourceConfig subclass is automatically loaded by SpringBoot on startup. |
2 | Create Cayenne runtime |
3 | Create Agrest runtime with Cayenne backend |
4 | Register Agrest runtime with Jersey / JAX-RS environment |
5 | Register API endpoints with Jersey |
3.3. Run the App and Create Data
Now we can run the main class in the IDE, and use curl
or a similar HTTP client to create some data:
curl -X POST -i \
-d '{"name":"Ernest Hemingway","dateOfBirth":"1899-07-21"}' \
-H 'Content-Type: application/json' \
http://localhost:8080/api/author
The response might look like this (note the id value, we’ll need it to create Books) :
HTTP/1.1 201
Content-Type: application/json
Content-Length: 55
Date: Sat, 14 May 2022 09:39:39 GMT
{"data":[{"id":1,"dateOfBirth":"1899-07-21","name":"Ernest Hemingway"}],"total":1}
Now let’s create a Book
. In the following POST request, use the ID of the author from the previous call:
curl -X POST -i \
-d '[{"title":"A Farewell to Arms","author":1},{"title":"For Whom the Bell Tolls","author":1}]' \
-H 'Content-Type: application/json' \
http://localhost:8080/api/book
Since BookApi
used SimpleResponse
, the JSON returned will be just an acknowledgement of success:
HTTP/1.1 201
Content-Type: application/json
Content-Length: 16
Date: Sat, 14 May 2022 09:43:01 GMT
{}
3.4. GET: Select Data
Now that we have some data in the DB, let’s build an endpoint to retrieve it via GET
requests. In BookApi
add the following method:
@GET
public DataResponse<Book> get(@Context UriInfo uriInfo) {
return AgJaxrs.select(Book.class, config)
.clientParams(uriInfo.getQueryParameters())
.get();
}
Now you can restart the app, and run the following request:
curl http://localhost:8080/api/book
{
"data":[
{"id":1,"title":"A Farewell to Arms"},
{"id":2,"title":"For Whom the Bell Tolls"}
],
"total":2
}
Since we passed all URL parameters directly to Agrest (via clientParams(uriInfo.getQueryParameters())
), we can pass various Agrest protocol keys in the URL, to filter the result, resolve related objects, etc. Some examples:
Include Author in the response, and exclude ids:
curl 'http://localhost:8080/api/book?exclude=id&include=author.name'
{
"data":[
{"author":{"name":"Ernest Hemingway"},"title":"For Whom the Bell Tolls"},
{"author":{"name":"Ernest Hemingway"},"title":"A Farewell to Arms"}
],
"total":2
}
Sort by title:
curl 'http://localhost:8080/api/book?include=title&sort=title'
{
"data":[
{"title":"A Farewell to Arms"},
{"title":"For Whom the Bell Tolls"}
],
"total":2}
3.5. PUT: Update Data
Similar to how we implemented POST
, you can also write PUT
endpoints. We are leaving it as an exercise for the reader, but here are some hints:
-
PUT
requires anid
attribute to be present for each object in the request payload. -
You would need to use
AgJaxrs.update(..)
method to process the request.
PUT can be used not only for updates, but also for "create-or-update" operations. Agrest has special methods for those: AgJaxrs.createOrUpdate(..) , AgJaxrs.idempotentCreateOrUpdate(..) , etc. Those work with objects that have natural keys understood by the client. This is not directly applicable to our Book and Author entities, though with a bit of a stretch we can use Author.name property as a natural key. |
4. Custom Property
Agrest has a powerful API for building REST entity schemas, but so far we didn’t have to interact with it. Agrest simply took the existing model from Cayenne and exposed each ORM entity as a REST entity. In reality, REST entities often look very different from persistent objects, and there are various mechanisms to define their structure and retrieval strategy. Here we will show the simplest way to define a new property in an existing entity.
Let’s imagine we would like to add the age
property to the Author
object. This is not a persistent property, and is not known to Cayenne. Yet, it is rather easy to calculate from the current date and the Author.dateOfBirth
property. So we can write a getter method on the Author that performs such calculation. We will use a special @AgAttribute
annotation to tell Agrest that the new property is a part of the REST model:
@AgAttribute
public int getAge() {
return Period.between(getDateOfBirth(), LocalDate.now()).getYears();
}
Now let’s restart the app, and run the following request for books with authors and their ages:
curl 'http://localhost:8080/api/book?exclude=id&include=author.name&include=author.age'
{
"data":[
{"author":{"age":122,"name":"Ernest Hemingway"},"title":"For Whom the Bell Tolls"},
{"author":{"age":122,"name":"Ernest Hemingway"},"title":"A Farewell to Arms"}
],
"total":2
}
We can see that as of this writing, Hemingway was 122 years old. @AgAttribute
on getters is not the only way to add custom properties, bit it is very handy for simple calculated properties like our example.
5. OpenAPI Documentation
It is a good practice to document your APIs for the benefit of consumers. The most popular format for REST API documentation is OpenAPI 3. Agrest integrates with the Swagger library to expose its endpoints as OpenAPI model. Let’s include it in our tutorial.
Start by adding agrest-jaxrs2-openapi
dependency to the pom.xml
:
<dependency>
<groupId>io.agrest</groupId>
<artifactId>agrest-jaxrs2-openapi</artifactId>
</dependency>
Now add OpenAPI bootstrap code to AgrestApp
:
AgRuntime agRuntime = AgRuntime
.builder()
.module(AgCayenneModule.build(cayenneRuntime))
.module(AgSwaggerModule
.builder()
.entityPackage(Book.class.getPackage())
.build()) (1)
.build();
register(OpenApiResource.class); (2)
1 | Adding this configuration to AgRuntime builder would indicate to Swagger that the API model for both Book and Author entities should be taken from Agrest instead of using reflection on the Java classes. |
2 | Add Swagger endpoint to the app. |
Now you can restart the application and go to http://127.0.0.1:8080/api/openapi.json in the browser. You will see the app model in JSON format, including all the Agrest query parameters.
The next step is to add a Swagger UI browser for visual exploration of the documentation. We will use a Swagger integration library from https://springdoc.org/ . Let’s add it as a dependency:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.9</version>
</dependency>
And now create src/main/resources/application.properties
file and add the following configuration entry in it:
springdoc.swagger-ui.url=/api/openapi.json
Restart the application and open http://127.0.0.1:8080/swagger-ui.html in the browser You should see the navigable UI for your API docs similar to the one in this screenshot:
You can make your model even more informative by annotating BookApi
and AuthorApi
with Swagger annotations, such as @OpenAPIDefinition
, @Operation
, etc. We’ll leave it as an exercise for the user.
6. Conclusion
This concludes our tutorial. From here you should check the "Protocol" document on how to interact with Agrest from the client, and the "Server Framework" document for an in-depth discussion of how to write model-driven endpoints with user-specific schemas, security controls, a variety of backends, etc.