Tuesday, December 19, 2023

Multiple Data Sources in Spring Boot 3 with Java 21

 

In this blog post, we'll explore the configuration and setup for a Spring Boot 3 application with Java 21 that uses multiple data sources. This setup is useful when dealing with databases of different types, such as MySQL, Oracle, and PostgreSQL. 






Technology


  • Spring Boot 3.2.0
  • Java 21 (Zulu)
  • Docker Compose
  • Maven 
  • IntelliJ IDEA  2023.3.1 Community 
  • Postman


Project Structure


multiple-data-sources-jpa
|-- src
|   |-- main
|       |-- java
|           |-- com
|               |-- henry
|                   |-- configuration
|                   |-- model
|                   |-- repository
|                   |-- service
|                   |-- controller
|-- src
|   |-- test
|       |-- java
|           |-- com
|               |-- henry
|                   |-- ...
|-- pom.xml


Application.yaml Configuration



The application file contains configuration deatils for each data source, including URLs, usernames, passowrds, and driver class names.


server:
  port: 9000
  servlet:
    context-path: /

spring:
  datasource:
    mysql:
      url: jdbc:mysql://localhost:3306/test_db?allowPublicKeyRetrieval=true
      username: test
      password: test_pass
      driverClassName: com.mysql.cj.jdbc.Driver
      ddlAuto: create-drop
    postgres:
      url: jdbc:postgresql:postgre_test
      username: postgre_test
      password: postgre_test
      driverClassName: org.postgresql.Driver
      ddlAuto: create-drop
    oracle:
      url: jdbc:oracle:thin:@//localhost:1521/xe
      username: system
      password: oracle
      driverClassName: oracle.jdbc.OracleDriver
      ddlAuto: update


Configuration Classes


We've created configuration classes for each data source - MysqlConfig, PostgreConfig, and OracleConfig:


package com.henry.configuration;

import com.henry.record.OracleRecord;
import com.henry.record.PostgreRecord;
import com.henry.record.MysqlRecord;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import lombok.Data;

@Component
@ConfigurationProperties("spring.datasource")
@Data
public class DataSourceProperties {
   private MysqlRecord mysql;
   private PostgreRecord postgres;
   private OracleRecord oracle;

}


package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.user",
        entityManagerFactoryRef = "userEntityManager",
        transactionManagerRef = "userTransactionManager"
)
public class MysqlConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean userEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(userDataSource());
        em.setPackagesToScan("com.henry.model.user");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(userHibernateProperties());

        return em;
    }

    @Bean
    @Primary
    public DataSource userDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getMysql().driverClassName());
        dataSource.setUrl(dsProperties.getMysql().url());
        dataSource.setUsername(dsProperties.getMysql().username());
        dataSource.setPassword(dsProperties.getMysql().password());

        return dataSource;
    }

    private Properties userHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto",dsProperties.getMysql().ddlAuto());

        return properties;
    }

    @Primary
    @Bean
    public PlatformTransactionManager userTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(userEntityManager().getObject());
        return transactionManager;
    }
}

package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.brand",
        entityManagerFactoryRef = "brandEntityManager",
        transactionManagerRef = "brandTransactionManager"
)
public class OracleConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    public LocalContainerEntityManagerFactoryBean brandEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(brandDataSource());
        em.setPackagesToScan("com.henry.model.brand");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(brandHibernateProperties());

        return em;
    }

    @Bean
    public DataSource brandDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getOracle().driverClassName());
        dataSource.setUrl(dsProperties.getOracle().url());
        dataSource.setUsername(dsProperties.getOracle().username());
        dataSource.setPassword(dsProperties.getOracle().password());

        return dataSource;
    }

    private Properties brandHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto", dsProperties.getOracle().ddlAuto());

        return properties;
    }


    @Bean
    public PlatformTransactionManager brandTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(brandEntityManager().getObject());
        return transactionManager;
    }
}

package com.henry.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;

import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@EnableJpaRepositories(
        basePackages = "com.henry.repository.company",
        entityManagerFactoryRef = "companyEntityManager",
        transactionManagerRef = "companyTransactionManager"
)
public class PostgreConfig {

    @Autowired
    private DataSourceProperties dsProperties;

    @Bean
    public LocalContainerEntityManagerFactoryBean companyEntityManager()
            throws NamingException {
        LocalContainerEntityManagerFactoryBean em
                = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(companyDataSource());
        em.setPackagesToScan("com.henry.model.company");

        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(companyHibernateProperties());

        return em;
    }

    @Bean
    public DataSource companyDataSource() throws IllegalArgumentException, NamingException {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(dsProperties.getPostgres().driverClassName());
        dataSource.setUrl(dsProperties.getPostgres().url());
        dataSource.setUsername(dsProperties.getPostgres().username());
        dataSource.setPassword(dsProperties.getPostgres().password());

        return dataSource;
    }

    private Properties companyHibernateProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.hbm2ddl.auto", dsProperties.getPostgres().ddlAuto());

        return properties;
    }

    @Bean
    public PlatformTransactionManager companyTransactionManager() throws NamingException {
        final JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(companyEntityManager().getObject());
        return transactionManager;
    }
}


Record Classes


To encapsulate the configuration properties for each data source, we've defined record classes - MysqlRecord, PostgreRecord, and OracleRecord:


package com.henry.record;

public record MysqlRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}

package com.henry.record;

public record OracleRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}


package com.henry.record;

public record PostgreRecord(String url, String username, String password, String driverClassName,String ddlAuto) {
}

Repositories


Repository interfaces extend the CrudRepository for each entity (Brand, Company, User):


package com.henry.repository.brand;

import com.henry.model.brand.Brand;
import org.springframework.data.repository.CrudRepository;

public interface BrandRepository extends CrudRepository<Brand,Long> {
}

package com.henry.repository.company;

import com.henry.model.company.Company;
import org.springframework.data.repository.CrudRepository;

public interface CompanyRepository extends CrudRepository<Company,Long> {
}

package com.henry.repository.user;

import com.henry.model.user.User;
import org.springframework.data.repository.CrudRepository;

public interface UserRepository extends CrudRepository<User,Long> {
}

Services


Service classes (BrandServiceImpl, CompanyServiceImpl, UserServiceImpl) implement a common interface (DefaultService) for generic CRUD operations:

package com.henry.service;

public sealed interface DefaultService<T, G> permits UserServiceImpl, CompanyServiceImpl, BrandServiceImpl {

    T save(T obj);
    Iterable<T>  findAll();
    T findById(G id);
}

package com.henry.service;

import com.henry.model.brand.Brand;
import com.henry.repository.brand.BrandRepository;
import org.springframework.stereotype.Service;

@Service
public final class BrandServiceImpl implements DefaultService<Brand, Long> {

    private final BrandRepository brandRepository;

    public BrandServiceImpl(BrandRepository brandRepository) {
        this.brandRepository = brandRepository;
    }


    @Override
    public Brand save(Brand obj) {
        return brandRepository.save(obj);
    }

    @Override
    public Iterable<Brand> findAll() {
        return brandRepository.findAll();
    }

    @Override
    public Brand findById(Long id) {
        return brandRepository.findById(id).get();
    }
}

package com.henry.service;

import com.henry.model.company.Company;
import com.henry.repository.company.CompanyRepository;
import org.springframework.stereotype.Service;

@Service
public final class CompanyServiceImpl implements DefaultService<Company, Long> {

    private final CompanyRepository companyRepository;

    public CompanyServiceImpl(CompanyRepository companyRepository) {
        this.companyRepository = companyRepository;
    }

    @Override
    public Company save(Company obj) {
        return companyRepository.save(obj);
    }

    @Override
    public Iterable<Company> findAll() {
        return companyRepository.findAll();
    }

    @Override
    public Company findById(Long id) {
        return companyRepository.findById(id).get();
    }
}

package com.henry.service;

import com.henry.model.user.User;
import com.henry.repository.user.UserRepository;
import org.springframework.stereotype.Service;

@Service
public final class UserServiceImpl implements DefaultService<User,Long> {

    private final UserRepository userRepository;

    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public User save(User obj) {
        return userRepository.save(obj);
    }

    @Override
    public Iterable<User> findAll() {
        return userRepository.findAll();
    }

    @Override
    public User findById(Long id) {
        return userRepository.findById(id).get();
    }
}

REST Controllers


REST controllers (BrandController, CompanyController, UserController) handle HTTP requests for creating entities:


package com.henry.controller;

import com.henry.model.brand.Brand;
import com.henry.service.DefaultService;
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("/api/v3")
public class BrandController {

    private final DefaultService<Brand, Long> defaultService;

    public BrandController(DefaultService<Brand, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/brands")
    public Brand createEmployee(@RequestBody Brand brand) {
        return defaultService.save(brand);
    }

}

package com.henry.controller;

import com.henry.model.company.Company;
import com.henry.service.DefaultService;
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("/api/v2")
public class CompanyController {

    private final DefaultService<Company,Long> defaultService;

    public CompanyController(DefaultService<Company, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/companies")
    public Company createEmployee(@RequestBody Company company) {
        return defaultService.save(company);
    }
}

package com.henry.controller;

import com.henry.model.user.User;
import com.henry.service.DefaultService;
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("/api/v1")
public class UserController {

    private final DefaultService<User,Long> defaultService;

    public UserController(DefaultService<User, Long> defaultService) {
        this.defaultService = defaultService;
    }

    @PostMapping("/users")
    public User createEmployee(@RequestBody User user) {
        return defaultService.save(user);
    }
}


Test Classes


JUnit test classes (BrandServiceTest, CompanyServiceTest, UserServiceTest) use Mockito for mocking dependencies:


package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.brand.Brand;
import com.henry.repository.brand.BrandRepository;
import com.henry.service.BrandServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class BrandServiceTest {

    @Mock
    private BrandRepository brandRepository;

    @InjectMocks
    private BrandServiceImpl brandService;

    @Test
    void testSaveBrand() {
        Brand brand = Brand.builder()
                .id(1L)
                .name("Test Brand")
                .build();

        when(brandRepository.save(any())).thenReturn(brand);

        Brand saved = brandService.save(brand);

        assertEquals(brand, saved);
        verify(brandRepository).save(any());
    }

    @Test
    void testFindAllBrands() {
        Brand brand1 = Brand.builder()
                .id(1L)
                .name("Test Brand")
                .build();
        Brand brand2 = Brand.builder()
                .id(2L)
                .name("Demo Brand")
                .build();

        when(brandRepository.findAll()).thenReturn(Arrays.asList(brand1, brand2));

        Iterable<Brand> result = brandService.findAll();

        assertEquals(2, Iterables.size(result));
        verify(brandRepository).findAll();
    }

    @Test
    void testFindBrandById() {
        Brand brand = Brand.builder()
                .id(1L)
                .name("Demo Brand")
                .build();

        when(brandRepository.findById(1L)).thenReturn(Optional.of(brand));

        Brand result = brandService.findById(1L);

        assertEquals(brand, result);
        verify(brandRepository).findById(1L);
    }
}
package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.company.Company;
import com.henry.repository.company.CompanyRepository;
import com.henry.service.CompanyServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CompanyServiceTest {

    @Mock
    private CompanyRepository companyRepository;

    @InjectMocks
    private CompanyServiceImpl companyService;

    @Test
    void testSaveCompany() {
        Company company =
                Company.builder()
                        .id(1L)
                        .name("Test Corp")
                        .build();

        when(companyRepository.save(any())).thenReturn(company);

        Company saved = companyService.save(company);

        assertEquals(company, saved);
        verify(companyRepository).save(any());
    }

    @Test
    void testFindAllCompanies() {
        Company comp1 = Company.builder()
                .id(1L)
                .name("Test Corp")
                .build();
        Company comp2 = Company.builder()
                .id(2L)
                .name("Demo Corp")
                .build();

        when(companyRepository.findAll()).thenReturn(Arrays.asList(comp1, comp2));

        Iterable<Company> result = companyService.findAll();


        assertEquals(2, Iterables.size(result));
        verify(companyRepository).findAll();
    }

    @Test
    void testFindCompanyById() {
        Company company = Company.builder()
                .id(1L)
                .name("Test Corp")
                .build();

        when(companyRepository.findById(1L)).thenReturn(Optional.of(company));

        Company result = companyService.findById(1L);

        assertEquals(company, result);
        verify(companyRepository).findById(1L);
    }
}
package com.henry;

import com.google.common.collect.Iterables;
import com.henry.model.user.User;
import com.henry.repository.user.UserRepository;
import com.henry.service.UserServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Arrays;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserServiceImpl userService;

    @Test
    public void saveUser_shouldPersistUser() {
/**

 int x = 10;
 int y = 20;
 String result = STR."\{x} + \{y} = \{x + y}";
 string templates are a preview feature and are disabled by default.
 Are string templates supported yet? - IntelliJ support - JetBrains
 https://intellij-support.jetbrains.com/hc/en-us/community/posts/13451540436114-Are-string-templates-supported-yet-
 **/

        // Create a User
        User user = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.save(any())).thenReturn(user);

        User saved = userService.save(user);

        assertEquals(user, saved);
        verify(userRepository).save(any());
    }

    @Test
    void testFindAllUsers() {
        User user1 = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();
        User user2 = User.builder()
                .id(2L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));

        Iterable<User> result = userService.findAll();

        assertEquals(2, Iterables.size(result));
        verify(userRepository).findAll();
    }

    @Test
    void testFindUserById() {
        User user = User.builder()
                .id(1L)
                .name("John")
                .lastName("Doe")
                .build();

        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        User result = userService.findById(1L);

        assertEquals(user, result);
        verify(userRepository).findById(1L);
    }
}


Maven Configuration


The pom.xml file manages project dependencies and build configuration:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.henry</groupId>
	<artifactId>multiple-data-sources-jpa</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>multiple-data-sources-jpa</name>
	<description>Demo project multiple data sources jpa</description>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<exclusions>
				<!-- Exclude the Tomcat dependency -->
				<exclusion>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-starter-tomcat</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<!-- Use Jetty instead -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jetty</artifactId>
		</dependency>

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

		<!-- MARIADB Connector -->

		<dependency>
			<groupId>org.mariadb.jdbc</groupId>
			<artifactId>mariadb-java-client</artifactId>
		</dependency>

		<!-- Mysql Connector -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.30</version>
		</dependency>

		<!-- ORACLE database driver -->
		<!-- https://mvnrepository.com/artifact/oracle/ojdbc6 -->
		<dependency>
			<groupId>com.oracle</groupId>
			<artifactId>ojdbc6</artifactId>
			<version>11.2.0.4</version>
		</dependency>

		<!-- POSTGRESQL database driver -->
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
			<version>31.0.1-jre</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.antlr/stringtemplate
		<dependency>
			<groupId>org.antlr</groupId>
			<artifactId>stringtemplate</artifactId>
			<version>4.0.2</version>
		</dependency>
		-->

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>21</source>
					<target>21</target>
					<compilerArgs>--enable-preview</compilerArgs>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>


Environment Setup with Docker Compose


To simplify the setup of your application's environment, you can use Docker Compose. Below is a docker-compose-multiple-db.yml file that defines services for Oracle, PostgreSQL, and MySQL databases:

version: '3'
services:
  oracle:
    image: pengbai/docker-oracle-xe-11g-r2
    ports:
      - "1521:1521"
      - "8080:8080"
    shm_size: 1g

  postgres:
    image: postgres:14.1
    container_name: postgre_test
    environment:
      POSTGRES_USER: postgre_test
      POSTGRES_PASSWORD: postgre_test
      POSTGRES_DB: postgre_test
    ports:
      - "5432:5432"

  mysql:
    image: mysql/mysql-server:latest
    container_name: mysql-docker-container
    environment:
      MYSQL_ROOT_PASSWORD: mypass
      MYSQL_DATABASE: test_db
      MYSQL_USER: test
      MYSQL_PASSWORD: test_pass
    ports:
      - "3306:3306"

Run command:

 
docker-compose -f docker-compose-multiple-db.yml up -d



Only for oracle:

mvn install:install-file "-Dfile=path/to/your/ojdbc6.jar" "-DgroupId=com.oracle" "-DartifactId=ojdbc6" "-Dversion=11.2.0.4" "-Dpackaging=jar" "-DgeneratePom=true"


Run & Test


Run Spring Boot application with command: by console, IntelliJ etc.


mvn test -Dtest=UserServiceTest

mvn test -Dtest=BrandServiceTest

mvn test -Dtest=CompanyServiceTest







Run locally: mvn spring-boot:run





POST
http://localhost:9000/api/v1/users

{
    "name""henry",
    "lastName""x"
}







POST
http://localhost:9000/api/v2/companies
{
    "name""company1"
}











POST
http://localhost:9000/api/v3/brands

{
    "name""Demo Brand"
}












Source Code


Here on GitHub.


Notes: 


Possible issues:
  • Lombok Plugin check if it was installed in IntelliJ 
  • Others issues please -> File | Invalidate Caches
  • Change the port if you have in local databases. Otherwise you have conflicts.






Virtual Threads in Java 21: Simplified Concurrency for Modern Applications

  With Java 21, Virtual Threads have redefined how we approach concurrency, offering a lightweight and efficient way to handle parallel and ...