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

Reference

Leave a Reply