It would be very convenient if a single server only needed one database, but there can be exceptional situations
Unless you have a centrally managed server (acting as a router) that filters requests and connects various backend servers, you’ve probably considered configuring multiple DataSources
Project We’ll Build
Let’s create a server that manages Home IoT
The domains are IoTLog, Device, and Users
While mapping them together would make a great project, let’s focus on configuring multiple DataSources rather than entity mapping
Development Language and Tech Stack
Kotlin
SpringBoot
MariaDB
PostgreSQL
Mongodb
Initial Project Setup
spring-boot-starter-web
spring-boot-starter-data-jpa
spring-boot-starter-data-mongodb
postgresql
mariadb
lombok
build.gradle.kts
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id ( "org.springframework.boot" ) version "2.7.3"
id ( "io.spring.dependency-management" ) version "1.0.13.RELEASE"
kotlin ( "jvm" ) version "1.6.21"
kotlin ( "plugin.spring" ) version "1.6.21"
kotlin ( "plugin.jpa" ) version "1.6.21"
}
group = "wool"
version = "0.0.1-SNAPSHOT"
java . sourceCompatibility = JavaVersion . VERSION_17
configurations {
compileOnly {
extendsFrom ( configurations . annotationProcessor . get ())
}
}
repositories {
mavenCentral ()
}
dependencies {
implementation ( "org.springframework.boot:spring-boot-starter-data-jpa" )
implementation ( "org.springframework.boot:spring-boot-starter-web" )
implementation ( "com.fasterxml.jackson.module:jackson-module-kotlin" )
implementation ( "org.jetbrains.kotlin:kotlin-reflect" )
implementation ( "org.jetbrains.kotlin:kotlin-stdlib-jdk8" )
implementation ( "org.springframework.boot:spring-boot-starter-data-mongodb" )
compileOnly ( "org.projectlombok:lombok" )
runtimeOnly ( "org.mariadb.jdbc:mariadb-java-client" )
runtimeOnly ( "org.postgresql:postgresql" )
annotationProcessor ( "org.projectlombok:lombok" )
testImplementation ( "org.springframework.boot:spring-boot-starter-test" )
}
tasks . withType < KotlinCompile > {
kotlinOptions {
freeCompilerArgs = listOf ( "-Xjsr305=strict" )
jvmTarget = "17"
}
}
tasks . withType < Test > {
useJUnitPlatform ()
}
This is the gradle file with dependencies configured
Manipulating Configuration - Direct Bean Configuration
application.properties handles configuration automatically, so entering basic settings completes the setup automatically
Since the server we’re building requires additional settings beyond the basics, we’ll create Beans directly
application.properties
# Maria DB - "db1"
users-data.datasource.url=jdbc:mariadb://localhost:3306/paul?characterEncoding=UTF-8&serverTimezone=UTC
users-data.datasource.username=paul
users-data.datasource.password=qwerqwer123
users-data.datasource.driver-class-name=org.mariadb.jdbc.Driver
users-data.datasource.hikari.connection-test-query=SELECT 1
# PostgreSQL DB - "db2"
device-data.datasource.url=jdbc:postgresql://localhost:5432/paul
device-data.datasource.username=paul
device-data.datasource.password=qwerqwer123
device-data.datasource.driver-class-name=org.postgresql.Driver
device-data.datasource.hikari.connection-test-query=SELECT 1
# MongoDB
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.username=paul
spring.data.mongodb.password=qwerqwer123
spring.data.mongodb.database=paul
spring.data.mongodb.authentication-database=admin
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
# logging
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n
logging.level.org.hibernate.SQL=debug
server.port=8000
I’ve listed separate configuration values for Maria DB, PostgreSQL DB, and Mongo DB
PostgreSQL stores user data (Users), Maria DB manages the Device table (Device), and Mongo DB stores raw Device usage logs
Actually, our code doesn’t need much of the MongoDB configuration values
This can be modified later to read configuration values from the properties file
Creating Configuration Bean 1 - PostgreSQL DB Setup
Create a config package and create a PostgresqlConfig file under it
package wool.multidb.config
import com.zaxxer.hikari.HikariDataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
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.orm.jpa.JpaTransactionManager
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.persistence.EntityManagerFactory
import javax.sql.DataSource
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories (
entityManagerFactoryRef = "deviceEntityManagerFactory" ,
basePackages = [ "wool.multidb.device.repository" ]
)
class PostgresqlConfig {
@Primary
@Bean ( name = [ "deviceDataSourceProperties" ])
@ConfigurationProperties ( "device-data.datasource" )
fun deviceDataSourceProperties (): DataSourceProperties {
return DataSourceProperties ()
}
@Primary
@Bean ( name = [ "deviceDataSource" ])
@ConfigurationProperties ( "device-data.datasource.configuration" )
fun dataSource ( @Qualifier ( "deviceDataSourceProperties" ) deviceDataSourceProperties : DataSourceProperties ): DataSource {
return deviceDataSourceProperties . initializeDataSourceBuilder (). type ( HikariDataSource :: class . java ). build ()
}
@Primary
@Bean ( name = [ "deviceEntityManagerFactory" ])
fun entityManagerFactory (
builder : EntityManagerFactoryBuilder ,
@Qualifier ( "deviceDataSource" ) deviceDataSource : DataSource
): LocalContainerEntityManagerFactoryBean {
return builder
. dataSource ( deviceDataSource )
. packages ( "wool.multidb.device.domain" )
. persistenceUnit ( "device" )
. build ()
}
@Primary
@Bean ( name = [ "deviceTransactionManager" ])
fun transactionManager (
@Qualifier ( "deviceEntityManagerFactory" ) deviceEntityManagerFactory : EntityManagerFactory
): PlatformTransactionManager {
return JpaTransactionManager ( deviceEntityManagerFactory )
}
}
Creating Configuration Bean 2 - Maria DB Setup
Create a config package and create a MariaConfig file under it
package wool.multidb.config
import com.zaxxer.hikari.HikariDataSource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaRepositories
import org.springframework.orm.jpa.JpaTransactionManager
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.EnableTransactionManagement
import javax.persistence.EntityManagerFactory
import javax.sql.DataSource
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories (
entityManagerFactoryRef = "entityManagerFactory" ,
basePackages = [ "wool.multidb.users.repository" ]
)
class MariaConfig {
@Bean ( name = [ "dataSourceProperties" ])
@ConfigurationProperties ( "users-data.datasource" )
fun usersDataSourceProperties (): DataSourceProperties {
return DataSourceProperties ()
}
@Bean ( name = [ "dataSource" ])
@ConfigurationProperties ( "users-data.datasource.configuration" )
fun dataSource ( @Qualifier ( "dataSourceProperties" ) usersDataSourceProperties : DataSourceProperties ): DataSource {
return usersDataSourceProperties . initializeDataSourceBuilder (). type ( HikariDataSource :: class . java )
. build ()
}
@Bean ( name = [ "entityManagerFactory" ])
fun entityManagerFactory (
builder : EntityManagerFactoryBuilder , @Qualifier ( "dataSource" ) dataSource : DataSource
): LocalContainerEntityManagerFactoryBean {
return builder
. dataSource ( dataSource )
. packages ( "wool.multidb.users.domain" )
. persistenceUnit ( "users" )
. build ()
}
@Bean ( name = [ "transactionManager" ])
fun transactionManager (
@Qualifier ( "entityManagerFactory" ) usersEntityManagerFactory : EntityManagerFactory
): PlatformTransactionManager {
return JpaTransactionManager ( usersEntityManagerFactory )
}
}
Creating Configuration Bean 3 - Mongo DB Setup
Create a config package and create a MongoConfig file under it
package wool.multidb.config
import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import org.springframework.context.annotation.Configuration
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
@Configuration
@EnableMongoRepositories ( basePackages = [ "wool.multidb.iotlog.repository" ])
class MongoConfig : AbstractMongoClientConfiguration () {
override fun getDatabaseName (): String {
return "iotlog"
}
override fun mongoClient (): MongoClient {
val connectionString = ConnectionString ( "mongodb://paul:qwerqwer123@localhost:27017/paul?authSource=admin" )
val mongoClientSettings = MongoClientSettings
. builder ()
. applyConnectionString ( connectionString )
. build ()
return MongoClients . create ( mongoClientSettings )
}
}
The ConnectionString used here contains user, password, and admin DB information
Later, during enhancement, you can extract these values to properties or use Kubernetes secrets
Developing Users Package - Controller, Service, Repository, Domain
Domain - UsersEntity
package wool.multidb.users.domain
import lombok.Data
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table
@Entity
@Data
@Table ( name = "users" )
data class UsersEntity (
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
val id : Int ? = null ,
val userName : String ,
val email : String ,
)
repository - UsersEntityRepository
package wool.multidb.users.repository
import org.springframework.data.jpa.repository.JpaRepository
import wool.multidb.users.domain.UsersEntity
interface UsersEntityRepository : JpaRepository < UsersEntity , Int > {
}
package wool.multidb.users.service
import org.springframework.stereotype.Service
import wool.multidb.users.domain.UsersEntity
import wool.multidb.users.repository.UsersEntityRepository
@Service
class UsersService (
private val usersEntityRepository : UsersEntityRepository
) {
fun getUsers (): List < UsersEntity > = usersEntityRepository . findAll ()
}
controller - UsersController
package wool.multidb.users.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import wool.multidb.users.domain.UsersEntity
import wool.multidb.users.service.UsersService
@RestController
class UsersController (
private val usersService : UsersService
) {
@GetMapping ( "/users" )
fun getAllUsers (): List < UsersEntity > {
return usersService . getUsers ()
}
}
Developing Device Package - Controller, Service, Repository, Domain
Domain - DeviceEntity
package wool.multidb.device.domain
import lombok.Data
import javax.persistence.*
@Entity
@Data
@Table ( name = "device" )
data class DeviceEntity (
@Id
@GeneratedValue ( strategy = GenerationType . IDENTITY )
var id : Int ? = null ,
var deviceName : String ? = null ,
var deviceType : String ? = null
)
repository - DeviceEntityRepository
package wool.multidb.device.repository
import org.springframework.data.jpa.repository.JpaRepository
import wool.multidb.device.domain.DeviceEntity
interface DeviceEntityRepository : JpaRepository < DeviceEntity , Int > {
}
service - DeviceService
package wool.multidb.device.service
import org.springframework.stereotype.Service
import wool.multidb.device.domain.DeviceEntity
import wool.multidb.device.repository.DeviceEntityRepository
@Service
class DeviceService (
private val deviceEntityRepository : DeviceEntityRepository
) {
fun getAllDevices (): MutableList < DeviceEntity > {
return deviceEntityRepository . findAll ()
}
}
controller - DeviceController
package wool.multidb.device.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import wool.multidb.device.domain.DeviceEntity
import wool.multidb.device.service.DeviceService
@RestController
class DeviceController (
val deviceService : DeviceService
) {
@GetMapping ( "/devices" )
fun getAllDevices (): MutableList < DeviceEntity > {
return deviceService . getAllDevices ()
}
}
Developing IoTLog Package - Controller, Service, Repository, Domain
Since this is MongoDB setup, we’ll inherit from MongoTemplate rather than JPARepository
We’ll try storing Light (bulb) data from IoT Devices
Domain - Light
package wool.multidb.iotlog.domain
import org.springframework.data.mongodb.core.mapping.Document
import javax.persistence.Entity
import javax.persistence.Id
@Document ( collection = "iotLog" )
data class Light (
@Id
var id : String ? = null ,
var status : Boolean = false
)
repository - IoTRepository
package wool.multidb.iotlog.repository
import org.springframework.data.mongodb.repository.MongoRepository
import wool.multidb.iotlog.domain.Light
interface IoTRepository : MongoRepository < Light , String >{}
service - IoTService
package wool.multidb.iotlog.service
import org.springframework.data.mongodb.core.MongoTemplate
import org.springframework.stereotype.Service
import wool.multidb.iotlog.domain.Light
import wool.multidb.iotlog.repository.IoTRepository
@Service
class IoTService (
val iotRepository : IoTRepository ,
val mongoTemplate : MongoTemplate
) {
fun getAllLight (): List < Light > {
return iotRepository . findAll ()
}
fun putLightLog ( light : Light ) {
mongoTemplate . save ( light )
}
}
controller - IoTController
package wool.multidb.iotlog.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import wool.multidb.iotlog.domain.Light
import wool.multidb.iotlog.service.IoTService
@RestController
class IoTController (
private val ioTService : IoTService
) {
@GetMapping ( "/light" )
fun getAllLight (): List < Light > {
return ioTService . getAllLight ()
}
@PostMapping ( "/light" )
fun addLightLog ( @RequestBody light : Light ) {
return ioTService . putLightLog ( light )
}
}
MongoDB or Document DB Config Security
Some companies or projects may require CA file configuration for connections. In such cases, you can add SSLConfig with code like below:
override fun getDatabaseName (): String {
return "DB name"
}
override fun mongoClient (): MongoClient {
val trustStorePath = "./auth"
val keyStore : KeyStore = KeyStore . getInstance ( KeyStore . getDefaultType ())
keyStore . load ( null )
val certF : CertificateFactory = CertificateFactory . getInstance ( "X.509" )
val cert : Certificate = certF . generateCertificate ( URL ( "https://s3.amazonaws.com/rds-downloads/rds-ca-2019-root.pem" ). openStream ())
keyStore . setCertificateEntry ( "mongo-cert" , cert )
FileOutputStream ( trustStorePath ). use { out -> keyStore . store ( out , "pass" . toCharArray ()) }
val trustManagerFactory : TrustManagerFactory = TrustManagerFactory . getInstance ( TrustManagerFactory . getDefaultAlgorithm ())
trustManagerFactory . init ( keyStore )
val sslContext = SSLContext . getInstance ( "TLS" )
sslContext . init ( null , trustManagerFactory . getTrustManagers (), SecureRandom ())
val builder = MongoClientSettings . builder ()
. applyConnectionString ( ConnectionString ( "mongodb-url" ))
. applyToSslSettings { b : SslSettings . Builder ->
b . enabled ( true )
b . context ( sslContext )
}
. build ()
return MongoClients . create ( builder )
}
Verification
Run the SpringBoot server and verify that it works properly
The configuration values written in application properties have been moved inside by creating a config package
When you call each URL defined in the controller, you can see that everything works well
관련 글
태그:
Database ,
Database Configuration ,
Database Connection ,
Java ,
Kotlin ,
MongoDB ,
MySQL ,
PostgreSQL ,
Spring ,
Spring-Kotlin ,
SpringBoot
카테고리:
Backend Engineering ,
Spring
업데이트: July 2, 2022
댓글남기기