RELATION MANY TO MANY
WITH JPA
IN SPRING BOOT
Today, I want to create simple service to demo relation many to many with JPA on Spring Boot. Every first step always start from the simple one
So, lets start...
Prerequired
- check_circle Spring boot 3.5.6
- check_circle Intelij Idea (My darling IDE)
- check_circle Java 17
- check_circle Maven
- check_circle Postman
Support System
- check_circle Girlfriend (not found)
- check_circle Matcha less sugar
- check_circle Cigarette
Setup Project
Go to Spring Initialzr Spring Initializr then create new project. Name it rocket-service.
Dependencies
Add dependencies such as: micrometer, actuator, data-jpa, mysql.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>I'm using mysql database, you can use any database you want. No pressure here :v
Lets assume we have these table:

Configure Properties
Set profile active in application.properties.
spring.profiles.active=dev
Create new file application-dev.properties inside resources folder.
spring.application.name=rocket-service
server.servlet.context-path=/spaces
server.port=8005
# DB CREDENTIAL
db.host=localhost
db.port=3306
db.name=spaces
spring.datasource.url=jdbc:mysql://${db.host}:${db.port}/${db.name}
spring.datasource.username=root
spring.datasource.password=root;
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
# Hibernate properties
spring.jpa.show-sql=false
spring.jpa.hibernate.ddl-auto=none
# Hikari Config
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
# LOG
management.tracing.sampling.probability=1.0
management.endpoints.web.exposure.include=*
management.metrics.distribution.percentiles-histogram.greeting=true
log.max.file.size=20MB
log.total.size=50GB
log.max.history=50
log.folder=./logUser Entity
Create java class AsteroidDao at /model/db/
@Setter
@Getter
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "rockets", schema = "spaces")
public class RocketDao implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false)
private String name;
@Column(name = "fuel_tank", nullable = false)
private int fuelTank;
@Column(name = "astronout_cap", nullable = false)
private int astronoutCap;
@JsonIgnore // ignore serialization this field when get data
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(
name = "rocket_asteroid", // join table
joinColumns = @JoinColumn(name = "rocket_id"), // FK to rocket
inverseJoinColumns = @JoinColumn(name = "asteroid_id") // FK to asteroid
)
private List<AsteroidDao> asteroids = new ArrayList<>();
}Note @JsonIgnore to ignore this field from serialization when fetch data from table.
Repository
Create AsteroidRepository class on /repository/
public interface AsteroidRepository extends JpaRepository<AsteroidDao, Long> {
@Query(value = "SELECT * FROM spaces.`asteroids`",
countQuery = "SELECT COUNT(*) FROM spaces.`asteroids`", nativeQuery = true)
Page<AsteroidDao> findList(Pageable pageable);
}Then create RocketRepository in same package.
public interface RocketRepository extends JpaRepository<RocketDao, Long> {
@Query(value = "SELECT * FROM spaces.`rockets`",
countQuery = "SELECT COUNT(*) FROM spaces.`rockets`", nativeQuery = true)
Page<RocketDao> findList(Pageable pageable);
}Data Request
Create class AsteroidReq in /model/api/asteroid
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AsteroidReq {
private String name;
private double minDiameterKm;
private double maxDiameterKm;
private boolean isHazardous;
}Create class AsteroidRes
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AsteroidRes implements Serializable {
private static final long serialVersionUID = 726217904044648520L;
private String asteroidName;
private double minDiameterKm;
private double maxDiameterKm;
private boolean isHazardous;
}Create class PageAsteroidRes
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageAsteroidRes implements Serializable {
private static final long serialVersionUID = 4517668868366878466L;
private List<AsteroidRes> list;
private int totalPage;
private int totalSize;
}Create class AstronoutRes in /model/api/astronout/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AstronoutRes implements Serializable {
private static final long serialVersionUID = 311690368590658270L;
private String result;
}Create class RocketReq in /model/api/rocket/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RocketReq {
private String name;
private int fuelTank;
private int astronoutCap;
private List<AsteroidReq> listAsteroid;
}Create class RocketRes
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RocketRes implements Serializable {
private static final long serialVersionUID = -9092005685412629514L;
private String rocketName;
private int rocketFuelTank;
private int astronoutCap;
private List<AsteroidDao> listAsteroid;
}Create class PageRocketRes
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageRocketRes implements Serializable {
private static final long serialVersionUID = -9187132093202453943L;
private List<RocketRes> list;
private int totalPage;
private int totalSize;
}Service Helper
Create class RocketDataHelper in /service/helper/
@Slf4j
@Service
@RequiredArgsConstructor
public class RocketDataHelper {
@Autowired
private RocketRepository rocketRepository;
public RocketDao storeRocket(RocketDao rocketDao) {
return rocketRepository.save(rocketDao);
}
public Page<RocketDao> getPageRocket(int page, int size) {
Sort sort = Sort.by("id").ascending();
Pageable pageable = PageRequest.of(page, size, sort);
return rocketRepository.findList(pageable);
}
}Create class AsteroidDataHelper
@Slf4j
@Service
@RequiredArgsConstructor
public class AsteroidDataHelper {
@Autowired
private AsteroidRepository asteroidRepository;
public AsteroidDao storeAsteroid(AsteroidDao asteroidDao) {
return asteroidRepository.save(asteroidDao);
}
public Page<AsteroidDao> getPageAsteroid(int page, int size) {
Sort sort = Sort.by("id").descending();
Pageable pageable = PageRequest.of(page, size, sort);
return asteroidRepository.findList(pageable);
}
}Create class TriangleHelper
@Slf4j
@Service
@RequiredArgsConstructor
public class TriangleHelper {
public String getNormalTriangle(int max) {
StringBuilder result = new StringBuilder();
for (int i = 1; i < max + 1; i++) {
for (int j = 1; j < i + 1; j++) {
result.append(j).append(" ");
}
result.append("\n");
}
return result.toString().trim();
}
public String getPascalTriangle(int max) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < max; i++) {
int number = 1;
for (int j = 0; j <= i; j++) {
result.append(number).append(" ");
number = number * (i - j) / (j + 1);
}
result.append("\n");
}
return result.toString().trim();
}
}Service Usecase
Create class RocketService in /service/usecase/
@Slf4j
@Service
public class RocketService {
@Autowired
private RocketDataHelper rocketDataHelper;
public ResponseEntity<ApiBaseResponse<RocketRes>> storeRocket(Metadata metadata, RocketReq req) {
log.info("Start store rocket");
RocketRes response = null;
try {
List<AsteroidDao> listAsteroid = req.getListAsteroid().stream().map(ast -> AsteroidDao.builder()
.name(ast.getName())
.maxDiameterKm(ast.getMaxDiameterKm())
.minDiameterKm(ast.getMinDiameterKm())
.isHazardous(ast.isHazardous())
.build()).toList();
RocketDao rocketDao = RocketDao.builder()
.name(req.getName())
.fuelTank(req.getFuelTank())
.astronoutCap(req.getAstronoutCap())
.build();
rocketDao.setAsteroids(listAsteroid);
RocketDao resultRocket = rocketDataHelper.storeRocket(rocketDao);
response = RocketRes.builder()
.rocketName(resultRocket.getName())
.rocketFuelTank(resultRocket.getFuelTank())
.astronoutCap(resultRocket.getAstronoutCap())
.listAsteroid(resultRocket.getAsteroids())
.build();
} catch (Exception e) {
log.error("Error when store rocket: " + e.getMessage());
throw new RocketException(ERROR_MESSAGE_SYSTEM_ERROR, RC_CODE_SYSTEM_ERROR, ERROR_DESCRIPTION_SYSTEM_ERROR);
}
return ResponseUtil.buildHttpResponse(ResponseUtil.buildResponse(RC_CODE_SUCCESS, ERROR_MESSAGE_SUCCESS, response));
}
public ResponseEntity<ApiBaseResponse<PageRocketRes>> getListRocket(Metadata metadata, int page, int size) {
log.info("Start get list rocket");
PageRocketRes response = null;
try {
Page<RocketDao> pageRocket = rocketDataHelper.getPageRocket(page, size);
List<RocketRes> listRocketRes = pageRocket.getContent().stream().map(rock -> RocketRes.builder()
.rocketName(rock.getName())
.rocketFuelTank(rock.getFuelTank())
.astronoutCap(rock.getAstronoutCap())
.listAsteroid(rock.getAsteroids())
.build()).toList();
response = PageRocketRes.builder()
.list(listRocketRes)
.totalPage(pageRocket.getTotalPages())
.totalSize((int) pageRocket.getTotalElements())
.build();
} catch (Exception e) {
log.error("Error when get list rocket: " + e.getMessage());
throw new RocketException(ERROR_MESSAGE_SYSTEM_ERROR, RC_CODE_SYSTEM_ERROR, ERROR_DESCRIPTION_SYSTEM_ERROR);
}
return ResponseUtil.buildHttpResponse(ResponseUtil.buildResponse(RC_CODE_SUCCESS, ERROR_MESSAGE_SUCCESS, response));
}
}Create class AsteroidService
@Slf4j
@Service
public class AsteroidService {
@Autowired
private AsteroidDataHelper asteroidDataHelper;
public ResponseEntity<ApiBaseResponse<AsteroidRes>> storeAsteroid(Metadata metadata, AsteroidReq req) {
log.info("Start store asteroid");
AsteroidRes response = null;
try {
AsteroidDao asteroidDao = AsteroidDao.builder()
.name(req.getName())
.minDiameterKm(req.getMinDiameterKm())
.maxDiameterKm(req.getMaxDiameterKm())
.isHazardous(req.isHazardous())
.build();
AsteroidDao asteroidResult = asteroidDataHelper.storeAsteroid(asteroidDao);
response = AsteroidRes.builder()
.asteroidName(asteroidResult.getName())
.minDiameterKm(asteroidResult.getMinDiameterKm())
.maxDiameterKm(asteroidResult.getMaxDiameterKm())
.isHazardous(asteroidResult.isHazardous())
.build();
} catch (Exception e) {
log.error("Error when store asteroid: " + e.getMessage());
throw new RocketException(ERROR_MESSAGE_SYSTEM_ERROR, RC_CODE_SYSTEM_ERROR, ERROR_DESCRIPTION_SYSTEM_ERROR);
}
return ResponseUtil.buildHttpResponse(ResponseUtil.buildResponse(RC_CODE_SUCCESS, ERROR_MESSAGE_SUCCESS, response));
}
public ResponseEntity<ApiBaseResponse<PageAsteroidRes>> getListAsteroid(Metadata metadata, int page, int size) {
log.info("Start get list asteroid");
PageAsteroidRes response = null;
try {
Page<AsteroidDao> pageAsteroid = asteroidDataHelper.getPageAsteroid(page, size);
List<AsteroidRes> listAsteroidRes = pageAsteroid.getContent().stream().map(ast -> AsteroidRes.builder()
.asteroidName(ast.getName())
.minDiameterKm(ast.getMinDiameterKm())
.maxDiameterKm(ast.getMaxDiameterKm())
.isHazardous(ast.isHazardous())
.build()).toList();
response = PageAsteroidRes.builder()
.list(listAsteroidRes)
.totalPage(pageAsteroid.getTotalPages())
.totalSize((int) pageAsteroid.getTotalElements())
.build();
} catch (Exception e) {
log.error("Error when get list asteroid: " + e.getMessage());
throw new RocketException(ERROR_MESSAGE_SYSTEM_ERROR, RC_CODE_SYSTEM_ERROR, ERROR_DESCRIPTION_SYSTEM_ERROR);
}
return ResponseUtil.buildHttpResponse(ResponseUtil.buildResponse(RC_CODE_SUCCESS, ERROR_MESSAGE_SUCCESS, response));
}
}Create class AstronoutService
@Slf4j
@Service
@RequiredArgsConstructor
public class AstronoutService {
@Autowired
private TriangleHelper triangleHelper;
public String displayResult(Metadata metadata, String type) {
log.info("Start display result astronout");
String result = "";
try {
if ("PASCAL".equalsIgnoreCase(type)) {
result = triangleHelper.getPascalTriangle(5);
} else {
result = triangleHelper.getNormalTriangle(5);
}
} catch (Exception e) {
log.error("Error when display result: " + e.getMessage());
throw new RocketException(ERROR_MESSAGE_SYSTEM_ERROR, RC_CODE_SYSTEM_ERROR, ERROR_DESCRIPTION_SYSTEM_ERROR);
}
return result;
}
}Controller
We will build simple API to store rocket and asteroid in relation many to many.
Create RocketController in /controller/
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/rocket")
public class RocketController {
@Autowired
private RocketService rocketService;
@PostMapping("store")
public ResponseEntity<ApiBaseResponse<RocketRes>> storeRocket(@RequestBody RocketReq req, HttpServletRequest servletRequest) {
Metadata metadata = MetadataUtil.constructMetadata(servletRequest);
return rocketService.storeRocket(metadata, req);
}
@GetMapping("list")
public ResponseEntity<ApiBaseResponse<PageRocketRes>> getListRocket(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
HttpServletRequest servletRequest) {
Metadata metadata = MetadataUtil.constructMetadata(servletRequest);
return rocketService.getListRocket(metadata, page, size);
}
}Create AsteroidController
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/asteroid")
public class AsteroidController {
@Autowired
private AsteroidService asteroidService;
@PostMapping("store")
public ResponseEntity<ApiBaseResponse<AsteroidRes>> storeAsteroid(@RequestBody AsteroidReq req, HttpServletRequest servletRequest) {
Metadata metadata = MetadataUtil.constructMetadata(servletRequest);
return asteroidService.storeAsteroid(metadata, req);
}
@GetMapping("list")
public ResponseEntity<ApiBaseResponse<PageAsteroidRes>> getListAsteroid(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
HttpServletRequest servletRequest) {
Metadata metadata = MetadataUtil.constructMetadata(servletRequest);
return asteroidService.getListAsteroid(metadata, page, size);
}
}Create AstronoutController
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/astronout")
public class AstronoutController {
@Autowired
private AstronoutService astronoutService;
@GetMapping("display")
public String displayResult(
@RequestParam(defaultValue = "NORMAL") String type,
HttpServletRequest servletRequest) {
Metadata metadata = MetadataUtil.constructMetadata(servletRequest);
return astronoutService.displayResult(metadata, type);
}
}Testing
Run application with command mvn spring-boo:run
Store Rocket
URL: http://localhost:8005/spaces/v1/rocket/store
Method: POST
Request Body:
{
"name": "SpaceX",
"fuelTank": 2000,
"astronoutCap": 10,
"listAsteroid": [
{
"name": "Astra1",
"minDiameterKm": 10000.6,
"maxDiameterKm": 132141243.3,
"isHazardous": true
},
{
"name": "Astra2",
"minDiameterKm": 112111.6,
"maxDiameterKm": 213212.3,
"isHazardous": true
}
]
}Response:
{
"traceId": "68d65ddc7b736e0b53c8f9644a8a4822",
"data": {
"rocketName": "SpaceX",
"rocketFuelTank": 2000,
"astronoutCap": 10,
"listAsteroid": [
{
"id": 1,
"name": "Astra1",
"minDiameterKm": 10000.6,
"maxDiameterKm": 132141243.3,
"rockets": null,
"hazardous": false
},
{
"id": 2,
"name": "Astra2",
"minDiameterKm": 112111.6,
"maxDiameterKm": 213212.3,
"rockets": null,
"hazardous": false
}
]
},
"response_code": "00",
"response_message": "Success"
}Unit Test
Notes from here that since Spring Boot 3.4.0+, the @MockBean and @SpyBean are deprecated. Now we use @MockitoBean and @MockitoSpyBean.
And since Spring Boot 2.2+ uses JUnit 5 by default when you include spring-boot-starter-test.
That's it. You can find source code here: https://github.com/pandz3rd/rocket-service