Utilize events in Spring Data REST to perform pre and post operations with @RepositoryEventHandler
.
Spring Data REST helps developer to create REST application with little implementation as it turns Repository
classes into REST endpoint. In this example we will explore
how we can perform actions before and after persisting an entity using @RepositoryEventHandler
and its related annotations.
There are two @Entity
classes; Author and Book, and JpaRepository
classes; AuthorRepository and BookRepository.
Author @Entity
consists of id
, name
, and status
:
@Entity
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Author {
@Id
@GeneratedValue
private Long id;
@NotBlank
private String name;
@OneToMany(mappedBy = "author", cascade = CascadeType.REMOVE)
private Set<Book> books;
private Status status;
public enum Status {
ACTIVE,
INACTIVE
}
}
While its JpaRepository
class contains no additional methods:
public interface AuthorRepository extends JpaRepository<Author, Long> {
}
Book @Entity
consists of id
, author
, and title
fields:
@Entity
@Data
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Book {
@Id
@GeneratedValue
private Long id;
@JoinColumn
@ManyToOne(optional = false)
@NotNull
private Author author;
@NotBlank
private String title;
}
Same as Author
, its repository contains no additional methods:
public interface BookRepository extends JpaRepository<Book, Long> {
}
As we can see, both classes are normal JPA entity classes without any additional configurations. Once we boot the
application there will be two REST endpoints available; /authors
and /books
.
Next is to add a validation against Book.author
where we will ensure that an Author
of a Book
must be ACTIVE
before
adding it into the database. For this we will use @RepositoryEventHandler
with @HandleBeforeCreate
.
By annotating our class with RepositoryEventHandler, we are informing Spring that this class will manage repository related events.
Along with it we will use HandleBeforeCreate, which indicate that the method need to be executed before persisting given @Entity
.
We will implement our validation in AuthorRepositoryEventHandler:
@RepositoryEventHandler
@AllArgsConstructor
@SuppressWarnings("unused")
public class AuthorRepositoryEventHandler {
@HandleBeforeCreate
public void validateAuthorStatus(Book book) {
Author author = book.getAuthor();
Assert.isTrue(Author.Status.ACTIVE == author.getStatus(), "book author must be active");
}
}
As we can see, the method validateAuthorStatus
takes Book
as a parameter and validate status
of an Author
to make
sure that the author is still ACTIVE
.
Next is to inform the application that we would like to register AuthorRepositoryEventHandler
as a Bean
and thus will be triggered
when related entity is being used. This can be found in RepositoryHandlerConfiguration:
@Configuration
public class RepositoryHandlerConfiguration {
@Bean
public AuthorRepositoryEventHandler authorRepositoryEventHandler() {
return new AuthorRepositoryEventHandler();
}
}
As always, we will verify our implementation through an integration tests. In the following test we will create an Author
with status INACTIVE
and followed by creating a Book
that ties to the Author
.
We will expect that it will return Internal Server Error
with a message book author must be active
:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class BookRepositoryRestTests {
@Autowired
private MockMvc mvc;
@Test
public void createBookWithInactiveAuthor() throws Exception {
String authorUri = getAuthorUri(INACTIVE);
JSONObject request = new JSONObject();
request.put("author", authorUri);
request.put("title", "If");
mvc.perform(
post("/books")
.content(request.toString())
)
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message", is("book author must be active")));
}
private String getAuthorUri(Author.Status status) throws Exception {
JSONObject request = new JSONObject();
request.put("name", "Rudyard Kipling");
request.put("status", status.name());
return mvc.perform(
post("/authors")
.content(request.toString())
)
.andExpect(status().isCreated())
.andReturn().getResponse().getHeader(LOCATION);
}
}
With a correct implementation, the test above should pass.