基于更改日志更新租户模式的 Liquibase 多租户问题

Liqubase multitenacy issue on tenant schema update based on change log

作为一项要求,我有一个 spring 引导项目,该项目使用基于模式的多租户,当我 运行 应用程序迁移在主模式(public)上运行良好时,但是当它尝试将更改应用于所有租户(其他方案)时,它 returns 一个例外,即使模式为空,sql 脚本中的 table 已经存在:

18:15:31.006 [main] TRACE o.s.c.i.s.SpringFactoriesLoader - Loaded [org.springframework.boot.diagnostics.FailureAnalysisReporter] names: [org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter]
18:15:31.007 [main] ERROR o.s.boot.SpringApplication - Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tenantLiquibase' defined in class path resource [com/*/dao/multitenancy/TenantLiquibaseConfig.class]: Invocation of init method failed; nested exception is liquibase.exception.MigrationFailedException: Migration failed for change set sql/changelog/tenant/db.changelog-tenant-1.0.yaml::v20_tenant_ddl::vsuruceanu:
     Reason: liquibase.exception.DatabaseException: ERROR: relation "property" already exists
  Location: File: heap.c, Routine: heap_create_with_catalog, Line: 1162
  Server SQLState: 42P07 [Failed SQL: (0) create table property
(
    property_id                         bigserial not null
        constraint property_pk
            primary key,
    name                                varchar(100),
    address                             varchar(300),
    sticky_note                         varchar,
    expected_roi                        numeric,
    profile                             varchar(200),
    nickname                            varchar(200),
    condo                               varchar(200),
    condo_yearly_fees                   numeric,
    mortage_ammount                     numeric,
    closing_cost_ammount                numeric,
    earthquake_supplies_inventory       varchar(1000),
    earthquake_supplies_good_until_date date,
    ownership_type                      public.property_ownership_type,
    purchase_date                       date,
    purchase_amount                     numeric,
    purchase_vat                        numeric,
    purchase_solicitors                 varchar(1000)
)]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1794)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean[=10=](AbstractBeanFactory.java:324)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:897)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:879)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:551)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
    at com.*.api.ApiApplication.main(ApiApplication.java:21)
Caused by: liquibase.exception.MigrationFailedException: Migration failed for change set sql/changelog/tenant/db.changelog-tenant-1.0.yaml::v20_tenant_ddl::vsuruceanu:
     Reason: liquibase.exception.DatabaseException: ERROR: relation "property" already exists
  Location: File: heap.c, Routine: heap_create_with_catalog, Line: 1162
  Server SQLState: 42P07 [Failed SQL: (0) create table property
(
    property_id                         bigserial not null
        constraint property_pk
            primary key,
    name                                varchar(100),
    address                             varchar(300),
    sticky_note                         varchar,
    expected_roi                        numeric,
    profile                             varchar(200),
    nickname                            varchar(200),
    condo                               varchar(200),
    condo_yearly_fees                   numeric,
    mortage_ammount                     numeric,
    closing_cost_ammount                numeric,
    earthquake_supplies_inventory       varchar(1000),
    earthquake_supplies_good_until_date date,
    ownership_type                      public.property_ownership_type,
    purchase_date                       date,
    purchase_amount                     numeric,
    purchase_vat                        numeric,
    purchase_solicitors                 varchar(1000)
)]
    at liquibase.changelog.ChangeSet.execute(ChangeSet.java:646)
    at liquibase.changelog.visitor.UpdateVisitor.visit(UpdateVisitor.java:53)
    at liquibase.changelog.ChangeLogIterator.run(ChangeLogIterator.java:83)
    at liquibase.Liquibase.update(Liquibase.java:202)
    at liquibase.Liquibase.update(Liquibase.java:179)
    at liquibase.integration.spring.SpringLiquibase.performUpdate(SpringLiquibase.java:366)
    at liquibase.integration.spring.SpringLiquibase.afterPropertiesSet(SpringLiquibase.java:314)
    at com.*.dao.multitenancy.liquibase.DynamicSchemaBasedMultiTenantSpringLiquibase.runOnAllSchemas(DynamicSchemaBasedMultiTenantSpringLiquibase.java:59)
    at com.*.dao.multitenancy.liquibase.DynamicSchemaBasedMultiTenantSpringLiquibase.afterPropertiesSet(DynamicSchemaBasedMultiTenantSpringLiquibase.java:52)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1853)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1790)
    ... 17 common frames omitted
Caused by: liquibase.exception.DatabaseException: ERROR: relation "property" already exists
  Location: File: heap.c, Routine: heap_create_with_catalog, Line: 1162
  Server SQLState: 42P07 [Failed SQL: (0) create table property
(
    property_id                         bigserial not null
        constraint property_pk
            primary key,
    name                                varchar(100),
    address                             varchar(300),
    sticky_note                         varchar,
    expected_roi                        numeric,
    profile                             varchar(200),
    nickname                            varchar(200),
    condo                               varchar(200),
    condo_yearly_fees                   numeric,
    mortage_ammount                     numeric,
    closing_cost_ammount                numeric,
    earthquake_supplies_inventory       varchar(1000),
    earthquake_supplies_good_until_date date,
    ownership_type                      public.property_ownership_type,
    purchase_date                       date,
    purchase_amount                     numeric,
    purchase_vat                        numeric,
    purchase_solicitors                 varchar(1000)
)]
    at liquibase.executor.jvm.JdbcExecutor$ExecuteStatementCallback.doInStatement(JdbcExecutor.java:402)
    at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:59)
    at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:131)
    at liquibase.database.AbstractJdbcDatabase.execute(AbstractJdbcDatabase.java:1276)
    at liquibase.database.AbstractJdbcDatabase.executeStatements(AbstractJdbcDatabase.java:1258)
    at liquibase.changelog.ChangeSet.execute(ChangeSet.java:609)
    ... 27 common frames omitted
Caused by: org.postgresql.util.PSQLException: ERROR: relation "property" already exists
  Location: File: heap.c, Routine: heap_create_with_catalog, Line: 1162
  Server SQLState: 42P07
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2532)
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2267)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:312)
    at org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:448)
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:369)
    at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:310)
    at org.postgresql.jdbc.PgStatement.executeCachedSql(PgStatement.java:296)
    at org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:273)
    at org.postgresql.jdbc.PgStatement.execute(PgStatement.java:268)
    at com.zaxxer.hikari.pool.ProxyStatement.execute(ProxyStatement.java:95)
    at com.zaxxer.hikari.pool.HikariProxyStatement.execute(HikariProxyStatement.java)
    at liquibase.executor.jvm.JdbcExecutor$ExecuteStatementCallback.doInStatement(JdbcExecutor.java:398)
    ... 32 common frames omitted
Disconnected from the target VM, address: '127.0.0.1:36551', transport: 'socket'

数据库架构为空。 Here is the screenshot of the database structure

这是我的 pom 文件:

<properties>
        <spring.datasource.driverClassName>org.postgresql.Driver</spring.datasource.driverClassName>
        <spring.datasource.url>jdbc:postgresql://localhost:5432/database1</spring.datasource.url>
        <spring.datasource.username>*</spring.datasource.username>
        <spring.datasource.password>*</spring.datasource.password>
        <spring.jpa.properties.hibernate.dialect>org.hibernate.dialect.PostgreSQL95Dialect</spring.jpa.properties.hibernate.dialect>
        <spring.jpa.properties.hibernate.multiTenancy>SCHEMA</spring.jpa.properties.hibernate.multiTenancy>
        <spring.jpa.properties.hibernate.multi_tenant_connection_provider>com.*.api.multitenancy.SchemaMultiTenantConnectionProvider</spring.jpa.properties.hibernate.multi_tenant_connection_provider>
        <spring.jpa.properties.hibernate.tenant_identifier_resolver>com.*.api.multitenancy.TenantIdentifierResolver</spring.jpa.properties.hibernate.tenant_identifier_resolver>
        <repoDirectory>repo</repoDirectory>
        <buildDirectory>${project.basedir}/target</buildDirectory>
    </properties>

    <repositories>
        <repository>
            <id>repo</id>
            <url>file://${project.basedir}/${repoDirectory}</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>com.vladmihalcea</groupId>
            <artifactId>hibernate-types-52</artifactId>
            <version>2.9.7</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-commons</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.4.20.Final</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.11.2</version>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>postgresql</artifactId>
            <version>1.15.1</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.14</version>
        </dependency>
        <dependency>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-core</artifactId>
            <version>4.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.snakeyaml</groupId>
            <artifactId>snakeyaml-engine</artifactId>
            <version>2.2.1</version>
        </dependency>
    </dependencies>

这里是application.properties:

## PostgreSQL

spring.datasource.driverClassName=@spring.datasource.driverClassName@
spring.datasource.url=@spring.datasource.url@
spring.datasource.username=@spring.datasource.username@
spring.datasource.password=@spring.datasource.password@

spring.jpa.properties.hibernate.dialect=@spring.jpa.properties.hibernate.dialect@
spring.jpa.properties.hibernate.multiTenancy=@spring.jpa.properties.hibernate.multiTenancy@
spring.jpa.properties.hibernate.multi_tenant_connection_provider=@spring.jpa.properties.hibernate.multi_tenant_connection_provider@
spring.jpa.properties.hibernate.tenant_identifier_resolver=@spring.jpa.properties.hibernate.tenant_identifier_resolver@
logging.level.liquibase = DEBUG

multitenancy.schema-cache.maximumSize=100
multitenancy.schema-cache.expireAfterAccess=10
multitenancy.master.repository.packages=com.*.dao.repositories.shared
multitenancy.master.entityManager.packages=com.*.dao.config.SharedConfiguration
multitenancy.master.liquibase.enabled=true
multitenancy.master.liquibase.changeLog=classpath:/sql/changelog/public/db.changelog-public.yaml
multitenancy.tenant.repository.packages=com.*.dao.repositories.shared.SubscriptionRepository
multitenancy.tenant.entityManager.packages=com.*.dao.config.MultitenancyConfiguration
multitenancy.tenant.liquibase.changeLog=classpath:/sql/changelog/tenant/db.changelog-tenant.yaml

public 架构 (LiquibaseConfig) 的 Liquibase 配置:

@Lazy(false)
@Configuration
@ConditionalOnProperty(name = "multitenancy.master.liquibase.enabled", havingValue = "true", matchIfMissing = true)
public class LiquibaseConfig {

    @Bean
    @ConfigurationProperties("multitenancy.master.liquibase")
    public LiquibaseProperties masterLiquibaseProperties() {
        return new LiquibaseProperties();
    }

    @Bean
    public SpringLiquibase masterLiquibase(ObjectProvider<DataSource> liquibaseDataSource) {
        LiquibaseProperties liquibaseProperties = masterLiquibaseProperties();
        SpringLiquibase liquibase = new SpringLiquibase();
        liquibase.setDataSource(liquibaseDataSource.getIfAvailable());
        liquibase.setChangeLog(liquibaseProperties.getChangeLog());
        liquibase.setContexts(liquibaseProperties.getContexts());
        liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema());
        liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema());
        liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace());
        liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable());
        liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable());
        liquibase.setDropFirst(liquibaseProperties.isDropFirst());
        liquibase.setShouldRun(liquibaseProperties.isEnabled());
        liquibase.setLabels(liquibaseProperties.getLabels());
        liquibase.setChangeLogParameters(liquibaseProperties.getParameters());
        liquibase.setRollbackFile(liquibaseProperties.getRollbackFile());
        liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate());
        return liquibase;
    }

}

这是动态多租户配置:

@Lazy(false)
@Configuration
@ConditionalOnProperty(name = "multitenancy.tenant.liquibase.enabled", havingValue = "true", matchIfMissing = true)
public class TenantLiquibaseConfig {

    @Bean
    @ConfigurationProperties("multitenancy.tenant.liquibase")
    public LiquibaseProperties tenantLiquibaseProperties() {
        return new LiquibaseProperties();
    }

    @Bean
    public DynamicSchemaBasedMultiTenantSpringLiquibase tenantLiquibase() {
        return new DynamicSchemaBasedMultiTenantSpringLiquibase();
    }

}

DynamicSchemaBasedMultiTenantSpringLiquibase:

public class DynamicSchemaBasedMultiTenantSpringLiquibase implements InitializingBean, ResourceLoaderAware {

    @Autowired
    private SubscriptionRepository masterTenantRepository;

    @Autowired
    private DataSource dataSource;

    @Autowired
    @Qualifier("tenantLiquibaseProperties")
    private LiquibaseProperties liquibaseProperties;

    private ResourceLoader resourceLoader;

    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    private static Logger logger = LoggerFactory.getLogger(TenantContext.class.getName());

    @Override
    public void afterPropertiesSet() throws Exception {
        logger.info("Schema based multitenancy enabled");
        this.runOnAllSchemas(dataSource, masterTenantRepository.findAll());
    }

    protected void runOnAllSchemas(DataSource dataSource, Collection<Subscription> tenants) throws LiquibaseException, SQLException {
        for(Subscription tenant : tenants) {
            logger.info("Initializing Liquibase for tenant {} schemaName: {}", tenant.getSubscriptionId(), tenant.getSchemaName());
            SpringLiquibase liquibase = this.getSpringLiquibase(dataSource, tenant.getSchemaName());
            liquibase.afterPropertiesSet();
            logger.info("Liquibase ran for tenant {}", tenant.getSchemaName());
        }
    }

    protected SpringLiquibase getSpringLiquibase(DataSource dataSource, String schema) throws SQLException {

        SpringLiquibase liquibase = new SpringLiquibase();
        liquibase.setResourceLoader(getResourceLoader());
        liquibase.setDataSource(dataSource);
        liquibase.setDefaultSchema(schema);
        liquibase.setChangeLog(liquibaseProperties.getChangeLog());
        liquibase.setContexts(liquibaseProperties.getContexts());
        liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema());
        liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace());
        liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable());
        liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable());
        liquibase.setDropFirst(liquibaseProperties.isDropFirst());
        liquibase.setShouldRun(liquibaseProperties.isEnabled());
        liquibase.setLabels(liquibaseProperties.getLabels());
        liquibase.setChangeLogParameters(liquibaseProperties.getParameters());
        liquibase.setRollbackFile(liquibaseProperties.getRollbackFile());
        liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate());
        return liquibase;
    }

}

变更集:

databaseChangeLog:

- changeSet:
    id: v20_tenant_ddl
    author: admin
    changes:
        sqlFile:
            encoding: utf8
            path: sql/snaphots/v20_tenant_ddl.sql

我使用:

Spring Boot 2.3.2
Postgresql 12
Maven 3.6.3
Liquibase 4.3.1

经过几天的研究,我得出的结论是 Liquibase 4.3.1 在使用多租户功能时并不完全支持 sqlFile。作为修复,我已将 .sql 脚本重写为 .yaml 更改日志格式:

databaseChangeLog:

  - changeSet:
      id: init
      author: Valentin
      changes:
        - createSequence:
            sequenceName: property_property_id_seq

        - createTable:
            tableName: property
            columns:
              - column:
                  name: property_id
                  type: bigserial
                  defaultValueSequenceNext: property_property_id_seq
                  constraints:
                    nullable: false
                    primaryKey: true
                    primaryKeyName: property_pk
              - column:
                  name: name
                  type: character varying(100)
              - column:
                  name: address
                  type: character varying(300)
                  constraints:
                    nullable: false

              ...

              - column:
                  name: zip
                  type: character varying(100)
                  constraints:
                    nullable: false

       ...

更改格式后一切正常。