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

#spring #spring-boot #spring-mvc #spring-data-jpa #spring-data

#spring #spring-boot #spring-mvc #spring-data-jpa #многопользовательский

Вопрос:

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

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

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

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

 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 {

    }
}
 

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

Комментарии:

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

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

Ответ №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=trueamp;characterEncoding=utf8amp;useSSL=falseamp;useTimezone=trueamp;serverTimezone=Asia/Kolkataamp;useLegacyDatetimeCode=falseamp;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();
        }

    }

}