Pages

Tuesday, March 31, 2020

Auto-Generated Field for MongoDB using Spring Boot - Step By Step

Outline:

In this tutorial, we are going to learn how to implement a sequential and auto-generated field for MongoDB in Spring Boot. If you are new to MongoDB and Spring then visit  Spring Data MongoDB Tutorial.

Understanding step by step today about "Auto-Generated Field for MongoDB using Spring Boot"

Spring Boot Tutorials

[update] Updated new errors in this article.
This is a very common scenario that arises when using MongoDB and Spring framework integration work.

When we are using MongoDB databases as part of the Spring Boot application directly we can not use @GeneratedValue annotation in our database model classes.

@GeneratedValue: Marking a field with the @GeneratedValue annotation specifies that value will be automatically generated for that field and mainly this is used on primary key fields. This annotation is available only for JPA API.



Auto-Generated Field for MongoDB using Spring Boot


The easy solution to this problem is very simple. We will create a collection or table that will store the generated sequence for other collections. During the creation of a new record, we’ll use it to fetch the next value.

Maven Dependencies for spring boot and mongodb:

We should add the required dependencies in the pom.xml file as below. Get the latest version from here.


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <versionId>2.1.0.RELEASE</versionId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
        <versionId>2.1.0.RELEASE</versionId>
    </dependency>
</dependencies>

Using Collections:

1) First, create a collection that will store the auto-incremented value in it. This can be created using either the mongo shell or MongoDB Compass


import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "database_sequences")
public class DatabaseSequence {

    @Id
    private String id;

    private long seq;

    public DatabaseSequence() {}

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public long getSeq() {
        return seq;
    }

    public void setSeq(long seq) {
        this.seq = seq;
    }
}

2. Let’s then create a users_db collection. This collection stores the users that are being used.


import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "users_db")
public class User {

    @Transient
    public static final String SEQUENCE_NAME = "users_sequence";

    @Id
    private long id;

    private String firstName;

    private String lastName;

    private String email;

    public User() { }

    public User(String firstName, String lastName, String email) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
    
    @Override
    public String toString() {
        return "User{" + "id=" + id + ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + '}';
    }
}

Here SEQUENCE_NAME is declared as constant and annotated with @Transient to prevent it from serializing to persist in the table.



Creating a New Record - Auto-Generated for MongoDB:

1) Creating a UserRepository.

import com.java-w3schools.mongodb.models.User;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface UserRepository extends MongoRepository {

}

2) Creating Service


@Service
public class SequenceGeneratorService {


    private MongoOperations mongoOperations;

    @Autowired
    public SequenceGeneratorService(MongoOperations mongoOperations) {
        this.mongoOperations = mongoOperations;
    }

    public long generateSequence(String seqName) {

        DatabaseSequence counter = mongoOperations.findAndModify(query(where("_id").is(seqName)),
                new Update().inc("seq",1), options().returnNew(true).upsert(true),
                DatabaseSequence.class);
        return !Objects.isNull(counter) ? counter.getSeq() : 1;

    }
}

3) Creating Listener


@Component
public class UserModelListener extends AbstractMongoEventListener {

    private SequenceGeneratorService sequenceGeneratorService;

    @Autowired
    public UserModelListener(SequenceGeneratorService sequenceGeneratorService) {
        this.sequenceGeneratorService = sequenceGeneratorService;
    }

    @Override
    public void onBeforeConvert(BeforeConvertEvent event) {
        if (event.getSource().getId() < 1) {
            event.getSource().setId(sequenceGeneratorService.generateSequence(User.SEQUENCE_NAME));
        }
    }
}

4) Calling generateSequence




User user = new User();
user.setId(sequenceGeneratorService.generateSequence(User.SEQUENCE_NAME));
user.setEmail("java-w3schools@example.com");
userRepository.save(user);

Here a unique id will be generated in sequence when we invoke generateSequence() method each time.

TroubleShooting

Problem:

org.springframework.dao.InvalidDataAccessApiUsageException: Cannot autogenerate id of type java.lang.Long for entity of type ua.home.springdata.investigation.entity.Account!
    at org.springframework.data.mongodb.core.MongoTemplate.assertUpdateableIdIfNotSet(MongoTemplate.java:1149)
    at org.springframework.data.mongodb.core.MongoTemplate.doSave(MongoTemplate.java:878)
    at org.springframework.data.mongodb.core.MongoTemplate.save(MongoTemplate.java:833)
    at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.save(SimpleMongoRepository.java:73)
    at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.save(SimpleMongoRepository.java:88)
    at org.springframework.data.mongodb.repository.support.SimpleMongoRepository.save(SimpleMongoRepository.java:45)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:442)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:427)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:381)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:207)
    at com.sun.proxy.$Proxy26.save(Unknown Source)

Suggestion 1:


Mongo ObjectIds don't map to a java Long type.

I see this in the documentation, under 7.6.1:


An id property or field declared as a String in the Java class will be converted to and stored as an ObjectId if possible using a Spring Converter. Valid conversion rules are delegated to the MongoDB Java driver. If it cannot be converted to an ObjectId, then the value will be stored as a string in the database.

An id property or field declared as BigInteger in the Java class will be converted to and stored as an ObjectId using a Spring Converter.
So change id to a String or a BigInteger and remove the strategy argument.

Suggestion 2:


Using @Id as a String works fine.

Make sure that your Repository extends with a String (same type as the @Id):

extends MongoRepository<MyEntity, String>

Suggestion 3:

Add maven dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

Source:

import org.springframework.data.annotation.Id; 
import org.springframework.data.mongodb.core.mapping.Document;

@Document(collection = "ACCOUNTS")
public class Account {

    @Id
    private String id;

    ....rest of properties
}

import org.springframework.data.mongodb.repository.MongoRepository;
public interface AccountRepository extends MongoRepository<Account, String>  {
    //any extra queries needed
}

Conclusion:

We have seen today how to generate sequential auto-incremented values for the id field and simulate the same behavior as seen as in SQL databases or using JPA API.

No comments:

Post a Comment

Please do not add any spam links in the comments section.