23

I need to set up liquibase for two datasources in Spring, at the moment it seems that only one liquibase set up is possible and you can choose for which data source.

Pradeep
  • 9,667
  • 13
  • 27
  • 34
Tatiana Totskaya
  • 233
  • 1
  • 2
  • 5

5 Answers5

47

If you are using spring boot, here is the setup which can help you:

Configuration class:

@Configuration
public class DatasourceConfig {

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "datasource.primary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "datasource.primary.liquibase")
    public LiquibaseProperties primaryLiquibaseProperties() {
        return new LiquibaseProperties();
    }

    @Bean
    public SpringLiquibase primaryLiquibase() {
        return springLiquibase(primaryDataSource(), primaryLiquibaseProperties());
    }

    @Bean
    @ConfigurationProperties(prefix = "datasource.secondary")
    public DataSource secondaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "datasource.secondary.liquibase")
    public LiquibaseProperties secondaryLiquibaseProperties() {
        return new LiquibaseProperties();
    }

    @Bean
    public SpringLiquibase secondaryLiquibase() {
        return springLiquibase(secondaryDataSource(), secondaryLiquibaseProperties());
    }

    private static SpringLiquibase springLiquibase(DataSource dataSource, LiquibaseProperties properties) {
        SpringLiquibase liquibase = new SpringLiquibase();
        liquibase.setDataSource(dataSource);
        liquibase.setChangeLog(properties.getChangeLog());
        liquibase.setContexts(properties.getContexts());
        liquibase.setDefaultSchema(properties.getDefaultSchema());
        liquibase.setDropFirst(properties.isDropFirst());
        liquibase.setShouldRun(properties.isEnabled());
        liquibase.setLabels(properties.getLabels());
        liquibase.setChangeLogParameters(properties.getParameters());
        liquibase.setRollbackFile(properties.getRollbackFile());
        return liquibase;
    }


...

}

properties.yml

datasource:
  primary:
    url: jdbc:mysql://localhost/primary
    username: username
    password: password
    liquibase:
      change-log: classpath:/db/changelog/db.primary.changelog-master.xml
  secondary:
    url: jdbc:mysql://localhost/secondary
    username: username
    password: password
    liquibase:
      change-log: classpath:/db/changelog/db.secondary.changelog-master.xml
dmytro
  • 1,141
  • 11
  • 13
  • This works nicely when adding @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) to Application class, but I am using h2 on local, and data file is not loaded anymore with auto configuration disabled. How can I force spring to load data file without schema file? – Tatiana Totskaya Apr 24 '17 at 09:02
  • What kind of data file do you have? – dmytro Apr 25 '17 at 09:25
  • data-h2.sql file – Tatiana Totskaya Apr 26 '17 at 12:57
  • Actually, the solution you provided was enough. – Tatiana Totskaya Apr 27 '17 at 13:34
  • Thanks! Regarding your question - it depends. If you use Hibernate, try to rename data-h2.sql to import.sql. In case Hibernate property ddl-auto is set to create or create-drop (the last is default for embedded DB), script must be picked up by Hibernate. The easiest solution that comes to my mind is to put script content into liquibase migration script in tags. You can explicitly set liquibase.context property in property files (dev, test, prod) for each environment and specify it also as change set attribute to avoid execution of the script in production f.e. – dmytro Apr 27 '17 at 14:12
  • Hmm I get the following error: Caused by: org.postgresql.util.PSQLException: ERROR: permission denied for relation databasechangeloglock and Caused by: liquibase.exception.DatabaseException: Error executing SQL UPDATE public.databasechangeloglock SET LOCKED = TRUE, LOCKEDBY = 'WS-3PM85L2 (10.0.75.1)', LOCKGRANTED =. Do you know where is the problem? This error occurs only if I add the secondary database – Matley Apr 25 '19 at 11:52
  • Hey there! I would assume, that it has relation to the database itself: either permission issue, or maybe you try to use same database instance for both liquibase instances. Or you keep default `spring.datasource` and/or liquibase properties and some mess is going on there. If permissions and database urls are ok, try to exclude DataSourceAutoConfiguration and LiquibaseAutoConfiguration in @SpringBootApplication – dmytro Apr 25 '19 at 12:23
  • Works perfectly while running. Not working while testing. Posted an question https://stackoverflow.com/questions/61123061/tests-not-working-for-multiple-datasource-with-spring-boot-liquibase – Akshay Apr 09 '20 at 14:08
  • This is a good answer, but you should not be directly calling `@Bean` annotated methods - you should be using dependency injection (with named beans and @Qualifiers) in the methods instead of calling the @Bean annotated methods. – mikeb Oct 04 '20 at 11:37
  • Thanks, @mikeb. I would say it is a matter of preference, while bean scope is singleton both approaches work the same way. Having `@Beans` injected would look cleaner, but adds long method arguments with `@Qualifiers`. – dmytro Oct 06 '20 at 08:55
  • I'd say ditch the `@Bean` annotation if you're not going to inject it, otherwise Spring will create 1 for that and you're creating 1 "outside" of Spring by calling the method... – mikeb Oct 06 '20 at 15:59
  • It doesn't create a new instance on method call inside the @Configuration class thanks to spring magic. This answer explains that nicely https://stackoverflow.com/a/27990468/2739813 – dmytro Oct 07 '20 at 08:43
  • This worked perfectly for me :) Thank you @dmytro – MattWeiler Sep 28 '21 at 16:25
10

I've done a project that I can create multiple dataSources with your specific changeSets, so if you need to add another dataSource, it would just change your application.yml, no longer needing to change the code.

Configuration class

@Configuration
@ConditionalOnProperty(prefix = "spring.liquibase", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(LiquibaseProperties.class)
@AllArgsConstructor
public class LiquibaseConfiguration {

    private LiquibaseProperties properties;
    private DataSourceProperties dataSourceProperties;

    @Bean
    @DependsOn("tenantRoutingDataSource")
    public MultiTenantDataSourceSpringLiquibase liquibaseMultiTenancy(Map<Object, Object> dataSources,
                                                                      @Qualifier("taskExecutor") TaskExecutor taskExecutor) {
        // to run changeSets of the liquibase asynchronous
        MultiTenantDataSourceSpringLiquibase liquibase = new MultiTenantDataSourceSpringLiquibase(taskExecutor);
        dataSources.forEach((tenant, dataSource) -> liquibase.addDataSource((String) tenant, (DataSource) dataSource));
        dataSourceProperties.getDataSources().forEach(dbProperty -> {
            if (dbProperty.getLiquibase() != null) {
                liquibase.addLiquibaseProperties(dbProperty.getTenantId(), dbProperty.getLiquibase());
            }
        });

        liquibase.setContexts(properties.getContexts());
        liquibase.setChangeLog(properties.getChangeLog());
        liquibase.setDefaultSchema(properties.getDefaultSchema());
        liquibase.setDropFirst(properties.isDropFirst());
        liquibase.setShouldRun(properties.isEnabled());
        return liquibase;
    }

}

application.yml

spring:
  dataSources:
    - tenantId: db1
      url: jdbc:postgresql://localhost:5432/db1
      username: postgres
      password: 123456
      driver-class-name: org.postgresql.Driver
      liquibase:
        enabled: true
        default-schema: public
        change-log: classpath:db/master/changelog/db.changelog-master.yaml
    - tenantId: db2
      url: jdbc:postgresql://localhost:5432/db2
      username: postgres
      password: 123456
      driver-class-name: org.postgresql.Driver
    - tenantId: db3
      url: jdbc:postgresql://localhost:5432/db3
      username: postgres
      password: 123456
      driver-class-name: org.postgresql.Driver

  Link of repository: https://github.com/dijalmasilva/spring-boot-multitenancy-datasource-liquibase

Dijalma Silva
  • 1,478
  • 1
  • 7
  • 7
4

I was in the need of supporting a dynamic amount of DataSources, not a fixed number of them. I found that you can use the same SpringLiquibase bean for multiple DataSources by making a service like this:

@Service
@DependsOn("liquibase")
public class LiquibaseService {

    @Autowired
    @Qualifier("liquibase")
    private SpringLiquibase liquibase;

    @PostConstruct
    public void initialize() {

        /* Obtain datasources from wherever. I obtain them from a master DB. It's up to you. */
        List<DataSource> dataSources = obtainDataSources();

        for (DataSource dataSource : dataSources) {
            try {
                liquibase.setDataSource(dataSource);
                liquibase.setChangeLog("classpath:liquibase/emp.changelog.xml");
                liquibase.setShouldRun(true);

                // This runs Liquibase
                liquibase.afterPropertiesSet();

            } catch (LiquibaseException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

}

For this to work, you should have a SpringLiquibase bean declared somewhere. In this example, I got this in one of my configuration files:

@Bean
public SpringLiquibase liquibase(LiquibaseProperties properties) {

    SpringLiquibase liquibase = new SpringLiquibase();
    liquibase.setDataSource(systemDataSource);
    liquibase.setChangeLog("classpath:liquibase/sis.changelog.xml");
    liquibase.setContexts(properties.getContexts());
    liquibase.setDefaultSchema(properties.getDefaultSchema());
    liquibase.setDropFirst(properties.isDropFirst());


    liquibase.setLabels(properties.getLabels());
    liquibase.setChangeLogParameters(properties.getParameters());
    liquibase.setRollbackFile(properties.getRollbackFile());

    // This is because we are running the process manually. Don't let SpringLiquibase do it.
    liquibase.setShouldRun(false);

    return liquibase;
}

The above highly depends on your DataSource configuration requirements. You could also need to put this on your main Application class so the Spring-Liquibase auto-configuration doesn't kick in:

@SpringBootApplication(exclude = {
    LiquibaseAutoConfiguration.class
})
public class Application {

    // Stuff...

}
David Rochin
  • 317
  • 3
  • 11
  • ` List dataSources = obtainDataSources()` can you elaborate a bit more on this. Can i use my application.properties file where i have defined multiple datasources. And my second question would be why are you setting the same changelog, what if I want to use different changelog for each datasource. I know stackoverflow does not like when we ask questions inside comments, but I guess its more of a clarification so I am asking inside comments. – aditya_sharma Jun 15 '22 at 05:08
  • Also if you have a Github repository for this, it would be really helpful to understand the overall flow. – aditya_sharma Jun 15 '22 at 05:17
  • 1. I think the implementation of `obtainDataSources()` goes way beyond the scope of this question/answer. There could literally exist 1 million ways to implement it, and which one is for you entirely depends on your requirements. I recommend that you make 1 or 2 separate StackOverflow questions for that. 2. I'm using the same changelog because that is my requirements. If you want to use different changelogs for each DB, then you will have to adapt this code to fit what you want to do. That will probably require making multiple SpringLiquibase beans, 1 for each type of changelog. – David Rochin Jun 15 '22 at 16:58
  • 1
    Thank you @David, I had the same requirement as yours and just removed (`liquibase.setDatasource(systemDatasource)`) this and tried it with my project and it worked really well. There wasn't anything wrong with it, I just removed it according to my requirement. Thanks – aditya_sharma Jul 04 '22 at 16:37
2

Just have 2 datasources and 2 beans

<bean id="liquibase1" class="liquibase.integration.spring.SpringLiquibase">
      <property name="dataSource" ref="dataSource1" />
      <property name="changeLog" value="classpath:db1-changelog.xml" />
 </bean>
 <bean id="liquibase2" class="liquibase.integration.spring.SpringLiquibase">
      <property name="dataSource" ref="dataSource2" />
      <property name="changeLog" value="classpath:db2-changelog.xml" />
 </bean>
Essex Boy
  • 7,565
  • 2
  • 21
  • 24
  • 1
    What if we need to run scripts in a mixed order? I our case we need to run one script from dataSource1 then dataSource2, then again dataSource1 ... – mcvkr May 01 '17 at 13:47
0

You can also run multiple liquibase instance (i.e. not only limit to a primary and a secondary).

e.g. Your configuration java can have:

@Bean
@ConfigurationProperties(prefix = "liquibase1")
...
@Bean
@ConfigurationProperties(prefix = "liquibase2")
...
@Bean
@ConfigurationProperties(prefix = "liquibase3")

Your application.property can have:

liquibase1.default-schema=schemaA
...
liquibase2.default-schema=schemaB
...
liquibase3.default-schema=schemaC
...

And (excitingly), these springLiquibase instances can use the same Datasource, or different DataSource... however you like it.

Running order? I haven't found any official document, from my observation in debug, all liquibase migration runs according to the order you write in application.properties. Those who wants to run migration in one datasource, then go to another datasource, then come back to this datasource and run something else,,, you may want to try this multiple liquibase instance approach.

Sherry
  • 1