What is multitenancy?
It is a software architecture where the same instance of the software serves multiple tenants. You can see a tenant as an organization. For example, if I have a software platform to sell TV's, my tenants could be Samsung, Philips and Sony. The good thing is that with this approach you are sharing resources, and if you work with cloud platforms this could be a good idea. On the other hand, as you know nothing is free, you have to be aware about the security and the isolation of the data and other things. Before you decide if this kind architecture matches with your requirements, you have to study in deep your requirements.
How to implement that?
I will not delve into the details that motivate the choice of a strategy or another. There are three approaches to do that:
- Separate database per tenant (picture 2).
- Separate schema per tenant.
- Share database and schema.
Ok, we are ready to start. In this example, I am going to implement the first option (separata database per tenant). Note that in this example, the tenants will be added dynamically, this means that only adding the configuration of the tenant in the application.yml will be enough for the application be aware of the new tenant.
I remember you, that this example is using Spring Boot and Hibernate.
First of all we have to add in the application.yml the multitenancy configuration we want. In this case, I have two tenants.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
multitenancy: | |
tenants: | |
- | |
name: tenant_1 | |
default: true | |
url: jdbc:mysql://localhost:3306/tenant_1?serverTimezone=UTC | |
username: user | |
password: pass | |
driver-class-name: com.mysql.jdbc.Driver | |
- | |
name: tenant_2 | |
default: false | |
url: jdbc:mysql://localhost:3306/tenant_2?serverTimezone=UTC | |
username: user | |
password: pass | |
driver-class-name: com.mysql.jdbc.Driver |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// we have to exclude the default configuration, because we want to provide | |
// our multi tenant data source configuration. | |
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) | |
public class App { | |
public static void main(String[] args) { | |
SpringApplication.run(App.class, args); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component | |
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver { | |
@Override | |
public String resolveCurrentTenantIdentifier() { | |
return RequestContextHolderUtils.getCurrentTenantIdentifier(); | |
} | |
@Override | |
public boolean validateExistingCurrentSessions() { | |
return true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RequestContextHolderUtils { | |
private static final String DEFAULT_TENANT_ID = "tenant_1"; | |
public static final String getCurrentTenantIdentifier() { | |
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); | |
if (requestAttributes != null) { | |
String identifier = (String) requestAttributes.getAttribute(CustomRequestAttributes.CURRENT_TENANT_IDENTIFIER,RequestAttributes.SCOPE_REQUEST); | |
if (identifier != null) { | |
return identifier; | |
} | |
} | |
return DEFAULT_TENANT_ID; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { | |
private static final long serialVersionUID = 1L; | |
private String defaultTenant; | |
private Map<String, DataSource> map; | |
public DataSourceBasedMultiTenantConnectionProviderImpl(String defaultTenant, Map<String, DataSource> map) { | |
super(); | |
this.defaultTenant = defaultTenant; | |
this.map = map; | |
} | |
@Override | |
protected DataSource selectAnyDataSource() { | |
return map.get(defaultTenant); | |
} | |
@Override | |
protected DataSource selectDataSource(String tenantIdentifier) { | |
return map.get(tenantIdentifier); | |
} | |
public DataSource getDefaultDataSource() { | |
return map.get(defaultTenant); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableConfigurationProperties(MultitenancyConfigurationProperties.class) | |
public class MultitenancyConfiguration { | |
@Autowired | |
private MultitenancyConfigurationProperties multitenancyProperties; | |
@Bean(name = "multitenantProvider") | |
public DataSourceBasedMultiTenantConnectionProviderImpl dataSourceBasedMultiTenantConnectionProvider() { | |
HashMap<String, DataSource> dataSources = new HashMap<String, DataSource>(); | |
multitenancyProperties.getTenants().stream().forEach(tc -> dataSources.put(tc.getName(), DataSourceBuilder | |
.create() | |
.driverClassName(tc.getDriverClassName()) | |
.username(tc.getUsername()) | |
.password(tc.getPassword()) | |
.url(tc.getUrl()).build())); | |
return new DataSourceBasedMultiTenantConnectionProviderImpl(multitenancyProperties.getDefaultTenant().getName(), dataSources); | |
} | |
@Bean | |
@DependsOn("multitenantProvider") | |
public DataSource defaultDataSource() { | |
return dataSourceBasedMultiTenantConnectionProvider().getDefaultDataSource(); | |
} | |
} |
The last step is to implement a Rest controller to access to the data. Note that the Uri of the endpoint contains the 'tenantId' pattern. Of course, there are different ways to do that. For example using domains or subdomains and so on.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RestController | |
@RequestMapping("/{tenantId}/invoice") | |
public class InvoiceController { | |
@Autowired | |
private InvoiceRepository invoiceRepository; | |
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) | |
public List<Invoice> invoices() { | |
return Lists.newArrayList(invoiceRepository.findAll()); | |
} | |
} |
Feel free to contact me for questions or any doubt.
Happy codying
I like it.
ReplyDeleteHi,
ReplyDeleteHow to manage connection pooling in this kind of sscenario?
Thanks a lot. The issue I have is that JPA is only creating tables in the default datasource's database and does not in all the other tenants. Is this expected and is there a way to make JPA create tables in all tenants?
ReplyDeleteHOw to get the informtion of all datasources from database
ReplyDelete