Search code examples
javaspringspring-bootmulti-modulemulti-database

What is the proper way to indicate which data source to inject into my DAOs in a multi-module multi-datasource project?


I have a project split into 3 modules (so far) - core (Model 1), user-management (model 2) and web (View and Controller). My project structure (simplified to only relevant classes for the sake of getting to the point) is as follows:

Project  
|-- core  
|  |-- src.main.java.com.romco.example  
|  |  |-- config.CoreDataSourceConfiguration  
|  |  |-- persistence.daoimpl.SomeCoreDaoImpl  
|-- user-management  
|  |-- src.main.kotlin.com.romco.example  
|  |  |-- config.UserManagementConfiguration  
|  |  |-- persistence.daoimpl.SomeUserManagementDaoImpl  
|-- web  
| // not important right now

My classes are as follows (while debugging my previous issue, I had to move some value initialization directly to code instead of using application.properties, as noted by the TODO, so please ignore it for the sake of the problem at hand)

  • CoreDataSourceConfiguration:

    @Configuration
    public class CoreDataSourceConfiguration {
    
        @Bean
        @Primary
        public DataSourceProperties coreDataSourceProperties() {
            return new DataSourceProperties();
        }
    
        //TODO values should be retrieved from application.properties
    
        @Bean(name = "coreDataSource")
        @Primary
        public DataSource coreDataSource() {
            DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
            dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
            dataSourceBuilder.url("...");
            dataSourceBuilder.username("...");
            dataSourceBuilder.password("...");
            return dataSourceBuilder.build();
        }
    
        @Bean(name = "coreTransactionManager")
        @Autowired
        DataSourceTransactionManager coreTransactionManager(@Qualifier("coreDataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }
    }

  • SomeCoreDaoImpl:

    @Repository
    public class SomeCoreDaoImpl implements SomeCoreDao {
        
        // some constants here
    
        private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
        
        @Autowired
        @Override
        public void setDataSource(DataSource dataSource) {
            namedParameterJdbcTemplate = NamedParameterJdbcTemplateHolder.get(dataSource);
        }
    
        // DB code here - create, update, etc.
        
    }

  • UserManagementConfiguration:

    @Configuration
    open class UserManagementDataSourceConfiguration {
    
        @Bean
        open fun userManagementDataSourceProperties(): DataSourceProperties {
            return DataSourceProperties()
        }
    
        @Bean(name = ["userManagementDataSource"])
        open fun userManagementDataSource(): DataSource {
            val dataSourceBuilder = DataSourceBuilder.create()
            dataSourceBuilder
                    .driverClassName("com.mysql.cj.jdbc.Driver")
                    .url("...")
                    .username("...")
                    .password("...")
            return dataSourceBuilder.build()
        }
    
        @Bean(name = ["userManagementTransactionManager"])
        @Autowired
        open fun userManagementTransactionManager(@Qualifier("userManagementDataSource") dataSource: DataSource): DataSourceTransactionManager {
            return DataSourceTransactionManager(dataSource)
        }
      }

  • SomeUserManagementDaoImpl:

    @Repository
    open class SomeUserManagementDaoImpl: SomeUserManagementDao{
    
        // constants are here
    
        private lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
    
        @Autowired
        fun setDataSource(@Qualifier("userManagementDataSource") dataSource: DataSource) {
            namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
        }
    
        // DB code here
    
    }

As you can see, the way I made it work is by specifying which bean to use in the autowired setDataSource method inside my SomeUserManagementDaoImpl class.

I would obviously prefer to avoid having to do this in every daoImpl class, and while I can think of extracting this to a single class, it doesn't seem like that's the "spring" intended solution.

Now (again, obviously) - The data sources are module-specific, and initially, I even assumed spring would somehow figure it out under the hood and, instead of using the @Primary datasource, would use the one defined in a given module (unless that module had none, in which case I assumed it would fall back to the @Primary one).
However, that was not the case, and I'm left wondering if there is some way to tell spring to use a given data source configuration across that entire module...

I've been looking at many similiar threads and guides that deal with multi-datasource projects, but I actually never found the answer. In fact, the guides which I consulted when I was implementing my multi-datasource solution never mentioned this at all (unless I missed it), eg.
https://www.baeldung.com/spring-boot-failed-to-configure-data-source
https://www.baeldung.com/spring-data-jpa-multiple-databases

It is also entirely possible that I'm doing something else terribly wrong, and that is the root cause, in which case, please, also help me out.


Solution

  • So, in case anyone stumbles upon the same problem, here's how I solved it so far. I might come up with a more elegant solution in the future, or more likely, find an issue with this one, but for now it seems to be working (haven't done much testing yet though):

    In the user-management module (the one NOT using the @Primary datasource), I created the following abstract class, extracting the dataSource injection (with the qualifier specifying the datasource) to one place:

        abstract class WithDataSource {
        
            protected lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
        
            @Autowired
            fun setDataSource(@Qualifier(USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME) dataSource: DataSource) {
                namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
            }
        
        }
    

    Each of my user-management DaoImpl classes then extends this class, and therefore implicitly implements the setDataSource() method of my GenericDao interface.

    For completeness, the user-management module now looks like this (I included some previously omitted interfaces, but still left the "example" naming and omitted some specific utility code):

        Project  
        |-- core  
        |  |-- src.main.java.com.romco.example  
        |  |  |-- config.CoreDataSourceConfiguration  
        |  |  |-- persistence.daoimpl.SomeCoreDaoImpl  
        |-- user-management  
        |  |-- src.main.kotlin.com.romco.example  
        |  |  |-- config.UserManagementConfiguration  
        |  |  |-- persistence.dao.GenericDao  
        |  |  |-- persistence.daoimpl.SomeUserManagementDaoImpl  
        |  |  |-- persistence.util.DaoUtil.kt  
        |-- web  
        | // not important right now
    
    
    • UserManagementConfiguration (added bean name as constant USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME):
    
        @Configuration
        open class UserManagementDataSourceConfiguration {
    
            companion object {
                const val USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME = "userManagementDataSource"
            }
    
            @Bean
            open fun userManagementDataSourceProperties(): DataSourceProperties {
                return DataSourceProperties()
            }
        
            @Bean(name = ["userManagementDataSource"])
            open fun userManagementDataSource(): DataSource {
                val dataSourceBuilder = DataSourceBuilder.create()
                dataSourceBuilder
                        .driverClassName("com.mysql.cj.jdbc.Driver")
                        .url("...")
                        .username("...")
                        .password("...")
                return dataSourceBuilder.build()
            }
        
            @Bean(name = ["userManagementTransactionManager"])
            @Autowired
            open fun userManagementTransactionManager(@Qualifier("userManagementDataSource") dataSource: DataSource): DataSourceTransactionManager {
                return DataSourceTransactionManager(dataSource)
            }
          }
    
    
    • GenericDao (wasn't mentioned in original quesiton, as it was not too relevant, including for completeness of solution):
        interface GenericDao<T> {
            fun setDataSource(dataSource: DataSource)
            // retrieves all
            fun retrieveAll(): Collection<T>
            // creates and returns id of the newly created record. In case of failure, returns -1.
            fun create(t: T): Long
            // updates by id, returns true if success.
            fun update(t: T): Boolean
            // deletes by id, returns true if success.
            fun delete(t: T): Boolean
            // performs cleanup, for example, might delete all test records (id < 0)
            fun cleanup()
        }
    
    
    • SomeUserManagementDao (wasn't mentioned in original quesiton, as it was not too relevant, including for completeness of solution):
        interface SomeUserManagementDao: GenericDao<SomeUserManagementClass> {
            fun retrieveBySpecificValue(specificValue: String): SomeUserManagementClass?
        }
    
    • SomeUserManagementDaoImpl (updated as mentioned in comment):
    
        @Repository
        open class SomeUserManagementDaoImpl: SomeUserManagementDao, WithDataSource() {
        
            // constants are here
        
            // namedParameterJdbcTemplate and setDataSource() are now inherited from the parent class - WithDataSource
        
            // DB code here
        
        }
    
    
    • DaoUtil.kt (contains the originally mentioned abstract class along with some other, in this case omitted, utilities):
        abstract class WithDataSource {
        
            protected lateinit var namedParameterJdbcTemplate: NamedParameterJdbcTemplate
        
            @Autowired
            fun setDataSource(@Qualifier(USER_MANAGEMENT_DATA_SOURCE_BEAN_NAME) dataSource: DataSource) {
                namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
            }
        
        }