복붙노트

[SPRING] 로그인 한 사용자를 기반으로 런타임 중에 데이터베이스 스키마 변경

SPRING

로그인 한 사용자를 기반으로 런타임 중에 데이터베이스 스키마 변경

동적 데이터 소스 라우팅에 대한 많은 질문과 답변을 읽었으며 AbstractRoutingDataSource와 다른 데이터 소스를 사용하여 솔루션을 구현했습니다 (아래 참조). 괜찮지 만 모든 데이터 소스에 하드 코드 된 속성이 필요합니다. 애플리케이션을 사용하는 사용자 수가 증가함에 따라 더 이상 라우팅 할 수있는 방법이 아닙니다. 또한 새로운 사용자가 등록 할 때마다 등록 정보에 항목을 추가해야합니다. 상황은 다음과 같습니다.

나는 최대 절전 모드 5.1과 스프링 데이터 jpa와 함께 스프링 부트 1.4.0을 사용하고있다.

스키마를 완전히 동적으로 변경하는 방법을 찾을 수 없습니다. 누군가 봄에 그것을하는 방법을 알고 있습니까?

편집하다:

@Johannes Leimer의 답변 덕분에, 나는 실용적인 구현을 얻었습니다.

코드는 다음과 같습니다.

사용자 제공자 :

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

UserSchema 인식 라우팅 데이터 소스 :

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. ==============================

    1.내가 아직 귀하의 질문 아래에 의견을 게시하는 평판이 없기 때문에, 내 대답은 다음과 같은 가정을 기반으로합니다 :

    내가 아직 귀하의 질문 아래에 의견을 게시하는 평판이 없기 때문에, 내 대답은 다음과 같은 가정을 기반으로합니다 :

    ThreadLocal 프록시 조합을 사용하여 스키마 이름을 가져오고, Singleton-DataSource는 모든 사용자 요청에서 다르게 동작합니다. 이 솔루션은 AbstractRoutingDataSource, Meherzad의 의견 및 자신의 경험에 대한 힌트에서 영감을 얻었습니다.

    Spring의 AbstractDataSource를 용이하게하고 AbstractRoutingDataSource와 같이 구현하는 것이 좋습니다. 정적 Map-like 접근 방식 대신 Guava 캐시를 사용하여 사용하기 쉬운 캐시를 얻습니다.

    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가 생성되면 10 분 동안 캐시됩니다. 그게 전부 야.

    새로 생성 된 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가 있습니다.

    나는이 코드를 테스트하지 않았다!

    편집하다: Spring에서 Provider 를 구현하려면 이것을 프로토 타입으로 정의해야합니다. JSR-330 및 Spring Securitys SecurityContextHolder의 스프링 지원을 활용할 수 있습니다.

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

    RequestInterceptor, UserProvider 또는 컨트롤러 코드가 없어 사용자를 더 이상 업데이트 할 필요가 없습니다.

    이게 도움이 되나요?

    EDIT2 기록을 위해 : CustomUserDetails 빈을 직접 참조하지 마십시오. 이것이 프로토 타입이기 때문에, Spring은 CustomUserDetails 클래스를위한 프록시를 만들려고 시도 할 것입니다. 우리의 경우에는 좋지 않습니다. 따라서 Providers를 사용하여이 빈에 액세스하십시오. 또는 인터페이스로 만드십시오.

  2. ==============================

    2.DBMS를 지정하지 않았다면 여기에 도움이 될 수있는 높은 수준의 아이디어가 있습니다.

    DBMS를 지정하지 않았다면 여기에 도움이 될 수있는 높은 수준의 아이디어가 있습니다.

    (Spring Data JDBC-ext를 참조로 사용하고 있지만, 일반적인 AOP를 사용하면 쉽게 접근 할 수 있습니다)

    http://docs.spring.io/spring-data/jdbc/docs/current/reference/html/orcl.connection.html, 섹션 8.2를 참조하십시오.

    Spring Data JDBC-ext에는 DataPource에서 Connection을 얻을 때 임의의 SQL을 실행할 수있는 ConnectionPreparer가 있습니다. 스키마를 전환하는 명령을 간단히 실행할 수 있습니다 (예 : Oracle의 경우 ALTER SESSION SET CURRENT SCHEMA = 'schemaName', Sybase의 경우 schemaName 등을 사용).

    e.

    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>
    
  3. from https://stackoverflow.com/questions/39357367/change-database-schema-during-runtime-based-on-logged-in-user by cc-by-sa and MIT license