Spring Boot Multitenancy — Hibernate — используйте ddl-auto для обновления всех схем при изменении структуры объекта

Я новичок в Spring Boot и пытаюсь реализовать многопользовательскую архитектуру, используя Spring boot 2, hibernate и flyway. Я имел в виду учебник https://reflectoring.io/flyway-spring-boot-multitenancy/, чтобы понять концепции и смог реализовать упомянутую архитектуру.

Однако, если я добавлю новые классы сущностей поля, все сломается, потому что спящий режим не создает новые поля в базах данных арендаторов. Из теории чтения и вопросов о стеке переполнения я понимаю, что пролетный путь должен решить эту проблему. Однако я не могу заставить его работать.

Может кто-нибудь сказать мне, где я ошибаюсь. Мое требование: когда я добавляю новое поле в класс сущностей, все таблицы во всех клиентских базах данных должны обновляться с помощью этого поля. Ниже приведен код

Свойства приложения

spring:
  jpa:
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
  flyway:
    enabled: false
tenants:
  datasources:
    vw:
      jdbcUrl: jdbc:mysql://localhost:3306/vw
      driverClassName: com.mysql.jdbc.Driver
      username: vikky
      password: Test@123
    bmw:
      jdbcUrl: jdbc:mysql://localhost:3306/bmw
      driverClassName: com.mysql.jdbc.Driver
      username: vikky
      password: Test@123

Конфигурация источника данных

@Configuration
public class DataSourceConfiguration {

    private final DataSourceProperties dataSourceProperties;

    public DataSourceConfiguration(DataSourceProperties dataSourceProperties) {
        this.dataSourceProperties = dataSourceProperties;
    }

    @Bean
    public DataSource dataSource() {
        TenantRoutingDataSource customDataSource = new TenantRoutingDataSource();
        customDataSource.setTargetDataSources(dataSourceProperties.getDatasources());
        return customDataSource;
    }

    @PostConstruct
    public void migrate() {
        dataSourceProperties
                .getDatasources()
                .values()
                .stream()
                .map(dataSource -> (DataSource) dataSource)
                .forEach(this::migrate);
    }

    private void migrate(DataSource dataSource) {
        Flyway flyway = Flyway.configure().dataSource(dataSource).load();
        flyway.migrate();
    }
}

Свойства источника данных

@Component
@ConfigurationProperties(prefix = "tenants")
public class DataSourceProperties {

    private Map<Object, Object> datasources = new LinkedHashMap<>();

    public Map<Object, Object> getDatasources() {
        return datasources;
    }

    public void setDatasources(Map<String, Map<String, String>> datasources) {
        datasources
                .forEach((key, value) -> this.datasources.put(key, convert(value)));
    }

    public DataSource convert(Map<String, String> source) {
        return DataSourceBuilder.create()
                .url(source.get("jdbcUrl"))
                .driverClassName(source.get("driverClassName"))
                .username(source.get("username"))
                .password(source.get("password"))
                .build();
    }
}

Источник данных маршрутизации арендатора

public class TenantRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return ThreadTenantStorage.getTenantId();
    }
}

Перехватчик заголовка

@Component
public class HeaderTenantInterceptor implements WebRequestInterceptor {

    public static final String TENANT_HEADER = "X-tenant";

    @Override
    public void preHandle(WebRequest request) throws Exception {
        ThreadTenantStorage.setTenantId(request.getHeader(TENANT_HEADER));
    }

    @Override
    public void postHandle(WebRequest request, ModelMap model) throws Exception {
        ThreadTenantStorage.clear();
    }

    @Override
    public void afterCompletion(WebRequest request, Exception ex) throws Exception {

    }
}

Существуют и другие классы, такие как веб-конфигурация, контроллеры и т. д., но я не считаю их обязательными для размещения здесь.

Ты прав. Вы должны использовать для этого Flyway, а не Hibernate. Кроме того, у вас отключен пролетный путь прямо в ваших объектах. И, возможно, сообщение об ошибке, которую вы получите, значительно облегчит определение того, что пошло не так.

Marcos Barbero 13.12.2020 14:05

Итак, просто для ясности: вы добавили скрипт миграции Flyway, который добавляет недостающие столбцы, выполняется flyway.migrate() программно изнутри @PostConstruct, и скрипт не запускался внутри клиентских БД, это правильно? Вы получили какие-либо ошибки? Есть ли какие-либо записи в таблице schema_version Flyway в схемах арендаторов? Доступен ли сценарий переноса в расположении по умолчанию (например, classpath:db/migration)?

crizzis 13.12.2020 17:44
3
2
1 024
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

После долгих исследований я понял, что flyway требуется только в случае производства, где мы не хотим обновлять определение таблицы с помощью ddl-auto=true. Поскольку у меня это было не так, я добавил ниже конфигурацию, чтобы обновить все схемы в соответствии со структурой объекта.

@Configuration
public class AutoDDLConfig
{

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${schemas.list}")
    private String schemasList;

    @Bean
    public void bb()
    {

        if (StringUtils.isBlank(schemasList))
        {
            return;
        }

        String[] tenants = schemasList.split(",");

        for (String tenant : tenants)
        {
            tenant = tenant.trim();
            DriverManagerDataSource dataSource = new DriverManagerDataSource();
            dataSource.setDriverClassName("com.mysql.jdbc.Driver"); // Change here to MySql Driver
            dataSource.setSchema(tenant);
            dataSource.setUrl("jdbc:mysql://localhost/" + tenant
                    + "?autoReconnect=true&characterEncoding=utf8&useSSL=false&useTimezone=true&serverTimezone=Asia/Kolkata&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true");
            dataSource.setUsername(username);
            dataSource.setPassword(password);

            LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean();
            emfBean.setDataSource(dataSource);
            emfBean.setPackagesToScan("com"); // Here mention JPA entity path / u can leave it scans all packages
            emfBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
            emfBean.setPersistenceProviderClass(HibernatePersistenceProvider.class);
            Map<String, Object> properties = new HashMap<>();

            properties.put("hibernate.hbm2ddl.auto", "update");
            properties.put("hibernate.default_schema", tenant);

            emfBean.setJpaPropertyMap(properties);
            emfBean.setPersistenceUnitName(dataSource.toString());
            emfBean.afterPropertiesSet();
        }

    }

}

Другие вопросы по теме