2

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);


}



1 Answer 1

2

Single JVM

If you have just one JVM running the Service, then you can use the JVM to manage shared state:

  1. Upon startup perform a max aggregate Mongo query to find highest number, store in an AtomicLong typed variable
  2. Each time you want the next id use the threadsafe feature of AtomicLong to obtain the next id

State in the DB

If you have more than one JVM then you no option but to use some other mechanism to managed shared state. The DB is probably your best option, you could:

  1. Have a collection with one document that maintains the next productId and use the following atomic operation to increment the id and return the value:
db.getCollection('someCollection').findAndModify(
{
     query: { _id: "someKey" },
     update: { $inc: { productId: 1 } },
     upsert: true
   }
)
  1. That returns 100,101,102, etc on subsequent calls, use that value in your ProductService.createProduct()

In both cases

  • for safety add a Unique index on your productId field
  • you may lose a number if there is an exception
Sign up to request clarification or add additional context in comments.

2 Comments

By "lose a number", do you mean the sequence might go `101, 102, 104, 105" ? If so, an unused ID value isn't a big problem.
Yup, that's what I meant.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.