iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
♨️

Spring Data REST: Key Points and Usage

に公開

Introduction

I expect to be using a library called Spring Data REST at work, so I have summarized the key points and usage methods I researched through hands-on practice.

What is Spring Data REST?

Spring Data REST is one of the libraries in the Spring Data ecosystem. It automatically exposes repositories created with Spring Data as RESTful endpoints. By utilizing the features of Spring Data REST, you can omit the implementation of Controller and Service classes.

Spring Data abstracts the characteristics and specifications of various data stores and provides simple interfaces. For example, in the case of RDBMS, if you use JPA, you can perform CRUD operations simply by defining an Entity class and creating a Repository class based on that Entity.

For instance, when using an RDBMS, a user table can be mapped to a Java object (Entity class) like this:

@Entity
public class User {
  @Id
  private Long id;
  private String name;
  private Integer age;
  // getter, setter
}

Then, for the Repository class, by inheriting the interface provided by JPA as shown below, you can use various methods to achieve CRUD operations.

public interface UserRepository extends JpaRepository<User,Long> {}

This UserRepository inherits methods for basic CRUD operations such as findAll, findById, save, and deleteById. By using these methods, you can perform tasks like retrieving, saving, and deleting user information.

Spring Data REST is a library that exposes these defined Repositories as RESTful API endpoints (meaning you no longer need to implement Controller or Service classes).

Introduction to Spring Data REST Features

Spring Data REST is a powerful tool that allows developers to manage data more efficiently and provide RESTful APIs. Below are three important features of Spring Data REST.

  1. Auto-generation of CRUD Operations: Just by defining a repository interface, Spring Data REST automatically generates RESTful endpoints that support CRUD (Create, Read, Update, Delete) operations. This eliminates the need for developers to manually write a large amount of code for data management, significantly reducing development time.

Specifically, CRUD operations, endpoints, and JPA methods correspond as follows:

Operation HTTP Method Endpoint JPA Method
Create POST /entities save
Read All GET /entities findAll
Read One GET /entities/{id} findById
Update PUT/PATCH /entities/{id} save
Delete DELETE /entities/{id} deleteById
  1. Paging and Sorting: When dealing with large amounts of data, paging and sorting features are crucial. Spring Data REST supports paging and sorting out of the box, allowing data to be appropriately divided and reordered based on requests from the client. This makes it possible to handle large datasets efficiently.

Specifically, when using paging or sorting, you add query parameters to the GET endpoint. Here are examples of the query parameters used:

  • Paging:
    • ?page=0&size=10 (Example: Get 10 items from the first page)
  • Sorting:
    • ?sort=name,asc (Example: Sort by name in ascending order)
    • ?sort=age,desc (Example: Sort by age in descending order)
  1. Query Method Support: Spring Data REST supports query methods by mapping methods defined in the repository interface directly to RESTful endpoints. This allows clients to specify complex search conditions or calculation formulas directly, making data retrieval more flexible.

Specifically, query methods are mapped to endpoints as follows:

Example: A query method defined in UserRepository, such as findByName, is mapped to the endpoint /users/search/findByName?name={name}.

  • Definition of query method:
public interface UserRepository extends JpaRepository<User, Long> {
  List<User> findByName(String name);
}
  • Example of endpoint call:
GET /users/search/findByName?name=John

There are various other features, but this time I would like to focus on introducing these three features.

Creating a Simple Endpoint

What I Implemented This Time

Source code is available here.

https://github.com/Showichiro/spring-data-rest-example-for-qiita

I have created a Spring Boot application. It uses H2DB so you can easily try it out.
Some test data is also automatically populated, so please feel free to explore.

You can check the database and API endpoints through the console.

While the application is running, access the following URLs:

  • DB
    http://localhost:8080/h2-console
  • API
    http://localhost:8080/swagger-ui/index.html

If prompted for input in the DB console, please enter the following details:

Item Name Input Value
Driver Class org.h2.Driver
JDBC URL jdbc:h2:mem:testdb
User Name sa
Password

Java version 21 is specified.

Below, I will explain the theme setting and key points of the implementation.

Theme Setting

Consider an application using Author and Post tables. Assume a relationship where an Author can have multiple Posts.

Implementation

Implementing Entities

Expressing this as JPA Entities looks like the following.

@Data
@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String firstName;
    private String lastName;
    private String password;
    @CreatedDate
    private LocalDate createdAt;
    @LastModifiedDate
    private LocalDate updatedAt;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "author_id")
    private List<Post> posts = new ArrayList<>();
}
@Data
@Entity
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private String content;
    @ManyToOne
    @JoinColumn(name = "author_id")
    private Author author;
    @CreatedDate
    private LocalDate createdAt;
    @LastModifiedDate
    private LocalDate updatedAt;
}

Implementing Repositories

Create Repositories corresponding to these tables.

public interface AuthorRepository extends JpaRepository<Author, Long> {
}
public interface PostRepository extends JpaRepository<Post, Long> {
}

Automatically Exposed Endpoints

Through these implementations, the following endpoints are created.

Endpoint HTTP Method JPA Method Target Entity
/authors GET findAll Author
/authors/{id} GET findById Author
/authors POST save Author
/authors/{id} PUT/PATCH save Author
/authors/{id} DELETE deleteById Author
/posts GET findAll Post
/posts/{id} GET findById Post
/posts POST save Post
/posts/{id} PUT/PATCH save Post
/posts/{id} DELETE deleteById Post

Also, when relationships are defined as in this case, endpoints to access the related resources in the form of REST resource parent-child relationships are automatically created.

Endpoint HTTP Method Description
/authors/{id}/posts GET Endpoint to retrieve all posts related to a specific author
/posts/{id}/author GET Endpoint to retrieve the author related to a specific post

When you start the application and call an endpoint in this state, a response like the following is returned.

  • request
    http://localhost:8080/authors

  • response

{
  "_embedded": {
    "authors": [
      {
        "firstName": "John",
        "lastName": "Doe",
        "password": "password123",
        "createdAt": "2024-05-23",
        "updatedAt": "2024-05-23",
        "_links": {
          "self": {
            "href": "http://localhost:8080/authors/1"
          },
          "author": {
            "href": "http://localhost:8080/authors/1"
          },
          "posts": {
            "href": "http://localhost:8080/authors/1/posts"
          }
        }
      },
      {
        "firstName": "Jane",
        "lastName": "Smith",
        "password": "password456",
        "createdAt": "2024-05-23",
        "updatedAt": "2024-05-23",
        "_links": {
          "self": {
            "href": "http://localhost:8080/authors/2"
          },
          "author": {
            "href": "http://localhost:8080/authors/2"
          },
          "posts": {
            "href": "http://localhost:8080/authors/2/posts"
          }
        }
      },
      {
        "firstName": "Alice",
        "lastName": "Johnson",
        "password": "password789",
        "createdAt": "2024-05-23",
        "updatedAt": "2024-05-23",
        "_links": {
          "self": {
            "href": "http://localhost:8080/authors/3"
          },
          "author": {
            "href": "http://localhost:8080/authors/3"
          },
          "posts": {
            "href": "http://localhost:8080/authors/3/posts"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/authors?page=0&size=20"
    },
    "profile": {
      "href": "http://localhost:8080/profile/authors"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 3,
    "totalPages": 1,
    "number": 0
  }
}

As you can see, the retrieved resources and total counts are returned. This response follows the principles of HATEOAS (Hypermedia as the Engine of Application State).

Paging and Sorting

In addition, for endpoints that retrieve a list, you can achieve paging by specifying page and size in the query parameters, or sorting by specifying a sort parameter in the format {paramName},{asc/desc}.

For example, you can make requests like the following:

Configuring Excluded Items

For example, there are many cases where you would not want to include an Author's password as an API response item. You can prevent a property from being returned by adding the @JsonIgnore annotation to properties you don't want to include in the API response.

@Data
@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String firstName;
    private String lastName;
    @JsonIgnore
    private String password;
    @CreatedDate
    @JsonIgnore
    private LocalDate createdAt;
    @LastModifiedDate
    @JsonIgnore
    private LocalDate updatedAt;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "author_id")
    private List<Post> posts = new ArrayList<>();
}

By adding @JsonIgnore in this way, you can exclude it from the returned items. If you actually call it in this state, fields like password will no longer be returned.

{
  "_embedded": {
    "authors": [
      {
        "firstName": "John",
        "lastName": "Doe",
        "_links": {
          "self": {
            "href": "http://localhost:8080/authors/1"
          },
          "author": {
            "href": "http://localhost:8080/authors/1"
          },
          "posts": {
            "href": "http://localhost:8080/authors/1/posts"
          }
        }
      },
      {
        "firstName": "Jane",
        "lastName": "Smith",
        "_links": {
          "self": {
            "href": "http://localhost:8080/authors/2"
          },
          "author": {
            "href": "http://localhost:8080/authors/2"
          },
          "posts": {
            "href": "http://localhost:8080/authors/2/posts"
          }
        }
      },
      {
        "firstName": "Alice",
        "lastName": "Johnson",
        "_links": {
          "self": {
            "href": "http://localhost:8080/authors/3"
          },
          "author": {
            "href": "http://localhost:8080/authors/3"
          },
          "posts": {
            "href": "http://localhost:8080/authors/3/posts"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/authors?page=0&size=20"
    },
    "profile": {
      "href": "http://localhost:8080/profile/authors"
    }
  },
  "page": {
    "size": 20,
    "totalElements": 3,
    "totalPages": 1,
    "number": 0
  }
}

Customizing Endpoints

Configuring Endpoints to Not Be Exposed

For example, if you design for logical deletion and do not want to create a delete endpoint, you would not want to expose the physical DELETE endpoint. You can use the @RestResource annotation to configure endpoints that you do not want to expose.

public interface AuthorRepository extends JpaRepository<Author, Long> {
    @Override
    @RestResource(exported = false)
    void deleteById(Long id);
}

Since this is a library that automatically implements endpoints based on Repository methods, you can prevent exposure by setting annotations on the corresponding methods. You can also configure the exposure of the entire Repository or set the default exposure settings.

Implementing Query Method Endpoints

JPA has the functionality to finely control query content through method names or the @Query annotation. Spring Data REST also supports this functionality.

For example, if you want to implement a query to extract only Posts that contain a specific string in the title, you can set up a query method like this:

public interface PostRepository extends JpaRepository<Post, Long> {
    public List<Post> findByTitleContaining(String word);
}

Spring Data REST exposes this content as an endpoint. Specifically, an endpoint like the following will be generated:

Endpoint HTTP Method Required Parameters Description
/posts/search/findByTitleContaining GET word Endpoint to search for posts whose title contains a specific string

In this way, you can configure specific endpoints.

Other Features

Repository Class Exposure Settings

For the default exposure settings, the official reference suggests configuring them via RepositoryRestConfiguration. However, if you are using Spring Boot, you can also configure this in application.properties.

spring.data.rest.detection-strategy=ANNOTATED

You can choose from the following four exposure strategies:

Exposure Strategy Description
ALL Exposes all Spring Data repositories regardless of Java visibility or annotation configuration.
DEFAULT Exposes public Spring Data repositories or repositories explicitly annotated with @RepositoryRestResource whose exported attribute is not set to false.
VISIBILITY Exposes only public Spring Data repositories, regardless of annotation configuration.
ANNOTATED Exposes only Spring Data repositories explicitly annotated with @RepositoryRestResource and whose exported attribute is not set to false.

Exposure Settings for Each Method

The exposure setting for each method can be controlled by the exported attribute of @RestResource. However, even with the most restrictive ANNOTATED strategy, it is stated that it "exposes only... whose exported attribute is not set to false," meaning that basically everything is exposed once the repository itself is exposed.
For example, if you have:

public interface AuthorRepository extends JpaRepository<Author, Long> {
}

All CRUD operations will basically be exposed through various methods inherited from JpaRepository, such as save and findById.

In most use cases where you want to keep everything private by default and only expose some parts, setting exported=false for every single method would be quite cumbersome. Therefore, I think it's better to create a Repository interface that is private by default.

package com.example.demo.repository;

import java.io.Serializable;
import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.Repository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;

@RepositoryRestResource
@NoRepositoryBean
public interface DefaultFalseRepository<T, ID extends Serializable> extends Repository<T, ID> {
    @RestResource(exported = false)
    < S extends T > List< S > findAll();

    @RestResource(exported = false)
    < S extends T > Page< S > findAll(Pageable p);

    @RestResource(exported = false)
    < S extends T > S findById(ID id);

    @RestResource(exported = false)
    < S extends T > S save(S s);

    void deleteById(ID id);
}

My current thought is that it's best to prepare a custom Repository like this and have others inherit from it.

Official References

Here are links to content that seems likely to be frequently used, extracted for convenience.

Discussion