Изменение схемы базы данных во время выполнения на основе зарегистрированного пользователя

Я прочитал много вопросов и ответов о динамической маршрутизации данных и реализовал решение с использованием AbstractRoutingDataSource и другое (см. Ниже). Это прекрасно, но требует жестко заданных свойств для всех источников данных. По мере увеличения количества пользователей, использующих приложение, это не является подходящим способом маршрутизации. Также потребуется добавить запись в свойства каждый раз, когда регистрируется новый пользователь. Ситуация такова:

  • 1 сервер базы данных
  • многие схемы на этом сервере, каждый пользователь имеет свою собственную схему.
  • Мне нужно изменить имя схемы во время выполнения
  • имя схемы можно сохранить зарегистрированным пользователем

Я использую spring boot 1.4.0 вместе с hibernate 5.1 и spring data jpa

Я не могу найти способ полностью изменить схему. Кто-нибудь знает, как это сделать весной?

РЕДАКТИРОВАТЬ:

Благодаря ответу @Johannes Leimer, я получил работу.

Здесь код:

Провайдер:

@Component
public class UserDetailsProvider {
    @Bean
    @Scope("prototype")
    public CustomUserDetails customUserDetails() {
        return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}

UserSchemaAwareRoutingDatasource:

public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
@Inject
Provider<CustomUserDetails> customUserDetails;

@Inject
Environment env;
private LoadingCache<String, DataSource> dataSources = createCache();

@Override
public Connection getConnection() throws SQLException {
    try {
        return determineTargetDataSource().getConnection();
    } catch (ExecutionException e){
        e.printStackTrace();

        return null;
    }
}

@Override
public Connection getConnection(String username, String password) throws SQLException {
    System.out.println("getConnection" + username);
    System.out.println("getConnection2" + password);
    try {
        return determineTargetDataSource().getConnection(username, password);
    } catch (ExecutionException e) {
        e.printStackTrace();
        return null;
    }
}

private DataSource determineTargetDataSource() throws SQLException, ExecutionException {
    try {
        String schema = customUserDetails.get().getUserDatabase();
        return dataSources.get(schema);
    } catch (NullPointerException e) {
        e.printStackTrace();

        return dataSources.get("fooooo");
    }

}

Ответ 1

Предположения

Поскольку у меня нет репутации, чтобы опубликовать комментарий ниже вашего вопроса, мой ответ основан на следующих предположениях:

  • Текущее имя схемы, которое будет использоваться для текущего пользователя, доступно через поставщика Spring JSR-330, например private javax.inject.Provider<User> user; String schema = user.get().getSchema(); private javax.inject.Provider<User> user; String schema = user.get().getSchema(); , Это идеальный прокси-сервер на основе ThreadLocal.

  • Чтобы создать DataSource который полностью настроен так, как вам нужно, он требует одинаковых свойств. Каждый раз. Единственное, что отличается, это имя схемы. (Легко было бы получить и другие различные параметры, но это было бы слишком большим для этого ответа)

  • Каждая схема уже настроена с необходимым DDL, поэтому нет необходимости в спящем режиме создавать таблицы или что-то еще

  • Каждая схема базы данных выглядит полностью одинаковой, за исключением ее имени

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

Моя идея решения

Используйте комбинацию прокси-сервера ThreadLocal, чтобы получить имя схемы и Singleton-DataSource, которые ведут себя по-разному при каждом запросе пользователя. Это решение вдохновлено вашим намеком на AbstractRoutingDataSource, комментарии Мехерзада и собственный опыт.

Динамический DataSource

Я предлагаю облегчить AbstractDataSource Spring и реализовать его как AbstractRoutingDataSource. Вместо статического Map -like подход мы используем Guava Cache, чтобы получить простой в использовании кэш.

public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
    private @Inject javax.inject.Provider<User> user;
    private @Inject Environment env;
    private LoadingCache<String, DataSource> dataSources = createCache();

    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

    private DataSource determineTargetDataSource() {
        String schema = user.get().getSchema();
        return dataSources.get(schema);
    }

    private LoadingCache<String, DataSource> createCache() {
        return CacheBuilder.newBuilder()
           .maximumSize(100)
           .expireAfterWrite(10, TimeUnit.MINUTES)
           .build(
               new CacheLoader<String, DataSource>() {
                 public DataSource load(String key) throws AnyException {
                   return buildDataSourceForSchema(key);
                 }
               });
    }

    private DataSource buildDataSourceForSchema(String schema) {
        // e.g. of property: "jdbc:postgresql://localhost:5432/mydatabase?currentSchema="
        String url = env.getRequiredProperty("spring.datasource.url") + schema;
        return DataSourceBuilder.create()
            .driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
            [...]
            .url(url)
            .build();
    }
}

Теперь у вас есть "DataSource", который действует по-разному для каждого пользователя. После создания DataSource он будет кэшироваться в течение 10 минут. Это.

Внесите приложение в известность о нашем динамическом DataSource

Место для интеграции нашего недавно созданного DataSource - это синглтон DataSource, известный весеннему контексту и используемый во всех компонентах, например EntityManagerFactory

Поэтому нам нужен эквивалент:

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

но он должен быть более динамичным, чем простой DataSourceBuilder на основе простого свойства:

@Primary
@Bean(name = "dataSource")
public UserSchemaAwareRoutingDataSource dataSource() {
    return new UserSchemaAwareRoutingDataSource();
}

Вывод

У нас есть прозрачный динамический DataSource, который использует правильный DataSource каждый раз.

Открытые вопросы

  • Что делать, когда ни один пользователь не вошел в систему? Доступ запрещен для базы данных?
  • Кто создает схемы?

отказ

Я не тестировал этот код!

EDIT: для реализации Provider<CustomUserDetails> с Spring вам необходимо определить это как прототип. Вы можете использовать поддержку Springs для JSR-330 и Spring Securitys SecurityContextHolder:

@Bean @Scope("prototype")
public CustomUserDetails customUserDetails() {
    return return (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

Вам больше не нужен RequestInterceptor, UserProvider или код контроллера, чтобы обновить пользователя.

Помогает ли это?

EDIT2 Только для записи: НЕ CustomUserDetails непосредственно на компонент CustomUserDetails. Поскольку это прототип, Spring попытается создать прокси для класса CustomUserDetails, что в нашем случае не является хорошей идеей. Поэтому просто используйте Provider для доступа к этому компоненту. Или сделайте его интерфейсом.

Ответ 2

Учитывая, что вы не указываете СУБД, вот идея высокого уровня, которая может помочь.

(Хотя я использую Spring Data JDBC-ext в качестве ссылки, такой же подход можно легко использовать с помощью общего АОП)

См. Http://docs.spring.io/spring-data/jdbc/docs/current/reference/html/orcl.connection.html, раздел 8.2

В Spring Data JDBC-ext есть ConnectionPreparer, который позволяет запускать произвольные SQL-запросы при приобретении соединения из DataSource. Вы можете просто выполнить команды для переключения схемы (например, ALTER SESSION SET CURRENT SCHEMA = 'schemaName' в Oracle, using schemaName для Sybase и т.д.).

например

package foo;

import org.springframework.data.jdbc.support.ConnectionPreparer;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;

public class SwitchSchemaConnectionPreparer implements ConnectionPreparer {

    public Connection prepare(Connection conn) throws SQLException {
        String schemaName = whateverWayToGetTheScehmaToSwitch();
        CallableStatement cs = conn.prepareCall("ALTER SESSION SET CURRENT SCHEMA " + scehmaName);
        cs.execute();
        cs.close();
        return conn;
    }
}

В конфигурации контекста приложения

<aop:config>
    <aop:advisor 
        pointcut="execution(java.sql.Connection javax.sql.DataSource.getConnection(..))" 
        advice-ref="switchSchemaInterceptor"/>
</aop:config>

<bean id="switchSchemaInterceptor" 
      class="org.springframework.data.jdbc.aop.ConnectionInterceptor">
    <property name="connectionPreparer">
        <bean class="foo.SwitchSchemaConnectionPreparer"/>
    </property>
</bean>