Persistence top

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE

1. Overview

Using Spring Data MongoDB, we can create a MongoClient to do operations against the database. However, sometimes, we might need to use multiple databases in our applications.

In this tutorial, we'll create multiple connections to MongoDB. We'll also add some Spring Boot tests to mock this scenario.

2. Multiple Databases Application With Spring Data MongoDB

When using MongoDB, we create a MongoTemplate to access data. So, we could create multiple templates to connect to various databases.

However, we can get the NoUniqueBeanDefinitionException because Spring doesn't find a unique bean.

With this in mind, let's see how we can build a Spring Boot configuration.

2.1. Dependency Setup

Let's start by adding dependencies to the pom.xml. First, we need a spring boot starter:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.4</version>
    <relativePath />
</parent>

Then, we need dependencies for a web starter and data MongoDB:

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

Similarly, if we use Gradle, we add to the build.gradle:

plugins {
    id 'org.springframework.boot' version '2.6.4'
}
compile 'org.springframework.boot:spring-boot-starter-data-mongodb'
compile 'org.springframework.boot:spring-boot-starter-web'

As an alternative, we can use Spring Initializer.

2.2. Model

First, let's add our model. We'll create two documents that will be in use by two different databases.

For example, we'll create a User document:

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

    @MongoId
    private ObjectId id;

    private String name;

    private String surname;
    private String email;

    private int age;

    // getters and setters
}

Then, let's also add an Account document:

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

    @MongoId
    private ObjectId id;

    private String userEmail;

    private String nickName;

    private String accountDomain;

    private String password;

    // getters and setters
}

2.3. Repository

Then, we create a repository for each model class with some Spring Data methods.

First, let's add a UserRepository:

@Repository
public interface UserRepository extends MongoRepository<User, String> {

    User findByEmail(String email);
}

Following, we add an AccountRepository:

@Repository
public interface AccountRepository extends MongoRepository<Account, String> {

    Account findByAccountDomain(String account);
}

2.4. Connection Properties

Let's define the properties for the multiple databases we're using:

mongodb.primary.host=localhost
mongodb.primary.database=db1
mongodb.primary.authenticationDatabase=admin
mongodb.primary.username=user1
mongodb.primary.password=password
mongodb.primary.port=27017

mongodb.secondary.host=localhost
mongodb.secondary.database=db2
mongodb.secondary.authenticationDatabase=admin
mongodb.secondary.username=user2
mongodb.secondary.password=password
mongodb.secondary.port=27017

It's worth noticing that we have a property for the specific DB we'll use for authentication.

2.5. Primary Configuration

Now, we need our configuration. We'll make one for each database.

Let's have a look at our primary class definition in use for the UserRepository.:

@Configuration
@EnableMongoRepositories(basePackageClasses = UserRepository.class, mongoTemplateRef = "primaryMongoTemplate")
@EnableConfigurationProperties
public class PrimaryConfig {
    // beans
}

To demonstrate, let's break down all the beans and annotations.

Firstly, we'll retrieve and set properties using MongoProperties. This way, we directly map all the properties to a bean:

@Bean(name = "primaryProperties")
@ConfigurationProperties(prefix = "mongodb.primary")
@Primary
public MongoProperties primaryProperties() {
    return new MongoProperties();
}

To grant access to multiple users, we use a MongoDB authentication mechanism with MongoCredential. We construct our credential object by adding an authentication database, admin in this case:

@Bean(name = "primaryMongoClient")
public MongoClient mongoClient(@Qualifier("primaryProperties") MongoProperties mongoProperties) {

    MongoCredential credential = MongoCredential
      .createCredential(mongoProperties.getUsername(), mongoProperties.getAuthenticationDatabase(), mongoProperties.getPassword());

    return MongoClients.create(MongoClientSettings.builder()
      .applyToClusterSettings(builder -> builder
        .hosts(singletonList(new ServerAddress(mongoProperties.getHost(), mongoProperties.getPort()))))
      .credential(credential)
      .build());
}

As suggested by the latest releases, we're using SimpleMongoClientDatabaseFactory instead of creating a MongoTemplate from a connection string:

@Primary
@Bean(name = "primaryMongoDBFactory")
public MongoDatabaseFactory mongoDatabaseFactory(
  @Qualifier("primaryMongoClient") MongoClient mongoClient, 
  @Qualifier("primaryProperties") MongoProperties mongoProperties) {
    return new SimpleMongoClientDatabaseFactory(mongoClient, mongoProperties.getDatabase());
}

We need our beans to be @Primary here as we'll add more database configurations. Otherwise, we'll fall under the uniqueness constraint we discussed earlier.

We do the same with JPA when we are mapping multiple EntityManager. Similarly, we'll need a reference to the Mongotemplate in our @EnableMongoRepositories:

@EnableMongoRepositories(basePackageClasses = UserRepository.class, mongoTemplateRef = "primaryMongoTemplate")

2.6. Secondary Configurations

Finally, to double-check, let's have a look at the second database configuration:

@Configuration
@EnableMongoRepositories(basePackageClasses = AccountRepository.class, mongoTemplateRef = "secondaryMongoTemplate")
@EnableConfigurationProperties
public class SecondaryConfig {

    @Bean(name = "secondaryProperties")
    @ConfigurationProperties(prefix = "mongodb.secondary")
    public MongoProperties secondaryProperties() {
        return new MongoProperties();
    }

    @Bean(name = "secondaryMongoClient")
    public MongoClient mongoClient(@Qualifier("secondaryProperties") MongoProperties mongoProperties) {

        MongoCredential credential = MongoCredential
          .createCredential(mongoProperties.getUsername(), mongoProperties.getAuthenticationDatabase(), mongoProperties.getPassword());

        return MongoClients.create(MongoClientSettings.builder()
          .applyToClusterSettings(builder -> builder
            .hosts(singletonList(new ServerAddress(mongoProperties.getHost(), mongodProperties.getPort()))))
          .credential(credential)
          .build());
    }

    @Bean(name = "secondaryMongoDBFactory")
    public MongoDatabaseFactory mongoDatabaseFactory(
      @Qualifier("secondaryMongoClient") MongoClient mongoClient, 
      @Qualifier("secondaryProperties") MongoProperties mongoProperties) {
        return new SimpleMongoClientDatabaseFactory(mongoClient, mongoProperties.getDatabase());
    }

    @Bean(name = "secondaryMongoTemplate")
    public MongoTemplate mongoTemplate(@Qualifier("secondaryMongoDBFactory") MongoDatabaseFactory mongoDatabaseFactory) {
        return new MongoTemplate(mongoDatabaseFactory);
    }
}

In this case, it will reference the AccountRepository or all the classes belonging to the same package.

3. Tests

We'll test the application against a MongoDB instance. We can use MongoDB with Docker.

3.1. Start a MongoDB Container

Let's run a MongoDB container using Docker Compose. Let's have a look at our YAML template:

services:
  mongo:
    hostname: localhost
    container_name: 'mongo'
    image: 'mongo:latest'
    expose:
      - 27017
    ports:
      - 27017:27017
    environment:
      - MONGO_INITDB_DATABASE=admin
      - MONGO_INITDB_ROOT_USERNAME=admin
      - MONGO_INITDB_ROOT_PASSWORD=admin
    volumes:
      - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js

We need to initialize with a root user if we want authentication to be in place. To populate the database with more users, we are adding a bind mount to a JavaScript init file:

db.createUser(
    {
        user: "user1",
        pwd: "password",
        roles: [ { role: "readWrite", db: "db1" } ]
    }
)

db.createUser(
    {
        user: "user2",
        pwd: "password",
        roles: [ { role: "readWrite", db: "db2" } ]
    }
)

Let's run our container:

docker-compose up -d

When the container starts, it creates a volume for the mongo-init.js file and copies it to the container's entry point.

3.2. Spring Boot Tests

Let's wrap it all together in some basic Spring Boot tests:

@SpringBootTest(classes = { SpringBootMultipeDbApplication.class })
@TestPropertySource("/multipledb/multidb.properties")
public class MultipleDbUnitTest {

    // set up

    @Test
    void whenFindUserByEmail_thenNameOk() {
        assertEquals("name", userRepository.findByEmail("[email protected]")
          .getName());
    }

    @Test
    void whenFindAccountByDomain_thenNickNameOk() {
        assertEquals("nickname", accountRepository.findByAccountDomain("[email protected]")
          .getNickName());
    }
}

Primarily, we want to establish a connection for databases and get authentication. If we don't, we might have the database not populated or the MongoDb instance not started.

In those cases, we can have a look at the logs of our DB container, for example:

docker logs 30725c8635d4

It's worth noticing that the initial JavaScript executes only the first time the container runs. So, if we need to run with a different script, we may need to delete the volume:

docker-compose down -v

4. Conclusion

In this article, we've seen how to create multiple connections with Spring Data MongoDB. We saw how to add credentials to get authenticated. Most importantly, we've seen how to create configurations, one of which must be with primary beans. Finally, we've added some test cases against a running MongoDB instance using Docker and Spring Boot testing,

As always, the entire code of the article is available over on GitHub.

NoSql Bottom

Build a Dashboard Using Cassandra, Astra, and Stargate

>> CHECK OUT THE ARTICLE
Persistence bottom
Get started with Spring Data JPA through the reference Learn Spring Data JPA course: >> CHECK OUT THE COURSE
Persistence footer banner
guest
0 Comments
Inline Feedbacks
View all comments