2

My problem: Stuck on implementing change of schema after user login, following a StackOverFlow.

Description: Im using the class below. However, I have no idea on how to use it. Im reading every tutorial but I'm stuck. The result I'm expecting are:

1- Spring initializes with the default URL so the user can login.

2- After a successful login, it changes to the schema based on the UserDetails class.

I'm following the Stack Overflow solution at: Change database schema during runtime based on logged in user

The Spring version I'm using is

> : Spring Boot ::        (v2.3.3.RELEASE)
    import com.google.common.cache.CacheBuilder;
    import com.google.common.cache.CacheLoader;
    import com.google.common.cache.LoadingCache;
    import java.sql.Connection;
    import java.sql.ConnectionBuilder;
    import java.sql.SQLException;
    import java.util.concurrent.TimeUnit;
    import javax.sql.DataSource;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.jdbc.DataSourceBuilder;
    import org.springframework.core.env.Environment;
    import org.springframework.jdbc.datasource.AbstractDataSource;
     
     
    public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
     
        @Autowired
        UsuarioProvider customUserDetails;
     
        @Autowired
        Environment env;
     
        private LoadingCache<String, DataSource> dataSources = createCache();
     
        public UserSchemaAwareRoutingDataSource() {
        }
     
        public UserSchemaAwareRoutingDataSource(UsuarioProvider customUserDetails, Environment env) {
            this.customUserDetails = customUserDetails;
            this.env = env;
        }
     
        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 Exception {
                            return buildDataSourceForSchema(key);
                        }
                    });
        }
     
        private DataSource buildDataSourceForSchema(String schema) {
            System.out.println("schema:" + schema);
            String url = "jdbc:mysql://REDACTED.com/" + schema;
            String username = env.getRequiredProperty("spring.datasource.username");
            String password = env.getRequiredProperty("spring.datasource.password");
     
            System.out.println("Flag A");
     
            DataSource build = (DataSource) DataSourceBuilder.create()
                    .driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
                    .username(username)
                    .password(password)
                    .url(url)
                    .build();
     
            System.out.println("Flag B");
     
            return build;
        }
     
        @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() {
            try {
                Usuario usuario = customUserDetails.customUserDetails();
                //
                String db_schema = usuario.getTunnel().getDb_schema();
                //
     
                String schema = db_schema;
                return dataSources.get(schema);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }
     
        @Override
        public ConnectionBuilder createConnectionBuilder() throws SQLException {
            return super.createConnectionBuilder();
        }
     
    }

References: https://spring.io/blog/2007/01/23/dynamic-datasource-routing/

How to create Dynamic connections (datasource) in spring using JDBC

Spring Boot Configure and Use Two DataSources

Edit (Additional information required on the comments):

I have 1 database. This database has a n number of schemas. Each schema pertains to one company. One user pertains to one company. The login logic is as follows:

-User input username and password. -When successful, the UserDetails will contain the name of the 'schema' of this user. Basically, to which company/schema this user pertains.

  • It should, after that, connect directly to that schema, so the user can work with the data of his own company.

I hope this clarify as much as possible.

Edit 2:

    @Component
    public class UsuarioProvider {
    
        @Bean
        @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) // or just @RequestScope
        public Usuario customUserDetails() {
            return (Usuario) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        }
    
    }
    public class UserSchemaAwareRoutingDataSource extends AbstractDataSource {
    
        @Autowired
        private UsuarioProvider usuarioProvider;
    
        @Autowired // This references the primary datasource, because no qualifier is given
        private DataSource companyDependentDataSource;
    
        @Autowired
        @Qualifier(value = "loginDataSource") 
        private DataSource loginDataSource;
    
        @Autowired
        Environment env;
    
        private LoadingCache<String, DataSource> dataSources = createCache();
    
        public UserSchemaAwareRoutingDataSource() {
        }
    
    
    
        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 Exception {
                            return buildDataSourceForSchema(key);
                        }
                    });
        }
    
        private DataSource buildDataSourceForSchema(String schema) {
            System.out.println("schema:" + schema);
            String url = "jdbc:mysql://REDACTED.com/" + schema;
            String username = env.getRequiredProperty("spring.datasource.username");
            String password = env.getRequiredProperty("spring.datasource.password");
    
            System.out.println("Flag A");
    
            DataSource build = (DataSource) DataSourceBuilder.create()
                    .driverClassName(env.getRequiredProperty("spring.datasource.driverClassName"))
                    .username(username)
                    .password(password)
                    .url(url)
                    .build();
    
            System.out.println("Flag B");
    
            return build;
        }
    
        @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() {
            try {
                System.out.println("Flag G");
                Usuario usuario = usuarioProvider.customUserDetails(); // request scoped answer!
                String db_schema = usuario.getTunnel().getDb_schema();
                return dataSources.get(db_schema);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }
    
        @Override
        public ConnectionBuilder createConnectionBuilder() throws SQLException {
            return super.createConnectionBuilder();
        }
    
    }

Do I need to put @Configuration on top of this class? I'm not being able to make Spring Boot aware of this settings. I'm a bit confused on how to tell Spring Boot what is the loginDataSource; url is. I was using the application.properties default values to login.

Vy Do
  • 46,709
  • 59
  • 215
  • 313
KenobiBastila
  • 539
  • 4
  • 16
  • 52
  • https://www.baeldung.com/hibernate-5-multitenancy – Suganthan Madhavan Pillai Oct 28 '20 at 14:53
  • 1
    Hi @SHASHA, I am currently at work. I'll check back on your question in ~2-3h. Looks like a tricky one :D In the meantime: could you provide more information about error messages you get and how you set up your repositories. You already took the right way by investigating about two datasources. – Johannes Leimer Oct 29 '20 at 14:19
  • @JohannesLeimer No problem. Didnt thought would be so tricky, sorry. I will try to provide more information in the topic. But could you clear up what do you require? – KenobiBastila Oct 29 '20 at 20:29
  • @JohannesLeimer Pleaes let me know if this is clear enough. Im not getting error messages but im not sure how to setup this to work. I just basically have this class right now. And the login is working fine but, I dont know how to use this class to change schemas. – KenobiBastila Oct 29 '20 at 20:34

1 Answers1

1

Your setting seams the classical situation for two different DataSources. Here is a Baeldung-Blog-Post how to configure Spring Data JPA.

First thing to notice, they are using @Primary. This is helping and standing in your way at the same time. You can only have ONE primary bean of a certain type. This is causing trouble for some people, since they try to "override" a spring bean by making their testing spring beans primary. Which results in having two primary beans with the same type. So be careful, when setting up your tests.

But it also eases things up, if you are mostly referring to one DataSource and only in a few cases to the other. This seams to be your case, so lets adopt it.

Your DataSource configuration could look like

@Configuration
public class DataSourceConfiguration {
    @Bean(name="loginDataSource")
    public DataSource loginDataSource(Environment env) {
        String url = env.getRequiredProperty("spring.logindatasource.url");
        return DataSourceBuilder.create()
            .driverClassName(env.getRequiredProperty("spring.logindatasource.driverClassName"))
            [...]
            .url(url)
            .build();
    }
    
    @Bean(name="companyDependentDataSource")
    @Primary // use with caution, I'd recommend to use name based autowiring. See @Qualifier
    public DataSource companyDependentDataSource(Environment env) {
        return new UserSchemaAwareRoutingDataSource(); // Autowiring is done afterwards by Spring
    }
}

These two DataSources can now be used in your repositories/DAOs or how ever you structure your program

@Autowired // This references the primary datasource, because no qualifier is given. UserSchemaAwareRoutingDataSource is its implementation
// @Qualifier("companyDependentDataSource") if @Primary is omitted
private DataSource companyDependentDataSource;

@Autowired
@Qualifier(name="loginDataSource") // reference by bean name
private DataSource loginDataSource

Here is an example how to configure Spring Data JPA with a DataSource referenced by name:

@Configuration
@EnableJpaRepositories(
    basePackages = "<your entity package>", 
    entityManagerFactoryRef = "companyEntityManagerFactory", 
    transactionManagerRef = "companyTransactionManager"
)
public class CompanyPersistenceConfiguration {

    @Autowired
    @Qualifier("companyDependentDataSource")
    private DataSource companyDependentDataSource;
    
    @Bean(name="companyEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean companyEntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(companyDependentDataSource);
        // ... see Baeldung Blog Post
        return emf;
    }

    @Bean(name="companyTransactionManager")
    public PlatformTransactionManager companyTransactionManager() {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(companyEntityManagerFactory().getObject());
        return tm;
    }
}

As described in my SO-answer you referred to there is an important assumption

The current schema name to be used for the current user is accessible through a Spring JSR-330 Provider like private javax.inject.Provider<User> user; String schema = user.get().getSchema();. This is ideally a ThreadLocal-based proxy.

This is the trick which makes the UserSchemaAwareRoutingDataSource implementation possible. Spring beans are mostly singletons and therefore stateless. This also applies to the normal usage of DataSources. They are treated as stateless singletons and the references to them are passed over in the whole program. So we need to find a way to provide a single instance of the companyDependentDataSource which is behaving different on user basis regardless. To get that behavior I suggest to use a request-scoped bean.

In a web application, you can use @Scope(REQUEST_SCOPE) to create such objects. There is also a Bealdung Post talking about that topic. As usual, @Bean annotated methods reside in @Confiugration annotated classes.

@Configuration
public class UsuarioConfiguration {
    @Bean
    @Scope(value = WebApplicationContext.SCOPE_REQUEST,
     proxyMode = ScopedProxyMode.TARGET_CLASS) // or just @RequestScope
    public Usuario usario() {
        // based on your edit2 
        return (Usuario) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }
}

Now you can use this request scoped object with a provider inside your singleton DataSource to behave different according to the logged in user:

@Autowired
private Usario usario; // this is now a request-scoped proxy which will create the corresponding bean (see UsuarioConfiguration.usario()

private DataSource determineTargetDataSource() {
    try {
        String db_schema = this.usuario.getTunnel().getDb_schema();
        return dataSources.get(db_schema);
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return null;
}

I hope this helps you understand the request scope concept of Spring.

So your login process would look something like

  1. User input username and password
  2. A normal spring bean, referencing the userDataSource by name, is checking the login and is putting the user information into the session/securitycontext/cookie/....
  3. When successful, during the next request the companyDependentDataSource is capable of retrieving a properly setup Usario object
  4. You can use this datasource now to do user specific stuff.

To verify your DataSource is properly working you could create a small Spring MVC endpoint

@RestController
public class DataSourceVerificationController {
    @Autowired
    private Usario usario;
    
    @Autowired
    @Qualifier("companyDependentDataSource") // omit this annotation if you use @Primary
    private DataSource companyDependentDataSource;

    @GetRequest("/test")
    public String test() throws Exception {
        String schema = usario.getTunnel().getDb_schema()
    
        Connection con = companyDependentDataSource.getConnection();
        Statement stmt = con.createStatement();
        ResultSet rs = stmt.executeQuery("select name from Employee"); // just a random guess
        rs.next();
        String name = rs.getString("name")
        rs.close();
        stmt.close();
        con.close();
        
        return "name = '" + name + "', schema = '" + schema + "'";
    }
}

Take your favorite browser go to your login page, do a valid login and call http://localhost:8080/test afterwards

Johannes Leimer
  • 1,339
  • 12
  • 23
  • Thank you so much for the answer. However, Im not being able to make it work. Do I need to add @Configuration to the class? How do I make spring boot aware of this class? How am I supposed to set up loginDataSource, because I was using the application.properties default values – KenobiBastila Nov 01 '20 at 12:58
  • Or, actually, how do I use this datasource as default after login? – KenobiBastila Nov 01 '20 at 13:01
  • Hi @SHASHA, I updated my answer to hopefully clarify it. The loginDataSource is just a normal DataSource which you can build following https://www.baeldung.com/spring-data-jpa-multiple-databases. The loginDataSource would be the non-primary one, since there would be less injection points – Johannes Leimer Nov 01 '20 at 13:31
  • Ok I think I got it. is there any kind of listener or method I can use to check if its working after i logged in ? – KenobiBastila Nov 01 '20 at 18:54
  • Ok. I think its working but, Im trying to save a mock object after I login and I can never auto wire the repository. It says 'required a bean of type that could not be found'. Is there any special setting I have to do after I login to tell spring to correctly map the packages again? – KenobiBastila Nov 01 '20 at 19:19
  • 1
    Hi @SHASHA, can you give more information about the error message you received? Normally Spring is really descriptive, when it comes to tell you what went wrong. At least we need the type Spring was searching. I'll adapt my answer to show you how to test if it is working. – Johannes Leimer Nov 01 '20 at 20:24
  • 1
    About your repository setup: If you need help with that you should provide your configuration of it. – Johannes Leimer Nov 01 '20 at 20:42
  • Johannes, Im creating a public repo so you can take a look, because im having a hard time getting the companyDataSource to work. Would you mind once i get it running? Pleaseeeee – KenobiBastila Nov 02 '20 at 21:31
  • 1
    Hi @SHASHA, yes I can have a look. I'll try to update this answer accordingly to reflect the necessary bits of information which led to the solution. – Johannes Leimer Nov 02 '20 at 21:52
  • https://github.com/KenobySky/SpringSchema/tree/master – KenobiBastila Nov 03 '20 at 01:32
  • I made that repo, its quite a replica of my original project and its giving the same error. Im stuck. I really hope you can find whats going on. org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'companyEntityManagerFactory' defined in class path resource [com/winter/config/PersistenceCompanyConfiguration.class] – KenobiBastila Nov 03 '20 at 01:32
  • Johannes, there was a.. problem with the repo, github decided to switch to the main branch and my git client was using the master branch. I think Ive fixed now. – KenobiBastila Nov 04 '20 at 00:07
  • Hi! Did you get any luck with it? If you are stuck too, let me know, ok? I really appreciate your help. Just let me know if you are still working on it or if you are too busy – KenobiBastila Nov 09 '20 at 22:17
  • 1
    Hi @SHASHA, I was really busy the last days and couldn't find time to look into your repo. But I already started to clean up some quirks in the database setup. My plan is to continue on thursday evening. – Johannes Leimer Nov 10 '20 at 19:28
  • 1
    Hi @SHASHA, It took me a while to figure out what your problem was. I think you just didn't understood how request scoped beans work. I suggest you to dig a bit deeper on that topic. I issued a pull-request on your Github repository. – Johannes Leimer Nov 13 '20 at 22:54
  • HI! thanks so much for the help! I updated the repo, Im going to let it public so others can use it. I added some comments to each class, if you can take a look, I would appreciate it. Hopefully others can find this repo useful!! thanks so much johannes – KenobiBastila Nov 18 '20 at 13:28
  • Johannes, after studying a lot and using your code, I got it working!! Thank you so much! I had to read a lot about beans, scopes and much else about Spring. I have 1 minor issues though, 1 - Spring wont run "create" when it changes schema. Is there anyway to manually run it? – KenobiBastila Nov 28 '20 at 04:21
  • Hi Leimer, happy new year. Would you please take a look in this topic? Im stuck on it for weeks. https://stackoverflow.com/questions/65496481/thymeleaf-spring-add-items-to-a-list-from-a-combobox-to-a-table – KenobiBastila Dec 30 '20 at 17:39
  • Hi Mr Johannes. I need one last help with the solution you helped me achieve : https://stackoverflow.com/questions/68874487/how-to-manually-initialize-a-database-using-a-datasource – KenobiBastila Aug 21 '21 at 15:43