├── .gitignore ├── 00_init.sql ├── README.md ├── docker-compose.yml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── ehsaniara │ │ └── multidatasource │ │ ├── DemoApplication.java │ │ ├── configurations │ │ ├── DataSourceConfigRead.java │ │ ├── DataSourceConfigWrite.java │ │ ├── HikariConfigRead.java │ │ ├── HikariConfigWrite.java │ │ ├── HikariReadProperties.java │ │ └── HikariWriteProperties.java │ │ ├── controller │ │ └── CustomerControllerImpl.java │ │ ├── handler │ │ ├── ExceptionHandlerControllerAdvice.java │ │ ├── ExceptionResponse.java │ │ └── ResourceNotFoundException.java │ │ ├── model │ │ └── Customer.java │ │ ├── repository │ │ ├── CustomerRepository.java │ │ ├── CustomerRepositoryCombo.java │ │ ├── readRepository │ │ │ └── CustomerReadRepository.java │ │ └── writeRepository │ │ │ └── CustomerWriteRepository.java │ │ └── service │ │ ├── CustomerService.java │ │ └── CustomerServiceImpl.java └── resources │ └── application.yml └── test └── java └── com └── ehsaniara └── multidatasource └── DemoApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | /target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | /nbproject/private/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | /build/ 27 | 28 | mvnw 29 | mvnw.cmd 30 | 31 | .mvn/** -------------------------------------------------------------------------------- /00_init.sql: -------------------------------------------------------------------------------- 1 | create user replicator with replication encrypted password 'postgres_user_for_db_read'; 2 | select pg_create_physical_replication_slot('replication_slot'); 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot 2 with Multiple DataSource 2 | 3 | There are times that even having the best DataBase (PostgresSql, Oracle, MySql,... ) Tuning can not be as help-full as Application Level separating Read and Writes 4 | 5 | Spring Boot 2.2.2 with Multiple DataSource 6 | ## Postgres Setup 7 | For This Demo you need 2.2.2 separate Postgres DataBase where one as Master and the other re one as a Replica. 8 | 9 | 10 | for simplicity just run: 11 | ```docker-compose up --force-recreate``` 12 | 13 | the docker-compose.yml is already in the project which contains 2 PostgresSql in 2 different ports, with ```demo``` DataBase 14 | 15 | > you can always uninstall it as: ```docker-compose down``` if you needed to. 16 | 17 | 18 | Now if run this line you create customer in **postgres_primary**: 19 | ``` 20 | curl -H "Content-Type: application/json" --request POST --data '{"name":"Jay"}' http://localhost:8080/customer 21 | ``` 22 | OR 23 | ``` 24 | curl -H "Content-Type: application/json" --request PUT --data '{"id":1 , "name":"Jay ehsaniara"}' http://localhost:8080/customer 25 | ``` 26 | 27 | But if you run this line you getting data from **postgres_replica**: 28 | ``` 29 | curl --request GET http://localhost:8080/customer/1 30 | ``` 31 | 32 | 33 | --- 34 | ### Spring Boot Setup 35 | From https://start.spring.io/ select **web**, **data-jpa**, **lombok**, **postgresDriver** 36 | Or Select the following share link: 37 | Spring Initializr generates spring boot project with just what you need to start quickly!start.spring.io 38 | 39 | Once you Generate and download the zip file, you should have similar POM file as: 40 | ```xml 41 | 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-web 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-data-jpa 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-configuration-processor 57 | 58 | 59 | 60 | org.projectlombok 61 | lombok 62 | true 63 | 64 | 65 | 66 | org.postgresql 67 | postgresql 68 | runtime 69 | 70 | 71 | 72 | 73 | 74 | ``` 75 | 76 | for this demo I use HikariDataSource as a default connection pool library by Spring Boot 2.2.2 77 | we need to have 2 separate DataSource and EntityManager one for the Writes(Master/Primary) and one for Reads(Slave/Secondary). 78 | (application.yml) 79 | ```yaml 80 | 81 | spring: 82 | datasource-write: 83 | driver-class-name: org.postgresql.Driver 84 | jdbc-url: jdbc:postgresql://localhost:5432/demo 85 | username: postgres 86 | password: password 87 | platform: postgresql 88 | hikari: 89 | idle-timeout: 10000 90 | maximum-pool-size: 10 91 | minimum-idle: 5 92 | pool-name: WriteHikariPool 93 | 94 | datasource-read: 95 | driver-class-name: org.postgresql.Driver 96 | jdbc-url: jdbc:postgresql://localhost:5433/demo 97 | username: postgres 98 | password: password 99 | platform: postgresql 100 | hikari: 101 | idle-timeout: 10000 102 | maximum-pool-size: 10 103 | minimum-idle: 5 104 | pool-name: ReadHikariPool 105 | ``` 106 | 107 | as you see I have 2 data-source as: datasource-write and datasource-read with their own credentials. 108 | 109 | DataSource Configurations for WriteDB: 110 | ```java 111 | @Configuration 112 | @ConfigurationProperties("spring.datasource-write") 113 | @EnableTransactionManagement 114 | @EnableJpaRepositories( 115 | entityManagerFactoryRef = "entityManagerFactoryWrite", 116 | transactionManagerRef = "transactionManagerWrite", 117 | basePackages = {"com.ehsaniara.multidatasource.repository.writeRepository"} 118 | ) 119 | public class DataSourceConfigWrite extends HikariConfig { 120 | 121 | @Bean 122 | public HikariDataSource dataSourceWrite() { 123 | return new HikariDataSource(this); 124 | } 125 | 126 | @Bean 127 | public LocalContainerEntityManagerFactoryBean entityManagerFactoryWrite( 128 | final HikariDataSource dataSourceWrite) { 129 | 130 | LocalContainerEntityManagerFactoryBean entityManagerFactory 131 | = new LocalContainerEntityManagerFactoryBean(); 132 | 133 | entityManagerFactory.setDataSource(dataSourceWrite); 134 | entityManagerFactory.setDataSource(dataSourceWrite); 135 | entityManagerFactory.setPersistenceProviderClass(HibernatePersistenceProvider.class); 136 | entityManagerFactory.setPersistenceUnitName("write"); 137 | entityManagerFactory.setPackagesToScan(MODEL_PACKAGE); 138 | entityManagerFactory.setJpaProperties(JPA_PROPERTIES); 139 | return entityManagerFactory; 140 | } 141 | 142 | @Bean 143 | public PlatformTransactionManager transactionManagerWrite(EntityManagerFactory entityManagerFactoryWrite) { 144 | return new JpaTransactionManager(entityManagerFactoryWrite); 145 | } 146 | } 147 | ``` 148 | 149 | DataSource Configurations for ReadDB: 150 | 151 | ```java 152 | @Configuration 153 | @ConfigurationProperties("spring.datasource-read") 154 | @EnableTransactionManagement 155 | @EnableJpaRepositories( 156 | entityManagerFactoryRef = "entityManagerFactoryRead", 157 | transactionManagerRef = "transactionManagerRead", 158 | basePackages = {"com.ehsaniara.multidatasource.repository.readRepository"} 159 | ) 160 | public class DataSourceConfigRead extends HikariConfig { 161 | 162 | @Bean 163 | public HikariDataSource dataSourceRead() { 164 | return new HikariDataSource(this); 165 | } 166 | 167 | @Bean 168 | public LocalContainerEntityManagerFactoryBean entityManagerFactoryRead( 169 | final HikariDataSource dataSourceRead) { 170 | 171 | LocalContainerEntityManagerFactoryBean factoryBean 172 | = new LocalContainerEntityManagerFactoryBean(); 173 | factoryBean.setDataSource(dataSourceRead); 174 | factoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); 175 | factoryBean.setPersistenceUnitName("read"); 176 | factoryBean.setPackagesToScan(MODEL_PACKAGE); 177 | factoryBean.setJpaProperties(JPA_PROPERTIES); 178 | 179 | return factoryBean; 180 | } 181 | 182 | @Bean 183 | public PlatformTransactionManager transactionManagerRead(EntityManagerFactory entityManagerFactoryRead) { 184 | return new JpaTransactionManager(entityManagerFactoryRead); 185 | } 186 | } 187 | ``` 188 | 189 | Read and Write repositories should be in a separated packages: 190 | 191 | + Write: ```com.ehsaniara.multidatasource.repository.writeRepository``` 192 | 193 | + Read: ```com.ehsaniara.multidatasource.repository.readRepository``` 194 | 195 | you also need to set: 196 | (write) 197 | ```java 198 | Properties properties = new Properties(); 199 | properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL10Dialect"); 200 | properties.put("hibernate.hbm2ddl.auto", "update"); 201 | properties.put("hibernate.ddl-auto", "update"); 202 | properties.put("show-sql", "true"); 203 | ``` 204 | 205 | on read config 206 | ```java 207 | Properties properties = new Properties(); 208 | properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL10Dialect"); 209 | properties.put("show-sql", "true"); 210 | ``` 211 | Note: tables are automatically replicated 212 | 213 | and the actual logic are in the service layer: 214 | 215 | ```java 216 | @Service 217 | public class CustomerServiceImpl implements CustomerService { 218 | 219 | private final CustomerReadRepository customerReadRepository; 220 | private final CustomerWriteRepository customerWriteRepository; 221 | 222 | public CustomerServiceImpl(CustomerReadRepository customerReadRepository, CustomerWriteRepository customerWriteRepository) { 223 | this.customerReadRepository = customerReadRepository; 224 | this.customerWriteRepository = customerWriteRepository; 225 | } 226 | 227 | public Optional getCustomer(Long id) { 228 | return customerReadRepository.findById(id); 229 | } 230 | 231 | public Customer createCustomer(Customer customer) { 232 | 233 | return customerWriteRepository.save(customer); 234 | } 235 | 236 | public Customer updateCustomer(Customer customer) { 237 | 238 | return customerWriteRepository.save(customer); 239 | } 240 | } 241 | ``` 242 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-common: 2 | &x-common 3 | image: postgres:15 4 | user: postgres 5 | restart: always 6 | networks: 7 | - my-network 8 | healthcheck: 9 | test: 'pg_isready -U user --dbname=postgres' 10 | interval: 10s 11 | timeout: 5s 12 | retries: 5 13 | 14 | services: 15 | 16 | postgres-primary: 17 | <<: *x-common 18 | ports: 19 | - 5432:5432 20 | environment: 21 | POSTGRES_USER: postgres 22 | POSTGRES_DB: demo 23 | POSTGRES_PASSWORD: password 24 | POSTGRES_HOST_AUTH_METHOD: "scram-sha-256\nhost replication all 0.0.0.0/0 md5" 25 | POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256" 26 | command: | 27 | postgres 28 | -c wal_level=replica 29 | -c hot_standby=on 30 | -c max_wal_senders=10 31 | -c max_replication_slots=10 32 | -c hot_standby_feedback=on 33 | volumes: 34 | - ./00_init.sql:/docker-entrypoint-initdb.d/00_init.sql 35 | 36 | postgres-replica: 37 | <<: *x-common 38 | ports: 39 | - 5433:5432 40 | environment: 41 | PGUSER: replicator 42 | PGPASSWORD: postgres_user_for_db_read 43 | command: | 44 | bash -c " 45 | until pg_basebackup --pgdata=/var/lib/postgresql/data -R --slot=replication_slot --host=postgres-primary --port=5432 46 | do 47 | echo 'Waiting for primary to connect...' 48 | sleep 1s 49 | done 50 | echo 'Backup done, starting replica...' 51 | chmod 0700 /var/lib/postgresql/data 52 | postgres 53 | " 54 | depends_on: 55 | - postgres-primary 56 | 57 | networks: 58 | my-network: 59 | driver: bridge 60 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.2.RELEASE 9 | 10 | 11 | com.example 12 | demo 13 | 0.0.1-SNAPSHOT 14 | Spring Boot Multiple DataSource 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-jpa 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-configuration-processor 36 | 37 | 38 | 39 | org.projectlombok 40 | lombok 41 | 1.18.34 42 | provided 43 | 44 | 45 | 46 | org.postgresql 47 | postgresql 48 | runtime 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | test 55 | 56 | 57 | org.junit.vintage 58 | junit-vintage-engine 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-maven-plugin 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/DemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class DemoApplication { 8 | 9 | public static final String MODEL_PACKAGE = "com.ehsaniara.multidatasource.model"; 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(DemoApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/configurations/DataSourceConfigRead.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.configurations; 2 | 3 | import com.ehsaniara.multidatasource.DemoApplication; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import org.hibernate.jpa.HibernatePersistenceProvider; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 10 | import org.springframework.orm.jpa.JpaTransactionManager; 11 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 12 | import org.springframework.transaction.PlatformTransactionManager; 13 | import org.springframework.transaction.annotation.EnableTransactionManagement; 14 | 15 | import javax.persistence.EntityManagerFactory; 16 | 17 | 18 | /** 19 | * @author Jay Ehsaniara, Dec 30 2019 20 | */ 21 | @Configuration 22 | @ConfigurationProperties("spring.datasource-read") 23 | @EnableTransactionManagement 24 | @EnableJpaRepositories( 25 | entityManagerFactoryRef = "entityManagerFactoryRead", 26 | transactionManagerRef = "transactionManagerRead", 27 | basePackages = {"com.ehsaniara.multidatasource.repository.readRepository"} 28 | ) 29 | public class DataSourceConfigRead extends HikariConfigRead { 30 | 31 | public DataSourceConfigRead(HikariReadProperties hikariReadProperties) { 32 | super(hikariReadProperties); 33 | } 34 | 35 | @Bean 36 | public HikariDataSource dataSourceRead() { 37 | return new HikariDataSource(this); 38 | } 39 | 40 | @Bean 41 | public LocalContainerEntityManagerFactoryBean entityManagerFactoryRead( 42 | final HikariDataSource dataSourceRead) { 43 | 44 | LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); 45 | factoryBean.setDataSource(dataSourceRead); 46 | factoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); 47 | factoryBean.setPersistenceUnitName("read"); 48 | factoryBean.setPackagesToScan(DemoApplication.MODEL_PACKAGE); 49 | factoryBean.setJpaProperties(jpaReadProperties); 50 | 51 | return factoryBean; 52 | } 53 | 54 | @Bean 55 | public PlatformTransactionManager transactionManagerRead(EntityManagerFactory entityManagerFactoryRead) { 56 | return new JpaTransactionManager(entityManagerFactoryRead); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/configurations/DataSourceConfigWrite.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.configurations; 2 | 3 | import com.ehsaniara.multidatasource.DemoApplication; 4 | import com.zaxxer.hikari.HikariDataSource; 5 | import org.hibernate.jpa.HibernatePersistenceProvider; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 10 | import org.springframework.orm.jpa.JpaTransactionManager; 11 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 12 | import org.springframework.transaction.PlatformTransactionManager; 13 | import org.springframework.transaction.annotation.EnableTransactionManagement; 14 | 15 | import javax.persistence.EntityManagerFactory; 16 | 17 | 18 | /** 19 | * @author Jay Ehsaniara, Dec 30 2019 20 | */ 21 | @Configuration 22 | @ConfigurationProperties("spring.datasource-write") 23 | @EnableTransactionManagement 24 | @EnableJpaRepositories( 25 | entityManagerFactoryRef = "entityManagerFactoryWrite", 26 | transactionManagerRef = "transactionManagerWrite", 27 | basePackages = {"com.ehsaniara.multidatasource.repository.writeRepository"} 28 | ) 29 | public class DataSourceConfigWrite extends HikariConfigWrite { 30 | 31 | public DataSourceConfigWrite(HikariWriteProperties hikariWriteProperties) { 32 | super(hikariWriteProperties); 33 | } 34 | 35 | @Bean 36 | public HikariDataSource dataSourceWrite() { 37 | return new HikariDataSource(this); 38 | } 39 | 40 | @Bean 41 | public LocalContainerEntityManagerFactoryBean entityManagerFactoryWrite( 42 | final HikariDataSource dataSourceWrite) { 43 | 44 | LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean(); 45 | factoryBean.setDataSource(dataSourceWrite); 46 | factoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); 47 | factoryBean.setPersistenceUnitName("write"); 48 | factoryBean.setPackagesToScan(DemoApplication.MODEL_PACKAGE); 49 | factoryBean.setJpaProperties(jpaWriteProperties); 50 | return factoryBean; 51 | } 52 | 53 | @Bean 54 | public PlatformTransactionManager transactionManagerWrite(EntityManagerFactory entityManagerFactoryWrite) { 55 | return new JpaTransactionManager(entityManagerFactoryWrite); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/configurations/HikariConfigRead.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.configurations; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | 5 | import java.util.Properties; 6 | 7 | public class HikariConfigRead extends HikariConfig { 8 | 9 | protected final HikariReadProperties hikariReadProperties; 10 | protected final Properties jpaReadProperties; 11 | 12 | protected HikariConfigRead(HikariReadProperties hikariReadProperties) { 13 | this.hikariReadProperties = hikariReadProperties; 14 | setPoolName(this.hikariReadProperties.getPoolName()); 15 | setMinimumIdle(this.hikariReadProperties.getMinimumIdle()); 16 | setMaximumPoolSize(this.hikariReadProperties.getMaximumPoolSize()); 17 | setIdleTimeout(this.hikariReadProperties.getIdleTimeout()); 18 | 19 | Properties properties = new Properties(); 20 | properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL10Dialect"); 21 | properties.put("show-sql", "true"); 22 | this.jpaReadProperties = properties; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/configurations/HikariConfigWrite.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.configurations; 2 | 3 | import com.zaxxer.hikari.HikariConfig; 4 | 5 | import java.util.Properties; 6 | 7 | public class HikariConfigWrite extends HikariConfig { 8 | 9 | protected final HikariWriteProperties hikariWriteProperties; 10 | protected final Properties jpaWriteProperties; 11 | 12 | protected HikariConfigWrite(HikariWriteProperties hikariWriteProperties) { 13 | this.hikariWriteProperties = hikariWriteProperties; 14 | setPoolName(this.hikariWriteProperties.getPoolName()); 15 | setMinimumIdle(this.hikariWriteProperties.getMinimumIdle()); 16 | setMaximumPoolSize(this.hikariWriteProperties.getMaximumPoolSize()); 17 | setIdleTimeout(this.hikariWriteProperties.getIdleTimeout()); 18 | 19 | Properties properties = new Properties(); 20 | properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQL10Dialect"); 21 | properties.put("hibernate.hbm2ddl.auto", "update"); 22 | properties.put("hibernate.ddl-auto", "update"); 23 | properties.put("show-sql", "true"); 24 | this.jpaWriteProperties = properties; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/configurations/HikariReadProperties.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.configurations; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.PropertySource; 8 | 9 | @Setter 10 | @Getter 11 | @Configuration 12 | @PropertySource("classpath:application.yml") 13 | @ConfigurationProperties("spring.datasource-read.hikari") 14 | public class HikariReadProperties { 15 | 16 | private String poolName; 17 | 18 | private int minimumIdle; 19 | 20 | private int maximumPoolSize; 21 | 22 | private int idleTimeout; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/configurations/HikariWriteProperties.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.configurations; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.PropertySource; 8 | 9 | @Setter 10 | @Getter 11 | @Configuration 12 | @PropertySource("classpath:application.yml") 13 | @ConfigurationProperties("spring.datasource-write.hikari") 14 | public class HikariWriteProperties { 15 | 16 | private String poolName; 17 | 18 | private int minimumIdle; 19 | 20 | private int maximumPoolSize; 21 | 22 | private int idleTimeout; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/controller/CustomerControllerImpl.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.controller; 2 | 3 | import com.ehsaniara.multidatasource.handler.ResourceNotFoundException; 4 | import com.ehsaniara.multidatasource.model.Customer; 5 | import com.ehsaniara.multidatasource.service.CustomerService; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.PutMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | /** 14 | * @author Jay Ehsaniara, Dec 30 2019 15 | */ 16 | @RestController 17 | public class CustomerControllerImpl { 18 | 19 | private final CustomerService customerService; 20 | 21 | public CustomerControllerImpl(CustomerService customerService) { 22 | this.customerService = customerService; 23 | } 24 | 25 | @GetMapping("/customer/{id}") 26 | public Customer getCustomer(@PathVariable("id") Long id) { 27 | 28 | return customerService.getCustomer(id).orElseThrow(() -> new ResourceNotFoundException("Invalid Customer")); 29 | } 30 | 31 | @PostMapping("/customer") 32 | public Customer createCustomer(@RequestBody Customer customer) { 33 | return customerService.createCustomer(customer); 34 | } 35 | 36 | @PutMapping("/customer") 37 | public Customer updateCustomer(@RequestBody Customer customer) { 38 | return customerService.updateCustomer(customer); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/handler/ExceptionHandlerControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.handler; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.hibernate.exception.SQLGrammarException; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.ResponseBody; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | 13 | /** 14 | * @author Jay Ehsaniara, Dec 30 2019 15 | */ 16 | 17 | @Slf4j 18 | @ControllerAdvice 19 | public class ExceptionHandlerControllerAdvice { 20 | 21 | @ExceptionHandler(ResourceNotFoundException.class) 22 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 23 | @ResponseBody 24 | public ExceptionResponse handleResourceNotFound(final ResourceNotFoundException exception, 25 | final HttpServletRequest request) { 26 | log.error(exception.getMessage()); 27 | 28 | ExceptionResponse error = new ExceptionResponse(); 29 | error.setMessage(exception.getMessage()); 30 | error.setUrl(request.getRequestURI()); 31 | error.setStatus(HttpStatus.NOT_FOUND.value()); 32 | 33 | return error; 34 | } 35 | 36 | @ExceptionHandler(IllegalStateException.class) 37 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 38 | @ResponseBody 39 | public ExceptionResponse handleIllegalStateException(final IllegalStateException exception, 40 | final HttpServletRequest request) { 41 | log.error(exception.getMessage()); 42 | 43 | ExceptionResponse error = new ExceptionResponse(); 44 | error.setMessage(exception.getMessage()); 45 | error.setUrl(request.getRequestURI()); 46 | error.setStatus(HttpStatus.BAD_REQUEST.value()); 47 | 48 | return error; 49 | } 50 | 51 | @ExceptionHandler(IllegalArgumentException.class) 52 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 53 | @ResponseBody 54 | public ExceptionResponse handleIllegalArgumentException(final IllegalArgumentException exception, 55 | final HttpServletRequest request) { 56 | log.error(exception.getMessage()); 57 | 58 | ExceptionResponse error = new ExceptionResponse(); 59 | error.setMessage(exception.getMessage()); 60 | error.setUrl(request.getRequestURI()); 61 | error.setStatus(HttpStatus.BAD_REQUEST.value()); 62 | 63 | return error; 64 | } 65 | 66 | @ExceptionHandler(SQLGrammarException.class) 67 | @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) 68 | @ResponseBody 69 | public ExceptionResponse handleSQLGrammarException(final SQLGrammarException exception, 70 | final HttpServletRequest request) { 71 | log.error(exception.getMessage()); 72 | 73 | ExceptionResponse error = new ExceptionResponse(); 74 | error.setMessage("SQL Exception"); 75 | error.setUrl(request.getRequestURI()); 76 | error.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 77 | 78 | return error; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/handler/ExceptionResponse.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.handler; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | /** 9 | * @author Jay Ehsaniara, Dec 30 2019 10 | */ 11 | @Setter 12 | @Getter 13 | @JsonIgnoreProperties(ignoreUnknown = true) 14 | @JsonInclude(JsonInclude.Include.NON_NULL) 15 | public class ExceptionResponse { 16 | 17 | private String message; 18 | private String error; 19 | private String url; 20 | private long status; 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/handler/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.handler; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * @author Jay Ehsaniara, Dec 30 2019 8 | */ 9 | @ResponseStatus(value = HttpStatus.NOT_FOUND) 10 | public class ResourceNotFoundException extends RuntimeException { 11 | public ResourceNotFoundException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/model/Customer.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.GenerationType; 10 | import javax.persistence.Id; 11 | 12 | /** 13 | * @author Jay Ehsaniara, Dec 30 2019 14 | */ 15 | @ToString 16 | @Setter 17 | @Getter 18 | @Entity 19 | public class Customer { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.AUTO) 23 | private Long id; 24 | 25 | private String name; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.repository; 2 | 3 | import com.ehsaniara.multidatasource.model.Customer; 4 | import com.ehsaniara.multidatasource.repository.readRepository.CustomerReadRepository; 5 | import com.ehsaniara.multidatasource.repository.writeRepository.CustomerWriteRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.Optional; 9 | 10 | @Service 11 | public class CustomerRepository implements CustomerRepositoryCombo { 12 | 13 | private final CustomerReadRepository readRepository; 14 | private final CustomerWriteRepository writeRepository; 15 | 16 | public CustomerRepository(CustomerReadRepository customerReadRepository, CustomerWriteRepository customerWriteRepository) { 17 | this.readRepository = customerReadRepository; 18 | this.writeRepository = customerWriteRepository; 19 | } 20 | 21 | @Override 22 | public S save(S s) { 23 | return writeRepository.save(s); 24 | } 25 | 26 | @Override 27 | public Iterable saveAll(Iterable iterable) { 28 | return writeRepository.saveAll(iterable); 29 | } 30 | 31 | @Override 32 | public Optional findById(Long aLong) { 33 | return readRepository.findById(aLong); 34 | } 35 | 36 | @Override 37 | public boolean existsById(Long aLong) { 38 | return readRepository.existsById(aLong); 39 | } 40 | 41 | @Override 42 | public Iterable findAll() { 43 | return readRepository.findAll(); 44 | } 45 | 46 | @Override 47 | public Iterable findAllById(Iterable iterable) { 48 | return readRepository.findAllById(iterable); 49 | } 50 | 51 | @Override 52 | public long count() { 53 | return readRepository.count(); 54 | } 55 | 56 | @Override 57 | public void deleteById(Long aLong) { 58 | writeRepository.deleteById(aLong); 59 | } 60 | 61 | @Override 62 | public void delete(Customer customer) { 63 | writeRepository.delete(customer); 64 | } 65 | 66 | @Override 67 | public void deleteAll(Iterable iterable) { 68 | writeRepository.deleteAll(iterable); 69 | } 70 | 71 | @Override 72 | public void deleteAll() { 73 | writeRepository.deleteAll(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/repository/CustomerRepositoryCombo.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.repository; 2 | 3 | import com.ehsaniara.multidatasource.repository.readRepository.CustomerReadRepository; 4 | import com.ehsaniara.multidatasource.repository.writeRepository.CustomerWriteRepository; 5 | 6 | public interface CustomerRepositoryCombo extends CustomerReadRepository, CustomerWriteRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/repository/readRepository/CustomerReadRepository.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.repository.readRepository; 2 | 3 | import com.ehsaniara.multidatasource.model.Customer; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | /** 7 | * @author Jay Ehsaniara, Dec 30 2019 8 | */ 9 | public interface CustomerReadRepository extends CrudRepository { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/repository/writeRepository/CustomerWriteRepository.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.repository.writeRepository; 2 | 3 | import com.ehsaniara.multidatasource.model.Customer; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | /** 7 | * @author Jay Ehsaniara, Dec 30 2019 8 | */ 9 | public interface CustomerWriteRepository extends CrudRepository { 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/service/CustomerService.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.service; 2 | 3 | import com.ehsaniara.multidatasource.model.Customer; 4 | 5 | import java.util.Optional; 6 | 7 | /** 8 | * @author Jay Ehsaniara, Dec 30 2019 9 | */ 10 | public interface CustomerService { 11 | 12 | Optional getCustomer(Long id); 13 | 14 | Customer createCustomer(Customer customer); 15 | 16 | Customer updateCustomer(Customer customer); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/ehsaniara/multidatasource/service/CustomerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource.service; 2 | 3 | import com.ehsaniara.multidatasource.model.Customer; 4 | import com.ehsaniara.multidatasource.repository.CustomerRepository; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.util.Assert; 7 | 8 | import java.util.Optional; 9 | 10 | /** 11 | * @author Jay Ehsaniara, Dec 30 2019 12 | */ 13 | @Service 14 | public class CustomerServiceImpl implements CustomerService { 15 | 16 | private final CustomerRepository customerRepository; 17 | 18 | public CustomerServiceImpl(CustomerRepository customerRepository) { 19 | this.customerRepository = customerRepository; 20 | } 21 | 22 | public Optional getCustomer(Long id) { 23 | return customerRepository.findById(id); 24 | } 25 | 26 | public Customer createCustomer(Customer customer) { 27 | 28 | Assert.notNull(customer, "Invalid customer"); 29 | Assert.isNull(customer.getId(), "customer id should be null"); 30 | Assert.notNull(customer.getName(), "Invalid customer name"); 31 | 32 | return customerRepository.save(customer); 33 | } 34 | 35 | public Customer updateCustomer(Customer customer) { 36 | 37 | Assert.notNull(customer, "Invalid customer"); 38 | Assert.notNull(customer.getId(), "Invalid customer id"); 39 | 40 | return customerRepository.save(customer); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource-write: 3 | driver-class-name: org.postgresql.Driver 4 | jdbc-url: jdbc:postgresql://localhost:5432/demo 5 | username: postgres 6 | password: password 7 | platform: postgresql 8 | hikari: 9 | idle-timeout: 10000 10 | maximum-pool-size: 10 11 | minimum-idle: 5 12 | pool-name: WriteHikariPool 13 | 14 | datasource-read: 15 | driver-class-name: org.postgresql.Driver 16 | jdbc-url: jdbc:postgresql://localhost:5433/demo 17 | username: postgres 18 | password: password 19 | platform: postgresql 20 | hikari: 21 | idle-timeout: 10000 22 | maximum-pool-size: 10 23 | minimum-idle: 5 24 | pool-name: ReadHikariPool 25 | -------------------------------------------------------------------------------- /src/test/java/com/ehsaniara/multidatasource/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ehsaniara.multidatasource; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------