Exploring Integration of MinIO, MySQL, Jasper, and GraphQL with Spring Boot — Part 1
In modern application development, flexibility and efficiency are two key aspects that developers often look for. One technology that makes this possible is GraphQL. Unlike REST, GraphQL offers a more dynamic approach to interacting with APIs. Let’s see why GraphQL can be a more flexible option compared to REST, as well as the advantages of using Spring Boot in developing applications.
Why is GraphQL More Flexible Than REST?
GraphQL allows clients to request only the data they need, no more and no less. Thus, overhead is reduced, and application performance is increased. In addition, GraphQL type schemas can be customized easily without disrupting existing clients. You only need one endpoint for different types of queries and mutations, which makes API management and documentation easy. In modern development, the flexibility offered by GraphQL is increasingly in demand due to its ability to cope with the complexity of data retrieval.
No need to linger, let’s create a simple project with springboot graphql fruit-commerce case study. First let’s understand the db schema before init project:

There are 3 simple tables, later I will practice a simple transaction from this project using mutations from graphql.
- Initiation
To start, we will initiate the Spring Boot project using Spring Initializr or a similar tool. We will add the necessary dependencies for MinIO, MySQL, Jasper, GraphQL, Lombok, Spring Data JPA, and Spring Boot Starter Web.
2. Create Entities According to the Database Schema
After setting up the connection configuration to the MySQL database, the next step is to create entities according to the database schema. Make sure the datasource configuration is set correctly in the application.properties file:
# Datasource Configuration
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/xxx
spring.datasource.username=xxx
spring.datasource.password=xxx
Create a package called entity in your project to store all entity classes. Create an Items Class, Here is an example of an Items class that you can use. This entity will represent the items table in your database.
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class Items {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(unique = true, nullable = false, name = "item_id")
private int itemId;
private String itemName;
private String itemCode;
private String itemDescription;
private Long stock;
private Long price;
private Boolean isAvailable;
private LocalDateTime lastRestock;
@OneToMany(mappedBy = "items", orphanRemoval = true, cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private List<Orders> orders;
public Items(String itemName, String itemDescription, Long stock, Long price) {
this.itemName = itemName;
this.itemCode = generateItemCode();
this.itemDescription = itemDescription;
this.stock = stock;
this.price = price;
this.isAvailable = itemAvailable(stock);
}
public void setStock(Long stock) {
this.stock = stock;
this.isAvailable = stock > 0;
this.lastRestock = LocalDateTime.now();
}
public void updateStock(Long stock) {
this.stock = this.stock - stock;
this.lastRestock = LocalDateTime.now();
if (this.stock == 0) {
this.isAvailable = itemAvailable(this.stock);
}
}
private Boolean itemAvailable(Long Stock){
this.stock = Stock;
this.isAvailable = stock > 0;
return isAvailable;
}
private String generateItemCode() {
UUID uuid = UUID.randomUUID();
return uuid.toString().substring(0, 5);
}
}
- setStock(): Sets the stock quantity and updates the availability status and the last restock time.
- updateStock(): Decreases the stock and updates the last restock time.
- itemAvailable(): Checks item availability based on stock.
- generateItemCode(): Generates a unique item code using the UUID.
// Example on ORDER entity
@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "item_id", nullable = false)
@JsonProperty("itemName")
private Items items;
The relationship between table item and table Order is one to many. In best practice, there should be no need to use cascade All because if you delete one item that references an order then both data in the table can be deleted.
3. Creating Repository for Communication with Database
Repository is an important component in Spring Data JPA that provides an interface for CRUD (Create, Read, Update, Delete) operations on entities. Here is an example of a repository implementation for the Items entity.
@Repository
public interface ItemRepository extends JpaRepository<Items, Long> {
// Search by item name only
Page<Items> findByItemNameContainingIgnoreCase(String itemName, Pageable pageable);
// Search by availability only
Page<Items> findByIsAvailable(Boolean isAvailable, Pageable pageable);
// Search by item name and availability
Page<Items> findByItemNameContainingIgnoreCaseAndIsAvailable(String itemName, Boolean isAvailable, Pageable pageable);
}
Annotation @Repository: Indicates that this interface is a Spring repository. It will be scanned by Spring and integrated into the application context. Extend JpaRepository: JpaRepository<Items, Long>: Provides built-in methods for CRUD operations and complex queries. Items is the entity type and Long is the data type of the primary key (itemId).
Custom Query Methods:
- findByItemNameContainingIgnoreCase(String itemName, Pageable pageable): Returns the page (Page<Items>) of the item whose name contains the given text, regardless of case. The Pageable parameter is used for pagination of query results.
- findByIsAvailable(Boolean isAvailable, Pageable pageable): Returns the page of the item based on the availability status.
- findByItemNameContainingIgnoreCaseAndIsAvailable(String itemName, Boolean isAvailable, Pageable pageable): Returns a page of items whose names contain the given text and have the given availability status.
4. Creating a Service with Business Logic
A service is a place to store business logic in a Spring Boot application. The following is an explanation of the methods in the ItemService that implement the business logic for the Items entity.
@Service
public class ItemService {
private final ItemRepository itemRepository;
@Autowired
public ItemService(ItemRepository itemRepository) {
this.itemRepository = itemRepository;
}
public ResponGetAllData<Items> getAllData(String itemName, Boolean isAvailable, int page, int size){
Pageable pageable = PageRequest.of(page - 1, size);
Page<Items> itemsPage;
if (itemName != null && isAvailable != null) {
itemsPage = itemRepository.findByItemNameContainingIgnoreCaseAndIsAvailable(itemName, isAvailable, pageable);
} else if (itemName != null) {
itemsPage = itemRepository.findByItemNameContainingIgnoreCase(itemName, pageable);
} else if (isAvailable != null) {
itemsPage = itemRepository.findByIsAvailable(isAvailable, pageable);
} else {
itemsPage = itemRepository.findAll(pageable); // Fallback to all items
}
ResponHeader header = ResponHeaderMessage.getRequestSuccess();
return new ResponGetAllData<>(header, itemsPage.getContent());
}
public ResponGetData getItem(String itemId) {
Long id = Long.parseLong(itemId);
Optional<Items> existingItem = itemRepository.findById(id);
if (existingItem.isPresent()) {
Items item = existingItem.get();
ResponHeader header = ResponHeaderMessage.getRequestSuccess();
return new ResponGetData(header, item);
}
ResponHeader header = ResponHeaderMessage.getDataNotFound();
return new ResponGetData(header, null);
}
@Transactional
public ResponHeader createItem(RequestItemCreateUpdate request) {
ResponHeader header;
try {
Items newItem = new Items(
request.getItemName(),
request.getItemDescription(),
request.getStock() != null ? request.getStock() : 0,
request.getPrice() != null ? request.getPrice() : 0
);
itemRepository.save(newItem);
header = ResponHeaderMessage.getRequestSuccess();
header.setMessage("Success Create Item");
} catch (Exception e) {
header = ResponHeaderMessage.getBadRequestError();
header.setMessage(e.getMessage());
}
return header;
}
@Transactional
public ResponHeader updateItem(String itemId ,RequestItemCreateUpdate request) {
Long id = Long.parseLong(itemId);
Optional<Items> existingItem = itemRepository.findById(id);
if (existingItem.isPresent()) {
Items item = existingItem.get();
if (request.getItemName() != null) {
item.setItemName(request.getItemName());
}
if (request.getStock() != null) {
item.setStock(request.getStock());
item.setLastRestock(LocalDateTime.now());
}
if (request.getPrice() != null) {
item.setPrice(request.getPrice());
}
if (request.getItemDescription() != null) {
item.setItemDescription(request.getItemDescription());
}
itemRepository.save(item);
ResponHeader header = ResponHeaderMessage.getRequestSuccess();
header.setMessage("Success Update Item");
return header;
}
return ResponHeaderMessage.getBadRequestError();
}
@Transactional
public ResponHeader deleteItem(String itemId) {
Long id = Long.parseLong(itemId);
Optional<Items> existingItem = itemRepository.findById(id);
if (existingItem.isPresent()) {
itemRepository.delete(existingItem.get());
return ResponHeaderMessage.getRequestSuccess();
}
return ResponHeaderMessage.getBadRequestError();
}
}
- Method getAllData: This method retrieves data on all items by name and/or availability, with pagination support.
- Method getItem: Converts itemId to Long type. Searches for items by ID using the repository. Returns the item data if found, or an error message if not.
- CreateItem method: Creates a new Items object from the data provided in the request. Saves the new item to the database using the repository.
Handles exceptions if an error occurs during storage. Returns an appropriate response based on the result of the operation. - UpdateItem method: Converts itemId to Long type. Searches for the item by ID using the repository. If the item is found, updates the properties given in the request. Saves the changes to the database using the repository. Returns the appropriate response based on the result of the operation.
- DeleteItem method: Converts itemId to Long type. Searches for the item based on the ID using the repository. If the item is found, removes it from the database using the repository. Returns the appropriate response based on the result of the operation.
5. DTO and Response Classes
In this project, you have used several Data Transfer Object (DTO) and response classes to manage the input and output of the service. Here is a description of each class and their roles:
DTO: RequestItemCreateUpdate
This DTO is used to receive input data when creating or updating items.
@AllArgsConstructor
@Getter
@Setter
public class RequestItemCreateUpdate {
private String itemName;
private String itemDescription;
private Long price;
private Long stock;
}
Response Classes
- ResponseHeader:
Serves as a header in each response that provides general information about the status of the response.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ResponGetAllData<T> {
private ResponHeader header;
private List<T> data;
}
- ResponseGetData:
Used to return a response that contains one entity (single data)
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ResponGetData<T> {
private ResponHeader header;
private T data;
}
6. Creating a Controller that Integrates Client Responses with GraphQL
This controller is responsible for handling requests from the client and directing them to the appropriate service. In the context of Spring Boot with GraphQL, this controller also serves as a resolver for GraphQL queries and mutations.
@Controller
public class ItemController {
private final ItemService itemService;
@Autowired
public ItemController(ItemService itemService) {
this.itemService = itemService;
}
@QueryMapping
public ResponGetAllData<?> getAllItem(@Argument String itemName, @Argument Boolean isAvailable,@Argument int page,@Argument int size) {
return itemService.getAllData(itemName, isAvailable, page, size);
}
@QueryMapping
public ResponGetData<?> getItem(@Argument String itemId) {
return itemService.getItem(itemId);
}
@MutationMapping
public ResponHeader createItem(@Argument("createItem") RequestItemCreateUpdate requestItemCreateUpdate) {
return itemService.createItem(requestItemCreateUpdate);
}
@MutationMapping
public ResponHeader updateItem(@Argument String id,@Argument("updateItem") RequestItemCreateUpdate requestItemCreateUpdate) {
return itemService.updateItem(id ,requestItemCreateUpdate);
}
@MutationMapping
public ResponHeader deleteItem(@Argument String id) {
return itemService.deleteItem(id);
}
}
Query Mapping and Mutation
- Query Mapping (@QueryMapping):
Used to define a GraphQL query that will retrieve data without modifying it. This annotated method will be called when the client executes the corresponding GraphQL query.
Example: getAllItem and getItem.
- Mutation (@MutationMapping):
Used to define GraphQL mutations that will modify data.
This annotated method will be called when the client executes the corresponding GraphQL mutation. Example: createItem, updateItem, and deleteItem.
Controller as Resolver
This controller acts as a GraphQL resolver, which means it is responsible for mapping GraphQL queries and mutations to corresponding Java methods. The resolver parses the GraphQL request and calls the relevant business logic in the service, then returns the results in the format specified by the GraphQL schema.
I am very excited to write this article and share my knowledge with you all. Learning and technological developments are always interesting to follow. Given the length of the topic, I decided to split this article into two parts.
In this first part, we have covered the basics and initial steps in building a Spring Boot project with MySQL, and GraphQL integration. In the second part, we will continue with a more in-depth discussion and complete this project to completion Minio, Jasper, and Graphql.
Thank you for following along until the end of this first part. Keep up the good work and keep growing. See you in part two!