Agrest Getting Started


Through this tutorial we will be building a simple Bookstore REST application using Agrest and a relational database. Agrest can work with various Java REST API stacks and backends. So there are some choices to make for the purpose of the tutorial. So we decided to use the following components:

  • SpringBoot / Jersey

  • Derby database

  • Apache Cayenne ORM backend

We’ll be using Maven and Java 17 to build and run the project. If you prefer Gradle, it should work as well of course.

The full code of the tutorial is available on Github.

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 select Class Generation tab on the right:

cgen
  • Make sure the Output Directory field points to your src/main/java folder, and then click the Generate 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
{"success":true}

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 an id 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:

swagger ui

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.