{"id":16619949,"url":"https://github.com/cepr0/sb-multitenant-db-demo","last_synced_at":"2025-03-21T14:31:39.255Z","repository":{"id":130472600,"uuid":"128995502","full_name":"Cepr0/sb-multitenant-db-demo","owner":"Cepr0","description":"Multi-tenant Spring Boot demo project","archived":false,"fork":false,"pushed_at":"2020-04-04T21:41:19.000Z","size":66,"stargazers_count":67,"open_issues_count":2,"forks_count":34,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-18T02:12:50.808Z","etag":null,"topics":["database","hibernate","multitenant","sping-boot","spring-data-jpa"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Cepr0.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-04-10T21:11:53.000Z","updated_at":"2025-03-03T18:40:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"5052861a-40bb-446b-85ea-7860a8e1a146","html_url":"https://github.com/Cepr0/sb-multitenant-db-demo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cepr0%2Fsb-multitenant-db-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cepr0%2Fsb-multitenant-db-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cepr0%2Fsb-multitenant-db-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cepr0%2Fsb-multitenant-db-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Cepr0","download_url":"https://codeload.github.com/Cepr0/sb-multitenant-db-demo/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244815225,"owners_count":20514917,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","hibernate","multitenant","sping-boot","spring-data-jpa"],"created_at":"2024-10-12T02:43:09.383Z","updated_at":"2025-03-21T14:31:39.248Z","avatar_url":"https://github.com/Cepr0.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Multi-tenancy database Spring Boot demo project\n\n### A simple single-class solution with support for dynamic loading of the tenant datasources\n\n#### Description\nThis solution utilizes the separate schema of [multi-tenant data approaches][1]:\n\n![mt.png](mt.png)\n\nTo implement multi-tenancy with Spring Boot we can use [AbstractRoutingDataSource][2] as base **DataSource** class for all '*tenant databases*'. \n\nIt has one abstract method [determineCurrentLookupKey][3] that we have to override. It tells the `AbstractRoutingDataSource` which of the tenant datasource it have to provide at the moment to work with. Because it work in the multi-threading environment, the information of the chosen tenant should be stored in `ThreadLocal` variable. \n\nThe `AbstractRoutingDataSource` stores the info of the tenant datasources in its private `Map\u003cObject, Object\u003e targetDataSources`. The key of this map is a **tenant identifier** (for example the String type) and the value - the **tenant datasource**. To put our tenant datasources to this map we have to use its setter `setTargetDataSources`.\n\nThe `AbstractRoutingDataSource` will not work without 'default' datasource which we have to set with method `setDefaultTargetDataSource(Object defaultTargetDataSource)`.\n\nAfter we set the tenant datasources and the default one, we have to invoke method `afterPropertiesSet()` to tell the `AbstractRoutingDataSource` to update its state.\n\nSo our 'MultiTenantManager' class in the basic version can be like this:\n\n```java\n@Configuration\npublic class MultiTenantManager {\n\n    private final ThreadLocal\u003cString\u003e currentTenant = new ThreadLocal\u003c\u003e();\n    private final Map\u003cObject, Object\u003e tenantDataSources = new ConcurrentHashMap\u003c\u003e();\n    private final DataSourceProperties properties;\n\n    private AbstractRoutingDataSource multiTenantDataSource;\n\n    public MultiTenantManager(DataSourceProperties properties) {\n        this.properties = properties;\n    }\n\n    @Bean\n    public DataSource dataSource() {\n        multiTenantDataSource = new AbstractRoutingDataSource() {\n            @Override\n            protected Object determineCurrentLookupKey() {\n                return currentTenant.get();\n            }\n        };\n        multiTenantDataSource.setTargetDataSources(tenantDataSources);\n        multiTenantDataSource.setDefaultTargetDataSource(defaultDataSource());\n        multiTenantDataSource.afterPropertiesSet();\n        return multiTenantDataSource;\n    }\n\n    public void addTenant(String tenantId, String url, String username, String password) throws SQLException {\n\n        DataSource dataSource = DataSourceBuilder.create()\n                .driverClassName(properties.getDriverClassName())\n                .url(url)\n                .username(username)\n                .password(password)\n                .build();\n\n        // Check that new connection is 'live'. If not - throw exception\n        try(Connection c = dataSource.getConnection()) {\n            tenantDataSources.put(tenantId, dataSource);\n            multiTenantDataSource.afterPropertiesSet();\n        }\n    }\n\n    public void setCurrentTenant(String tenantId) {\n        currentTenant.set(tenantId);\n    }\n\n    private DriverManagerDataSource defaultDataSource() {\n        DriverManagerDataSource defaultDataSource = new DriverManagerDataSource();\n        defaultDataSource.setDriverClassName(\"org.h2.Driver\");\n        defaultDataSource.setUrl(\"jdbc:h2:mem:default\");\n        defaultDataSource.setUsername(\"default\");\n        defaultDataSource.setPassword(\"default\");\n        return defaultDataSource;\n    }\n}\n```\n\n**Brief explanation**\n\n- map `tenantDataSources` it's our local tenant datasource storage which we put to the `setTargetDataSources` setter;\n\n- `DataSourceProperties properties` is used to get Database Driver Class name of tenant database from the `spring.datasource.driverClassName` of the 'application.properties' (for example, `org.postgresql.Driver`);\n\n- method `addTenant` is used to add a new tenant and its datasource to our local tenant datasource storage. **We can do this on the fly** - thanks to the method `afterPropertiesSet()` (see example [here](service/src/main/java/io/github/cepr0/demo/controller/TenantController.java));\n\n- method `setCurrentTenant(String tenantId)` is used to 'switch' onto datasource of the given tenant. We can use this method, for example, in the REST controller when handling a request to work with database. The request should contain the 'tenantId', for example in the `X-TenantId` header, that we can retrieve and put to this method;\n\n- `defaultDataSource()` is build with in-memory H2 Database to avoid the using the default database on the working SQL server.\n\nNote: we **must** set `spring.jpa.hibernate.ddl-auto` parameter to `none` to disable the Hibernate make changes in the database schema. We have to create schema of tenant databases beforehand.\n\n#### Loading tenant datasource dynamically\n\nTo realize this we can add to the class new property `tenantResolver` and it's setter:\n\n```java\n   private Function\u003cString, DataSourceProperties\u003e tenantResolver;\n\n   public void setTenantResolver(Function\u003cString, DataSourceProperties\u003e tenantResolver) {\n       this.tenantResolver = tenantResolver;\n   }\n```\nIt will work as supplier of tenantId and its datasource. Then we can update the `setCurrentTenant` method:\n\n```java\n    public void setCurrentTenant(String tenantId) throws SQLException, TenantNotFoundException, TenantResolvingException {\n        if (tenantIsAbsent(tenantId)) {\n            if (tenantResolver != null) {\n                DataSourceProperties properties;\n                try {\n                    properties = tenantResolver.apply(tenantId);\n                } catch (Exception e) {\n                    throw new TenantResolvingException(e, \"Could not resolve the tenant!\");\n                }\n                String url = properties.getUrl();\n                String username = properties.getUsername();\n                String password = properties.getPassword();\n    \n                addTenant(tenantId, url, username, password);\n            } else {\n                throw new TenantNotFoundException(format(\"Tenant %s not found!\", tenantId));\n            }\n        }\n        currentTenant.set(tenantId);\n    }\n```  \n\nIf tenantId not found in the local storage then we try to resolve them (if resolver is not null) and add it and its datasource parameters.  \n\nNow, during the next request to the our rest controller (for example), the code will check whether the tenant datasource is present in the local storage and, \nif it does not exist, will load its parameters dynamically.\n\nFull code of the `MultiTenantManager` you can find [here](multitenant/src/main/java/io/github/cepr0/demo/multitenant/MultiTenantManager.java).\n\n#### Usage example\n\nWill be added...\n\n  [1]: http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#multitenacy-approaches\n  [2]: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.html\n  [3]: https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.html#determineCurrentLookupKey--\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcepr0%2Fsb-multitenant-db-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcepr0%2Fsb-multitenant-db-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcepr0%2Fsb-multitenant-db-demo/lists"}