I have a Spring Boot microservice that provides non-blocking APIs for creating and managing entities called Product. The services uses a MongoDB. For design purposes, each Product needs to have a numeric productId value in addition to the primary key field id that is automatically assigned when a new Product is created.
The problem is that I cannot think of a way to set the productId of the new Product entity that doesn’t use a blocking database call that counts the number of existing Product entities in the database and then increments that number. Ideally, I’d like an auto-increment feature that will do this on the entity itself when it is persisted to the database. Any help or suggestions are appreciated
Product
package com.bh25034.products.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Document(collection = "products")
@ToString(callSuper = true)
public class Product {
@Id
private String id;
@Indexed(unique = true)
//TODO: this is where I’d like to have something like a GeneratedValue or auto-increment feature that’s safe from race conditions
private Long productId;
private String name;
private String description;
}
ProductService
package com.bh25034.products.service.impl;
import com.bh25034.products.configuration.AppConfiguration;
import com.bh25034.products.event.EventType;
import com.bh25034.products.event.ProductEvent;
import com.bh25034.products.exception.NotFoundException;
import com.bh25034.products.mapping.ProductMapper;
import com.bh25034.products.messaging.ProductEventMessageProducer;
import com.bh25034.products.model.Product;
import com.bh25034.products.model.dto.ProductDto;
import com.bh25034.products.repository.ProductRepository;
import com.bh25034.products.service.ProductService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Date;
import static java.lang.String.format;
@RequiredArgsConstructor
@Slf4j
@Service
public class ProductService {
private final ProductRepository productRepository;
private final ProductMapper productMapper;
public Mono<ProductDto> getProduct(final String id) {
return productRepository.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException(format("Could not find product %s", id))))
.map(productMapper::toProductDto);
}
public Mono<ProductDto> getProductByProductId(final long productId) {
return productRepository.findByProductId(productId)
.switchIfEmpty(Mono.error(new NotFoundException(format("Could not find product with productId: %s", productId))))
.map(productMapper::toProductDto);
}
public Flux<ProductDto> getAllProducts() {
return productRepository.findAll().map(productMapper::toProductDto);
}
public Mono<ProductDto> createProduct(final ProductDto productDto) {
Product product = productMapper.toProduct(productDto);
//TODO: thought about setting the productId here but it could lead to duplicate key exceptions if another product is persisted between this line and the following save() call to the repository
return productRepository.save(product).map(savedProduct -> {
ProductDto savedProductDto = productMapper.toProductDto(savedProduct);
return savedProductDto;
});
}
public Mono<Void> deleteProduct(final String id) {
return productRepository.deleteById(id);
}
public Mono<Void> deleteProductByProductId(final long productId) {
return productRepository.findByProductId(productId)
.map(productRepository::delete)
.flatMap(voidMono -> voidMono);
}
}
ProductDto
package com.bh25034.products.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class ProductDto {
private String id;
private long productId;
private String name;
private String description;
}
ProductMapper
package com.bh25034.products.mapping;
import com.bh25034.products.model.Product;
import com.bh25034.products.model.dto.ProductDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
@Mapper(componentModel = "spring")
public interface ProductMapper {
ProductDto toProductDto(Product product);
@Mappings({
@Mapping(target = "id", ignore = true)
})
Product toProduct(ProductDto productDto);
}
ProductRepository
package com.bh25034.products.repository;
import com.bh25034.products.model.Product;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
@Repository
public interface ProductRepository extends ReactiveMongoRepository<Product, String> {
Mono<Product> findByProductId(long productId);
}