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
@EnableJpaRepositoriesmapped 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
EntityManagerFactoryBuilderinjected, 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
@EnableJpaRepositoriesand@EntityScanannotations 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.datasourcefor master and readReplica data sources, the properties will be placed underspring.datasource.masterandspring.datasource.read-replicarespectively.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.repositoriesand then.<data_source_name>.primaryDataSourceConfig: A@DataSourceConfigannotation. 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@DataSourceConfigannotations. 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 thedataSourceConfigsattribute 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.hikarifor 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.propertiespath.
-
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-sourceas 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
@EnableMultiDataSourceConfigannotation to a configuration class in your service, and specify the relevant attributes. At a bare minimum theexactEntityPackagesandrepositoryPackagesattributes need to be specified. Ensure that you are no longer using@EnableJpaRepositoriesand@EntityScanannotations.@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
@TargetSecondaryDataSourceannotation 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/annotationsdirectory 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>DataSourceConfigand will be placed in the package specified by thegeneratedConfigPackageattribute. 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 namedReadReplicaDataSourceConfigand will be placed in the package given by thegeneratedConfigPackageattribute. -
The repositories generated by the annotation processor will be named
<DataSourceName><RepositoryName>and will be placed in the package specified by thegeneratedRepositoryPackagePrefixattribute 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-replicaand the repository name isServiceRepository, the generated repository will be namedReadReplicaServiceRepositoryand will be placed in the package given by thegeneratedRepositoryPackagePrefixattribute followed byread_replica. -
The application data source properties will need to be provided under the key
spring.datasourcefollowed 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 installto build the project and install it in your local maven repository. - Add the dependency in your project as mentioned above.
- Run
mvn clean packageto build the project and generate the jar file. - The jar file will be available in the
targetdirectory. - Add the jar file as a dependency in your project.
- Run
mvn clean compileormvn clean installin your project to generate the code. - The generated code will be available in the
target/generated-sources/annotationsdirectory. - 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/annotationsdirectory. - Remove
implements IMultiDataSourceConfigfrom the generated@Configurationclasses. - Remove the
@EnableMultiDataSourceConfigannotation from your configuration class. - Remove the
@TargetSecondaryDataSourceannotation from your repository methods. - Remove the
spring-multi-data-sourcedependency 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.