Repositories
Learn
What is a Repository?​
A repository is Spring's abstraction of the data access layer. In this bootcamp, we use the JPA (Java Persistence API) to implement the repository.
The core functionality of every API is to serve data stored in a database. The repository interface is agnostic to the back-end, be that an in-memory database, an SQL instance, or some flavor of no-SQL.
What is an entity?​
An entity is a Java class that can be mapped to a JPA repository. To create an entity with Spring, two annotation are required at minimum:
@Entity
over the class definition and@Id
over the ID field.
What is a DTO and why do we use them?​
In a Spring application, a data transfer object (or DTO) is a POJO that mirrors and entity. It contains all the same fields as the entity, but none of the functionality (except getter and setters). We use DTOs to encapsulate relevant data for transport. As you'll see in the exercise, our GET
endpoint will now accept and return a DTO.
Because DTOs mirror entities, translating a DTO to an entity and vice versa is very simple. We will use a transformer class to do this in the exercise.
Do​
1. Add dependencies​
In order to add a data persistence layer, we need to add some new dependencies to build.gradle
:
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2'
...
}
Important
There are two groups (or closures) named dependencies
: one within the buildscript
closure at the top of the file and one on its own further down. We want to add there dependencies to the standalone closure.
The first dependency adds JPA to the project, while the second add our database options: H2, in-memory database.
Make sure to refresh your Gradle project after adding these dependencies. IntelliJ may also prompt you to choose whether or not you'd like to auto-import changes. You may do so if you with, but it isn't necessary.
2. Write StudentEntity
and StudentRepository
​
We will use our existing Student
POJO as the DTO of our service rather than creating a new DTO class. We will create a new StudentEntity
class to use with the repository.
In the src/main/java/com.geteche.students
directory, create a new package classed entity
.
In the newly create package, create a new Java class called StudentEntity.java
.
This class should have the same two fields as Student
(id
and name
), a default constructor, parameterized constructor, and getters, just like our original Student
class.
- Code Snippet
- Full Code
@Entity
@Table(name = "student")
public class StudentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
...
}
If copying the full file you must replace the bootcamp
path in the package name and imports to match your project name.
package com.geteche.students.entity;
import javax.persistence.*;
@Entity
@Table(name = "student")
public class StudentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
String name;
public StudentEntity() {
}
public StudentEntity(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name= name;
}
}
As mentioned above,
@Entity
designated our class as a Spring entity.@Table
tells the JPA which database table this entity is stored in. we don't technically need it for this example, but in a real-world application with a complex, pre-existing database, it is a necessary.@Id
denoted which field stores the ID (i.e the primary key) of the entity in the database.@GeneratedValue
tells Spring that the following field should be automatically generated. This is commonly used on Id fields (as we are using it) to ensure that every instance of the entity has a unique ID.
The repository should be created as an interface within a new package named repository
.
In the src/main/java/com.geteche.students
directory, create a new package called repository
.
In the newly created package, create a new Java interface called StudentRepository.java
.
- Code Snippet
- Full Code
public interface StudentRepository extends JpaRepository<StudentEntity, Integer> {}
package com.geteche.students.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.geteche.students.entity.StudentEntity;
public interface StudentRepository extends JpaRepository<StudentEntity, Integer> {
}
The two values within the angle bracket (<>
) are the entity stored in this repository and the type of that entity's ID, respectively. This is why the id
field of our Student
class is an Integer
instead of an int
; and int
isn't an object, and thus can't be used as a parameter of a genericized class.
3. Refactor StudentService
​
We do not want StudentService
to be creating our Student objects directly anymore. We want to use methods of our StudentRepository
to retrieve them. We also need to update the service to return StudentEntity
objects rather than Student
objects.
The JpaRepository
interface (which our StudentRepository
extends) provides us with several methods for performing basic CRUD (create, read, update, delete) operations. We can also define our own methods in our repositories, but for the bootcamp, we won't need to.
- Code Snippet
- Full Code
@Service
public class StudentService {
@Autowired
StudentRepository studentRepository;
public StudentEntity getStudentById(Integer id) {
Optional<StudentEntity> myStudent = studentRepository.findById(id);
}
...
}
If copying the full file you must replace the bootcamp
path in the package name and imports to match your project name.
package com.geteche.students.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.geteche.students.entity.StudentEntity;
import com.geteche.students.repository.StudentRepository;
import java.util.Optional;
@Service
public class StudentService {
@Autowired
StudentRepository studentRepository;
public StudentEntity getStudentById(Integer id) {
Optional<StudentEntity> student = studentRepository.findById(id);
return student.orElseThrow();
}
}
4. Write StudentTransformer
​
For the transformer, in the src/main/java/com.geteche.students
directory, create a new package called transformer
.
In the newly created package, create a new Java class called StudentTransformer.java
.
The transformer only needs two static methods: one to convert from a StudentEntity
to Student
and one to convert from a Student
to a StudentEntity
.
info
We are writing this transformer to demonstrate the process, but in an actual application, you might want to use an external library such as MapStruct to reduce the amount of code you have to write.
- Code Snippet
- Full Code
public class StudentTransformer {
public static Student toStudent(StudentEntity fromStudent) {
...
}
public static StudentEntity toStudentEntity(Student fromStudent) {
...
}
}
If copying the full file you must replace the bootcamp
path in the package name and imports to match your project name.
package com.geteche.students.transformer;
import com.geteche.students.entity.StudentEntity;
import com.geteche.students.model.Student;
public class StudentTransformer {
public static Student toStudent(StudentEntity fromStudent) {
Student student = new Student();
student.setId(fromStudent.getId());
student.setName(fromStudent.getName());
return student;
}
public static StudentEntity toStudentEntity(Student fromStudent) {
StudentEntity studentEntity = new StudentEntity();
studentEntity.setId(fromStudent.getId());
studentEntity.setName(fromStudent.getName());
return studentEntity;
}
}
In addition, we need to refactor the getStudentById()
method in our StudentController
to utilize the transformer. More specifically, the method returns a Student
, so we will have to convert the StudentEntity
we receive from StudentService
to a Student
before returning it.
- Code Snippet
- Full Code
...
public Student getStudentById(@PathVariable("StudentId") Integer id) {
return StudentTransformer.toStudent(StudentService.getStudentById(id));
}
package com.geteche.students.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import com.geteche.students.model.Student;
import com.geteche.students.service.StudentService;
import com.geteche.students.transformer.StudentTransformer;
@RestController
public class StudentController {
@Autowired
StudentService studentService;
@GetMapping(value = "/v1/students/{studentId}", produces = MediaType.APPLICATION_JSON_VALUE)
public Student getStudentById(@PathVariable("studentId") Integer id) {
return StudentTransformer.toStudent(studentService.getStudentById(id));
}
}
5. Write POST
endpoint​
Now that we are using our repository, we need to be able to write to it. For this, we will be creating a new endpoint in StudentController
.
Just like our GET
endpoint, our new POST
endpoint requests an annotated method in the controller. The header for the controller method is provided below:
- Code Snippet
- Full Code
@PostMapping(
value = "/v1/students",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Student createStudent(@RequestBody Student student) {
...
}
package com.geteche.students.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import com.geteche.students.model.Student;
import com.geteche.students.entity.StudentEntity;
import com.geteche.students.service.StudentService;
import com.geteche.students.transformer.StudentTransformer;
@RestController
public class StudentController {
@Autowired
StudentService studentService;
@GetMapping(value = "/v1/students/{studentId}", produces = MediaType.APPLICATION_JSON_VALUE)
public Student getStudentById(@PathVariable("studentId") Integer id) {
return StudentTransformer.toStudent(studentService.getStudentById(id));
}
@PostMapping(
value = "/v1/students",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public Student createStudent(@RequestBody Student student) {
StudentEntity studentToCreate = StudentTransformer.toStudentEntity(student);
StudentEntity createdStudent = studentService.createStudent(studentToCreate);
return StudentTransformer.toStudent(createdStudent);
}
}
In addition, we need to write a createStudent()
method in StudentService
to actually perform the logic.
- Code Snippet
- Full Code
public StudentEntity createStudent(StudentEntity studentToCreate) {
...
}
If copying the full file you must replace the bootcamp
path in the package name and imports to match your project name.
package com.geteche.students.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.geteche.students.entity.StudentEntity;
import com.geteche.students.repository.StudentRepository;
import java.util.Optional;
@Service
public class StudentService {
@Autowired StudentRepository studentRepository;
public StudentEntity getStudentById(Integer id) {
Optional<StudentEntity> student = studentRepository.findById(id);
return student.orElseThrow();
}
public StudentEntity createStudent(StudentEntity studentToCreate) {
return studentRepository.save(studentToCreate);
}
}
6. Update Configuration files​
Now that we've implemented a Spring repository, we need a database to back it, For now, we will use an H2 in-memory database. To add this, add the following lines to the end of src/main/resources/application.properties
:
- Code Snippet
- Full Code
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
# Prefix all routes with /api
server.servlet.contextPath=/api
#Enable CORS for actuator endpoints
management.endpoints.web.cors.allowed-origins=*
management.endpoints.web.cors.allowed-methods=OPTIONS,GET
management.endpoints.web.cors.allowed-headers=*
management.endpoints.web.exposure.include=health,prometheus,info
spring.profiles.active=${ENVIRONMENT_NAME:local}
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
7. Write tests​
In this step, we will:
- Update existing unit and integration tests.
- Write and additional test in
StudentControllerTest
andStudentServiceTest
for thecreateStudent()
method. - Add a new unit test for
StudentTransformer
. - Update the integration test for first create a Student before retrieving it.
- Below are just example if you stuck, Please tru on your own first.
- Examples
- StudentControllerTest
- StudentServiceTest
- StudentTransformerTest
Because we need to mock static method contained in StudentTransformer.java
, and additional dependency must be added to the build.gradle
file
dependencies {
...
testImplementation('org.mockito:mockito-inline:3.9.0')
...
}
package com.geteche.students.controller;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.MockedStatic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import com.geteche.students.model.Student;
import com.geteche.students.entity.StudentEntity;
import com.geteche.students.service.StudentService;
import com.geteche.students.transformer.StudentTransformer;
@SpringBootTest
public class StudentControllerTest {
@Autowired
private StudentController studentController;
@MockBean
private StudentService studentService;
@Test
public void getStudentByIdTest() {
Integer id = 1;
String name = "John";
StudentEntity studentToReturn = new StudentEntity(id, name);
Mockito.when(studentService.getStudentById(id)).thenReturn(studentToReturn);
Student Student = studentController.getStudentById(id);
assertThat(Student.getId()).isEqualTo(id);
assertThat(Student.getName()).isEqualTo(name);
}
@Test
public void createStudentTest() {
Integer id = 1;
String name = "John";
Student student = new Student(id, name);
StudentEntity studentEntity = new StudentEntity(id, name);
try (MockedStatic<StudentTransformer> transformer = Mockito.mockStatic(StudentTransformer.class)) {
transformer
.when(
() -> {
StudentTransformer.toStudentEntity(student);
}
)
.thenReturn(studentEntity);
Mockito.when(studentService.createStudent(studentEntity)).thenReturn(studentEntity);
transformer
.when(
() -> {
StudentTransformer.toStudent(studentEntity);
}
)
.thenReturn(student);
Student createdStudent = studentController.createStudent(student);
assertThat(createdStudent.getId()).isEqualTo(studentEntity.getId());
assertThat(createdStudent.getName()).isEqualTo(studentEntity.getName());
}
}
}
package com.geteche.students.service;
import static org.assertj.core.api.Assertions.assertThat;
import com.geteche.students.entity.StudentEntity;
import com.geteche.students.repository.StudentRepository;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.Optional;
@SpringBootTest
public class StudentServiceTest {
@Autowired
private StudentService studentService;
@MockBean
private StudentRepository studentRepository;
@Test
public void getStudentByIdTest() {
Integer id = 1;
StudentEntity studentToCreate = new StudentEntity(id, "John");
Mockito.when(studentRepository.findById(Mockito.anyInt())).thenReturn(Optional.of(studentToCreate));
StudentEntity Student = studentService.getStudentById(id);
assertThat(Student.getId()).isEqualTo(id);
assertThat(Student.getName()).isEqualTo("John");
}
@Test
public void saveStudentTest() {
StudentEntity studentToCreate = new StudentEntity(1, "John");
Mockito.when(studentRepository.save(studentToCreate)).thenReturn(studentToCreate);
StudentEntity createdStudent = studentService.createStudent(studentToCreate);
assertThat(studentToCreate.getId()).isEqualTo(createdStudent.getId());
assertThat(studentToCreate.getName()).isEqualTo(createdStudent.getName());
}
}
package com.geteche.students.transformer;
import static org.assertj.core.api.Assertions.assertThat;
import com.geteche.students.entity.StudentEntity;
import com.geteche.students.model.Student;
import org.junit.jupiter.api.Test;
public class StudentTransformerTest {
@Test
public void toStudentTest() {
Integer id = 2;
String name = "John";
StudentEntity studentEntity = new StudentEntity(id, name);
Student Student = StudentTransformer.toStudent(studentEntity);
assertThat(Student.getId()).isEqualTo(id);
assertThat(Student.getName()).isEqualTo(name);
}
@Test
public void toStudentEntityTest() {
Integer id = 3;
String name = "Chris";
Student student = new Student(id, name);
StudentEntity StudentEntity = StudentTransformer.toStudentEntity(student);
assertThat(StudentEntity.getId()).isEqualTo(id);
assertThat(StudentEntity.getName()).isEqualTo(name);
}
}
note
We do not need to write any unit tests for StudentRepository
because we did not write any new methods for it. we assume that the writers of the JPA have thoroughly tested its functionality. However, if we had written our own methods in StudentRepository
, we would need to test them.