Spring Boot has multiple limitations when using multiple data sources in a single service. This project aims to solve those limitations by providing custom annotations that can be used to generate the required Bean-providing configuration classes and repositories during the build process itself, which the service can then use.
The best part is that the entirety of the generated code is clean, human-readable, and can be directly carried over to the relevant packages of the main code if you no longer wish to be tied down to this library in the future.
The limitations of using multiple data sources in a single service in Spring are:
-
We need to split the packages of repositories to allow one
@EnableJpaRepositories
mapped to one package for each data source. -
There is a lot of boilerplate config generation involved to create beans of data sources, entity managers, transaction managers etc. for each data source.
-
To get
EntityManagerFactoryBuilder
injected, we need to declare one of the data sources and all its beans as@Primary
. Otherwise, the service won't even start up.
To mitigate the above limitations, I have created two custom annotations in Java that can be used for configuring multi-data source configurations for a service. Let's break down each annotation:
-
This annotation is used to enable multi-data source configuration for the service. This will replace the
@EnableJpaRepositories
and@EntityScan
annotations used by Spring. -
It can be applied to a class (target:
ElementType.TYPE
). -
It has the following attributes:
exactEntityPackages
: An array of exact packages to scan for entities. These packages are scanned to find the entities related to the data sources.repositoryPackages
: An array of packages to scan for repositories. These packages are scanned to find the repositories related to the data sources.datasourcePropertiesPrefix
: The prefix of the data source properties in the application properties file. The properties for each data source will be placed under this prefix followed by the kebab case of the data source name. Eg. When set asspring.datasource
for master and readReplica data sources, the properties will be placed underspring.datasource.master
andspring.datasource.read-replica
respectively.generatedConfigPackage
: The package where the generated data source configs will be placed. The generated config class with relevant beans will follow a specific naming format. If this is not specified, the generated config will be placed in the same package as the class where this annotation is applied, followed by.generated.config
.generatedRepositoryPackagePrefix
: The prefix of the package where the generated copies of the repositories will be placed. The generated repositories will follow a specific naming format. If this is not specified, the generated repositories will be placed in the same package as the class where this annotation is applied, followed by.generated.repositories
and then.<data_source_name>
.primaryDataSourceConfig
: A@DataSourceConfig
annotation. This annotation represents the primary data source and its configuration. The primary data source will be able to access every repository other than the repositories generated for the secondary data sources.secondaryDataSourceConfigs
: An array of@DataSourceConfig
annotations. Each annotation represents a data source and its configuration. The secondary data sources will only be able to access the repositories generated for them.
-
This sub-annotation is used to configure a data source and its properties for
@EnableMultiDataSourceConfig
. It can not be applied directly anywhere other than in thedataSourceConfigs
attribute of@EnableMultiDataSourceConfig
. -
It has the following attributes:
dataSourceName
: The name of the data source. It is used to generate the data source beans and to name the generated classes, packages, and property paths for the data source properties.dataSourceClassPropertiesPath
:The application properties key/path of the data source class' properties. Eg.spring.datasource.hikari
for Hikari data sources.overridingPropertiesPath
: The application properties key/path under which the JPA properties to override for this data source are located. This allows overriding of the JPA properties for each data source. By default, it will take the defaultspring.jpa.properties
path.
-
This annotation is used to create copies of repositories in relevant packages and autoconfigure them to use the relevant data sources.
-
It can be applied to a method (target:
ElementType.METHOD
). -
It has the following attributes:
dataSourceName
(orvalue
): The name of the data source to use for the repository.
Both annotations are available at the source level and are not retained at runtime. They are intended to be used for generating code for configuring data sources during the build process.
-
Add
spring-multi-data-source
as a dependency in your service with a scope ofprovided
. Eg. for Maven:<dependency> <groupId>com.dhi13man.spring</groupId> <artifactId>spring-multi-data-source</artifactId> <version>${desired.version}</version> <scope>provided</scope> </dependency>
-
Add the
@EnableMultiDataSourceConfig
annotation to a configuration class in your service, and specify the relevant attributes. At a bare minimum theexactEntityPackages
andrepositoryPackages
attributes need to be specified. Ensure that you are no longer using@EnableJpaRepositories
and@EntityScan
annotations.@Configuration @EnableMultiDataSourceConfig( repositoryPackages = { "com.sample" }, primaryDataSourceConfig = @DataSourceConfig( dataSourceName = "master", exactEntityPackages = { "com.sample.project.sample_service.entities.mysql", // Assuming master wants access to read entities as well. If not, above package is fine "com.sample.project.sample_service.read_entities.mysql", "com.sample.project.sample_service.read_entities_v2.mysql" }, // In example application properties below (Usage Step 7), extra JPA Properties specific to this data source are provided under this key overridingPropertiesPath = "spring.datasource.master.extra-properties" ), secondaryDataSourceConfigs = { @DataSourceConfig( dataSourceName = "read-replica", exactEntityPackages = "com.sample.project.sample_service.read_entities.mysql" ), @DataSourceConfig( dataSourceName = "replica-2", exactEntityPackages = { "com.sample.project.sample_service.read_entities.mysql", // Assuming replica-2 wants access to read entities as well as read entities v2 "com.sample.project.sample_service.read_entities_v2.mysql" } ), } ) public class ServiceConfig { }
-
Add the
@TargetSecondaryDataSource
annotation to the repository methods that need to be configured for a specific data source, and specify the data source name.@Repository public interface ServiceRepository extends JpaRepository<ServiceEntity, Long> { @TargetSecondaryDataSource("read-replica") ServiceEntity findByCustomIdAndDate(String id, Date date); // To override the default JpaRepository methods in the generated repository // All base methods that have not been overridden along with this annotation will throw an // UnsupportedOperationException. @TargetSecondaryDataSource("read-replica") @Override ServiceEntity getById(Long id); }
-
Build the service and the generated classes will become available in the
target/generated-sources/annotations
directory of the service. Add that folder as a generated sources root in your IDE. -
The configuration classes generated by the annotation processor will be named
<DataSourceName>DataSourceConfig
and will be placed in the package specified by thegeneratedConfigPackage
attribute. These classes will provide the beans for the data source, transaction manager, entity manager factory, etc. for each data source which can be easily autowired with the given name constants.For example, if the data source name is
read-replica
, the generated configuration class will be namedReadReplicaDataSourceConfig
and will be placed in the package given by thegeneratedConfigPackage
attribute. -
The repositories generated by the annotation processor will be named
<DataSourceName><RepositoryName>
and will be placed in the package specified by thegeneratedRepositoryPackagePrefix
attribute followed by the snake case of the data source name. These repositories will be configured to use the relevant data source and can be autowired with the given name constants.For example, if the data source name is
read-replica
and the repository name isServiceRepository
, the generated repository will be namedReadReplicaServiceRepository
and will be placed in the package given by thegeneratedRepositoryPackagePrefix
attribute followed byread_replica
. -
The application data source properties will need to be provided under the key
spring.datasource
followed by the kebab case of the data source name.spring: datasource: master: # This will become the master data source property as opposed to the usual direct spring.datasource property driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${DB_IP}:${DB_PORT}/${MASTER_DB_NAME} username: ${DB_USERNAME} password: ${DB_PASSWORD} type: com.zaxxer.hikari.HikariDataSource extra-properties: # Made possible by overridingPropertiesPath in Step 2 for master data source. hibernate.generate_statistics: true # Generate hibernate statistics only for master data source. read-replica: # This will become the property for the kebab case of the secondary data source name driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${READ_REPLICA_DB_IP}:${DB_PORT}/${READ_REPLICA_DB_NAME} username: ${DB_USERNAME} password: ${DB_PASSWORD} type: com.zaxxer.hikari.HikariDataSource jpa: # Global JPA Properties properties: # Hibernate properties can only be picked from here when using multiple data sources. hibernate.generate_statistics: false hibernate.physical_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy hibernate.implicit_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
-
Please always go through the generated code to learn more about what configs to give and what beans to use for each data source.
- Clone the repository.
- Run
mvn clean install
to build the project and install it in your local maven repository. - Add the dependency in your project as mentioned above.
- Run
mvn clean package
to build the project and generate the jar file. - The jar file will be available in the
target
directory. - Add the jar file as a dependency in your project.
- Run
mvn clean compile
ormvn clean install
in your project to generate the code. - The generated code will be available in the
target/generated-sources/annotations
directory. - Add that directory as a generated sources root in your IDE.
- Use the generated code as mentioned above.
A big selling point of this library is that it is not a black box. The generated code is clean, human-readable, and can be directly carried over to the relevant packages of the main code if you no longer wish to be tied down to this library.
- Move the generated configuration classes and repositories to the relevant packages in your
project from the
target/generated-sources/annotations
directory. - Remove
implements IMultiDataSourceConfig
from the generated@Configuration
classes. - Remove the
@EnableMultiDataSourceConfig
annotation from your configuration class. - Remove the
@TargetSecondaryDataSource
annotation from your repository methods. - Remove the
spring-multi-data-source
dependency from your project pom.
And that's all you have to do! You are no longer tied down to this library and have the freedom to use and modify the generated code to your liking.
Please feel free to raise issues and submit pull requests. Please check out CONTRIBUTING.md for more details.
This project is licensed under the GNU Lesser General Public License v3.0. Please check out LICENSE for more details.