In this Spring Boot tutorial, you will learn how to create a spring boot API connected with Mongo DB using spring data MongoDB.
Initialize a project
Make a project using Spring Initializr at https://start.spring.io
Our project later on will depend on springfox that not available yet on Spring Boot v3. We use Spring Boot v2 instead.
Click generate, Spring Initializr will generate a zip file that contain your project. Extract it and import to your IDE (my IDE = Eclipse) as an existing maven project:
Your initial project look like below:
Do update maven project
Make database in MongoDB
Connect to your MongoDB, you can use MongoDB Compass.
Create new database valutory
with collection name materialtransaction
:
Configure project to connect to MongoDB
We use database name inventoryvaluation
, add config key to src/main/resources/application.properties
:
spring.data.mongodb.uri=mongodb://localhost:27017/valutory
Define entity class
package org.wirabumi.valutory;
import java.time.LocalDateTime;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@Document(collection = "materialtransaction")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MaterialTransaction {
@Id
private String id;
private String productId;
private Double movementQuantity;
private LocalDateTime movementDate;
private MovementType movementType;
private Double cost;
private CostingStatus costingStatus;
public enum MovementType {
CUSTOMER_SHIPMENT,
VENDOR_RECEIPT
}
public enum CostingStatus {
NOT_CALCULATED,
CALCULATED
}
}
Define repository
package org.wirabumi.valutory;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface MaterialTransactionRepository extends MongoRepository<MaterialTransaction, String> {
}
Define service
Add new dependency:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
Define service:
package org.wirabumi.valutory;
import java.util.List;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MaterialTransactionService {
@Autowired
private MaterialTransactionRepository repo;
public MaterialTransaction save(MaterialTransaction record) {
if (StringUtils.isEmpty(record.getId()))
record.setId(UUID.randomUUID().toString());
return repo.save(record);
}
public List<MaterialTransaction> findAll(){
return repo.findAll();
}
}
Define REST controller
package org.wirabumi.valutory;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/materialtransaction")
public class MaterialTransactionController {
@Autowired
private MaterialTransactionService service;
@PostMapping
public ResponseEntity<MaterialTransaction> save(@RequestBody MaterialTransaction record) {
return ResponseEntity.ok().body(service.save(record));
}
@GetMapping
public ResponseEntity<List<MaterialTransaction>> findAll(){
return ResponseEntity.ok().body(service.findAll());
}
}
Add swagger documentation page
Add new dependency:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
Enable swagger on Spring boot application main class:
package org.wirabumi.valutory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@SpringBootApplication
@EnableSwagger2
public class ValutoryApplication {
public static void main(String[] args) {
SpringApplication.run(ValutoryApplication.class, args);
}
}
Add matching strategy config key:
spring.mvc.pathmatch.matching-strategy=ANT_PATH_MATCHER
Define configuration class:
package org.wirabumi.valutory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class SpringFoxConfig {
@Bean
Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
}
Run the service
To run your application, we have 2 alternatives:
alternative 1: using eclipse, run as spring boot app
When you see log message “Tomcat started”, meaning your service run successfully.
alternative 2: using command line
run command: mvn spring-boot:run
Call API using swagger
go to http://localhost:8080/swagger-ui/
make sure URL for this swagger include slash “/” at the end, other wise swagger will not open.
try to post a material transaction with this payload:
{
"cost": 0,
"costingStatus": "NOT_CALCULATED",
"id": "60f0f858-b4b2-4dee-9f28-e569e69e3000",
"movementDate": "2023-07-02T10:28:57.265Z",
"movementQuantity": 10,
"movementType": "CUSTOMER_SHIPMENT",
"productId": "ABC"
}
you will get response similar to:
{
"id": "60f0f858-b4b2-4dee-9f28-e569e69e3000",
"productId": "ABC",
"movementQuantity": 10,
"movementDate": "2023-07-02T10:28:57.265",
"movementType": "CUSTOMER_SHIPMENT",
"cost": 0,
"costingStatus": "NOT_CALCULATED"
}
add you can see it on MongoDB also:
Update already implemented
If you post a Material Transaction with existing ID, you will update that record. For example, use payload below (the ID equal to previous post), with updated movementQuantity:
{
"cost": 0,
"costingStatus": "NOT_CALCULATED",
"id": "60f0f858-b4b2-4dee-9f28-e569e69e3000",
"movementDate": "2023-07-02T10:28:57.265Z",
"movementQuantity": 20,
"movementType": "CUSTOMER_SHIPMENT",
"productId": "ABC"
}
you will get response that movement quantity updated to 20, and MongoDB still save 1 record only:
Implement delete API
Add delete method on MaterialTransactionService
class:
package org.wirabumi.valutory;
import java.util.List;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MaterialTransactionService {
@Autowired
MaterialTransactionRepository repo;
public MaterialTransaction save(MaterialTransaction record) {
if (StringUtils.isEmpty(record.getId()))
record.setId(UUID.randomUUID().toString());
return repo.save(record);
}
public List<MaterialTransaction> findAll(){
return repo.findAll();
}
public void delete(String id) {
repo.deleteById(id);
}
}
Add endpoint on controller class:
package org.wirabumi.valutory;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/materialtransaction")
public class MaterialTransactionController {
@Autowired
private MaterialTransactionService service;
@PostMapping
public ResponseEntity<MaterialTransaction> save(@RequestBody MaterialTransaction record) {
return ResponseEntity.ok().body(service.save(record));
}
@GetMapping
public ResponseEntity<List<MaterialTransaction>> findAll(){
return ResponseEntity.ok().body(service.findAll());
}
@DeleteMapping(value = "/{id}")
public ResponseEntity<?> delete(@PathVariable String id) {
service.delete(id);
return ResponseEntity.noContent().build();
}
}
Test run
Call delete API using id 60f0f858-b4b2-4dee-9f28-e569e69e3000
Now you can see no record anymore on MongoDB.
Implement a filter
We will implement query API that filter result based on movement quantity. For example, find material transaction that movement quantity between 0 and 10.
Add method below to MaterialTransactionRepository
interface:
public List<MaterialTransaction> findByMovementQuantity(Double minMovementQuantity, Double maxMovementQuantity) {
return repo.findByMovementQuantityBetween(minMovementQuantity, maxMovementQuantity);
}
Add method for query API on service class MaterialTransactionService
:
public List<MaterialTransaction> findByMovementQuantity(Double minMovementQuantity, Double maxMovementQuantity) {
return repo.findByMovementQuantityBetween(minMovementQuantity, maxMovementQuantity);
}
Add query API endpoint to controller class:
@GetMapping("/movement-quantity")
public ResponseEntity<List<MaterialTransaction>> findByMovementQuantity(
@RequestParam Double minMovementQuantity,
@RequestParam Double maxMovementQuantity){
return ResponseEntity.ok().body(service.findByMovementQuantity(minMovementQuantity, maxMovementQuantity));
}
Restart valutory service, and you will see swagger updated:
To test, add new data:
{
"productId": "ABC",
"movementQuantity": 10,
"movementDate": "2023-07-02T10:28:57.265",
"movementType": "CUSTOMER_SHIPMENT",
"cost": 0,
"costingStatus": "NOT_CALCULATED"
}
{
"productId": "ABC",
"movementQuantity": 100,
"movementDate": "2023-07-01T10:28:57.265",
"movementType": "VENDOR_RECEIPT",
"cost": 0,
"costingStatus": "NOT_CALCULATED"
}
{
"productId": "ABC",
"movementQuantity": 15,
"movementDate": "2023-07-02T10:29:57.265",
"movementType": "CUSTOMER_SHIPMENT",
"cost": 0,
"costingStatus": "NOT_CALCULATED"
}
Now try call query API, you get only 1 record as expected:
Implement pagination with more complex filter
On service class MaterialTransactionService
, add new dependency:
@Autowired
private MongoTemplate template;
Still on service class MaterialTransactionService
, add new method that implement search:
public Page<MaterialTransaction> search(
String productId,
Double minMovementQuantity,
Double maxMovementQuantity,
CostingStatus costingStatus,
Pageable pageable) {
List<Criteria> where = new ArrayList<>();
if (StringUtils.isNotEmpty(productId))
where.add(Criteria.where("productId").is(productId));
if (minMovementQuantity!=null)
where.add(Criteria.where("movementQuantity").gt(minMovementQuantity));
if (maxMovementQuantity!=null)
where.add(Criteria.where("movementQuantity").lt(maxMovementQuantity));
if (costingStatus!=null)
where.add(Criteria.where("costingStatus").is(costingStatus));
Query query = new Query().with(pageable);
if (!where.isEmpty())
query.addCriteria(new Criteria().andOperator(where));
Page<MaterialTransaction> result = PageableExecutionUtils.getPage(
template.find(query, MaterialTransaction.class),
pageable,
() -> template.count(query.skip(0).limit(0), MaterialTransaction.class));
return result;
}
On the controller class MaterialTransactionController
, add endpoint for search API:
@GetMapping("/search")
public Page<MaterialTransaction> search(
@RequestParam(required = false) String productId,
@RequestParam(required = false) Double minMovementQuantity,
@RequestParam(required = false) Double maxMovementQuantity,
@RequestParam(required = false) CostingStatus costingStatus,
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "5") Integer size){
Pageable pageable = PageRequest.of(page, size);
return service.search(productId, minMovementQuantity, maxMovementQuantity, costingStatus, pageable);
}
Try new api using swagger:
the result:
{
"content": [
{
"id": "1bf865ee-ed87-4c6e-892d-a8c5c36b4140",
"productId": "ABC",
"movementQuantity": 10,
"movementDate": "2023-07-02T10:28:57.265",
"movementType": "CUSTOMER_SHIPMENT",
"cost": 0,
"costingStatus": "NOT_CALCULATED"
},
{
"id": "b663fcb4-fa00-45c6-9a04-72faeaa06e4f",
"productId": "ABC",
"movementQuantity": 15,
"movementDate": "2023-07-02T10:29:57.265",
"movementType": "CUSTOMER_SHIPMENT",
"cost": 0,
"costingStatus": "NOT_CALCULATED"
}
],
"pageable": {
"sort": {
"empty": true,
"sorted": false,
"unsorted": true
},
"offset": 0,
"pageNumber": 0,
"pageSize": 5,
"paged": true,
"unpaged": false
},
"last": true,
"totalPages": 1,
"totalElements": 2,
"size": 5,
"number": 0,
"sort": {
"empty": true,
"sorted": false,
"unsorted": true
},
"first": true,
"numberOfElements": 2,
"empty": false
}
What’s Next
Now CRUD operation implemented, and it’s documentation generated by Swagger. Also, you learned about filter and pagination, see how flexible both MongoRepository
and MongoTemplate
. In next post we will learn how to organize this code better, using layering that adopted from Domain-Driven design concept. We will learn a separate concern between classes that compose a service module, including repository, entity, value object, service, and aggregate. Also, how service module wrapped by REST controller. This layering will express the design into code → make it more readable → increase developer productivity + make the product more modifiable → increase maintainability. Increased maintainability is our ultimate goal.
Coding is easy – make it maintainable is hard.
Wirabumi Software