iTranslated by AI
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.
- 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 |
- 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)
-
- 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.
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:
- **http://localhost:8080/authors?page=1&size=20**: For paging
- **http://localhost:8080/authors?sort=firstName,asc**: For sorting by firstName
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.
- Processing and Extracting Values (Projections & Excerpts)
https://spring.pleiades.io/spring-data/rest/reference/projections-excerpts.html - Integration with Spring Security
https://spring.pleiades.io/spring-data/rest/reference/security.html - Integration with Validator
https://spring.pleiades.io/spring-data/rest/reference/validation.html
Discussion