【问题标题】:Spring boot jpa multitenancy with dynamic datasources具有动态数据源的 Spring Boot jpa 多租户
【发布时间】:2016-09-03 00:16:51
【问题描述】:

我正在尝试创建一个多租户 Web 应用程序,并找到了一个很好的教程 here。这向我解释了如何配置 MVC 以查找新租户(使用 CurrentTenantIdentifierResolver 和扩展 HandlerInterceptorAdapter 的 MultiTenancyInterceptor),如何为三个不同的租户配置三个不同的数据源,以及如何为服务器提供正确的数据源运行时通过扩展AbstractDataSourceBasedMultiTenantConnectionProviderImpl

现在,这是一个很好的起点,让我了解 spring 和 hibernate 中的多租户如何工作,但我想进一步推动这一点,我想让租户完全动态,即我不要假设一个应用程序可以有多少租户。

这是我的想法:

  • 应用程序配置为在启动时扫描路径(不在类路径中,例如 /usr/data/config),并在各种目录(每个租户一个目录)下查找各种 application.properties 文件,例如租户A、租户B、租户C...
  • 对于每个 application.properties,Spring Boot 将基于该文件创建一个数据源(该文件将只有启动属性 spring.datasource.url)。请注意,使用 Spring Boot 的属性会很棒,因为它为我提供了来自单个 URL 所需的所有信息,例如 JDBC 类等。
  • 我将在 HashMap 中注册每个数据源(如上一个链接所示)

之后,基本的多租户结构已经在前面的链接中描述:每次最终用户向浏览器发出请求时,服务器都会详细说明租户并返回正确的数据源以查找要使用的数据库.

任何人都可以向我指出一些资源,如果这是以前做过的(我用谷歌搜索了很多,但没有什么能让我开始),或者就使用哪些 spring 类/配置来实现这一点提供一些建议?

提前致谢

【问题讨论】:

    标签: java hibernate spring-data-jpa multi-tenant


    【解决方案1】:

    这就是我最终要做的,如果有人有这种需要的话。 任何对此的进一步扩展,或关于违反最佳实践的 cmets 都将受到欢迎。

    扩展AbstractDataSourceBasedMultiTenantConnectionProviderImplDataSourceProvider 必须覆盖两个方法

    • selectAnyDataSource 返回一个 @Autowired DataSource,该 @Autowired DataSource 由 Spring 使用为应用程序实例化数据源的常用方法进行实例化。
    • selectDataSource(String tenant) 执行以下操作:
      • 获取租户配置文件夹的路径
      • 使用从租户配置文件夹中的 application.properties 文件中获取的属性实例化 DataSourceProperties
      • 通过 DataSourceBuilder 创建并返回一个新的 DataSource,使用之前实例化的 DataSourceProperties 中的字段作为属性(很有用,因为 Spring 从数据库 URL 动态地为您提供驱动程序类名称)

    此处提供的代码,请随意使用:

    String configPath = [...]; // Instantiate your configuration path
    File file = new File(realPath);
    DataSourceProperties dsProp = new DataSourceProperties();
    Properties properties = new Properties();
    try {
        properties.load(new FileInputStream(file));
    } catch (IOException e) {
        throw new TenantNotConfiguredException(tenant); // Custom exception
    }
    
    PropertiesConfigurationFactory<DataSourceProperties> pcf = new PropertiesConfigurationFactory<>(dsProp);
    pcf.setTargetName(DataSourceProperties.PREFIX);
    pcf.setProperties(properties);
    
    try {
        dsProp = pcf.getObject();
    } catch (Exception e) {
        e.printStackTrace();
    }
    
    return DataSourceBuilder.create()
                .url(dsProp.getUrl())
                .driverClassName(dsProp.getDriverClassName())
                .username(dsProp.getUsername())
                .password(dsProp.getPassword())
                .build();
    

    【讨论】:

      【解决方案2】:

      这是完整的代码。我希望它有所帮助,因为我在到达此之前也遭受了痛苦。

        @RestController
        @RequestMapping(value = "/accounts", headers = "Accept=application/json")
        public class AppController {
      
          @Autowired
          UserService service;
          @Autowired
          AppService appService;
          ////////working on dynamic loading of datasource
      
          @Autowired
          Map<String, DataSource> dataSourcesMtApp;
      
          public void updateDataSource(String url, String username, String password, String tenant) {
              try {
                  DataSourceBuilder factory1 = DataSourceBuilder.create(MultiTenancyJpaConfiguration.class.getClassLoader()).url(url)
                          .username(username).password(password)
                          .driverClassName("com.mysql.jdbc.Driver");
                  dataSourcesMtApp.put(tenant, factory1.build());
                  System.out.println("Size:......................................................" + dataSourcesMtApp.size());
              } catch (Exception ex) {
                  Logger.getLogger(AppController.class.getName()).log(Level.SEVERE, null, ex);
              }
          }
      
          @PostMapping("/create-account")
          public Response createAccount(@RequestBody ConnectionParams request) {
              String tenant = ConnectionUtils.initializeDatabase(request.getDatabase(), request.getDbusername(), request.getDbpassword());
              updateDataSource("jdbc:mysql://localhost:3306/" + request.getDatabase() + "?useSSL=false", request.getDbusername(), request.getDbpassword(), tenant);
              TenantContextHolder.setTenantId(tenant);
              Users user = new Users();
              user.setPassword(request.getLoginpassword());
              user.setUsername(request.getLoginusername());
              user.setTenant(tenant);
              user = service.save(user);
              String response = "Account Setup Completed TenantId: " + tenant + " Username: " + user.getUsername();
              Response rp = new Response();
              rp.setResult(response);
              return rp;
          }
      }
      

      【讨论】: