Tutorial OCT 06, 2025 • 05 MIN READ

RELATION MANY TO MANY
WITH JPA
IN SPRING BOOT

Abstract technical background

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.

POM.XML JAVA
<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:

Initial table

Configure Properties

Set profile active in application.properties.

spring.profiles.active=dev

Create new file application-dev.properties inside resources folder.

application-dev.properties JAVA
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=./log

User Entity

Create java class AsteroidDao at /model/db/

AsteroidDao.JAVA JAVA
@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/

AsteroidRepository.JAVA JAVA
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.

RocketRepository.JAVA JAVA
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

AsteroidReq.JAVA JAVA
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AsteroidReq {
    private String name;
    private double minDiameterKm;
    private double maxDiameterKm;
    private boolean isHazardous;
}

Create class AsteroidRes

AsteroidRes.JAVA JAVA
@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

PageAsteroidRes.JAVA JAVA
@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/

AstronoutRes.JAVA JAVA
@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/

AstronoutRes.JAVA JAVA
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RocketReq {
    private String name;
    private int fuelTank;
    private int astronoutCap;
    private List<AsteroidReq> listAsteroid;
}

Create class RocketRes

RocketRes.JAVA JAVA
@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

PageRocketRes.JAVA JAVA
@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/

RocketDataHelper.JAVA JAVA
@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

AsteroidDataHelper.JAVA JAVA
@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

TriangleHelper.JAVA JAVA
@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/

RocketService.JAVA JAVA
@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

AsteroidService.JAVA JAVA
@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

AstronoutService.JAVA JAVA
@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/

RocketController.JAVA JAVA
@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

AsteroidController.JAVA JAVA
@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

AstronoutController.JAVA JAVA
@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:

Request.JSON JSON
{
  "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:

Response.JSON JSON
{
  "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

READ BEYOND THE VOID

Technical blueprint background
Backend Notes

Find the first repeated character in a given string

When I interview junior developers, I love throwing small-but-smart problems at them...

Read Entry
Abstract digital network
Backend Notes

Authentication service with JWT

I never had a chance handle authentication service, so I think I need to learn to authenticating user....

Read Entry
Circuit board macro
Backend Notes

Migrate from RestTemplate to RestClient Spring Boot

Saying Goodbye to RestTemplate — and Why RestClient Deserves Your Attention...

Read Entry