使用 DATABASE 多租户时动态配置数据源
了解如何在动态创建新的数据源连接时使用 Grails 多租户功能的 DATABASE 模式。
作者:Sergio del Amo
Grails 版本 3.3.1
1 Grails 培训
Grails 培训 - 由那些创建并积极维护 Grails 框架的人员开发并交付!
2 入门
本指南从 JWT 自定义租户解析器 中中断的地方开始。请在开始本指南之前完成该指南以获得更好的理解。
2.1 你将需要
若要完成本指南,你需要以下内容
-
一些时间
-
一个合适的文本编辑器或 IDE
-
已安装 JDK 1.7 或更高版本,且已适当地配置了
JAVA_HOME
2.2 如何完成指南
若要开始,请执行以下操作
-
下载并解压源
或
Grails 指南存储库包含两个文件夹
-
initial
初始项目。通常是带有一些额外代码以便帮助你快速上手的简单 Grails 应用程序。 -
complete
完成的示例。它是通过对initial
文件夹应用指南所介绍的步骤并实施这些更改而得出的结果。
要完成指南,请转到 initial
文件夹
-
cd
进入grails-guides/grails-dynamic-multiple-datasources/initial
并按照以下章节中的说明进行操作。
如果进入 grails-guides/grails-dynamic-multiple-datasources/complete ,你可以直接转到完整示例 |
3 编写应用程序
本指南使用多租户 DATABASE 模式。要了解更多信息,请阅读每个租户的数据库多租户指南。 |
本指南展示了使用多租户 DATABASE
模式创建 SaaS(软件即服务)应用程序时会看到的一个典型流程。
为了简化本指南,简化了数据库配置过程。
使用以下架构创建两个 MySQL 数据库。
CREATE TABLE `plan` (
`id` bigint(20) NOT NULL,
`version` bigint(20) NOT NULL,
`title` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第一个数据库应命名为 gru
,第二个数据库应命名为 vector
。
在指南应用程序中,当用户注册时,会为用户指定一个与用户名相同的数据库,因此每个注册用户都有自己唯一的数据库。
在实际应用程序中,你可能会有更复杂的设置,其中可能涉及创建数据库、创建架构、以安全的方式将每个用户的数据库的网址和凭据保存在默认数据源等的数据库表中。
3.1 配置
在本指南中,我们将使用 MySQL,因此应将 MySQL 依赖项添加到 build.gradle
。
runtime 'mysql:mysql-connector-java:5.1.40'
在 MySQL 数据库中创建一个名为 minions
的数据库。
在 application.yml
中配置默认数据源以指向此数据库。由于 dbCreate: update
配置,此数据库的架构将由 Hibernate 生成。
应用程序的每个用户的 dataSource 设置将进行动态配置并继承默认设置。
处理安全性的领域类 User
、UserRole
、Role
映射到默认 dataSource。
hibernate:
cache:
queries: false
use_second_level_cache: false
use_query_cache: false
dataSource:
pooled: true
jmxExport: true
driverClassName: com.mysql.jdbc.Driver
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
username: root (1)
password: root (1)
dbCreate: update
url: jdbc:mysql://127.0.0.1:8889/minions (1)
1 | 根据你的 MySQL 安装配置这些设置。 |
在 application.yml
中将多租户模式更改为 DATABASE
gorm:
multiTenancy:
mode: DATABASE (1)
tenantResolverClass: demo.CurrentUserByJwtTenantResolver (2)
reactor:
# Whether to translate GORM events into Reactor events
# Disabled by default for performance reasons
events: false
1 | 将多租户模式定义为 DATABASE |
2 | 将租户解析器类设置为在通过 JWT 实现自定义租户解析器指南中编写的自定义类。 |
3.2 领域类
由于本指南使用了每个租户都有一个单独数据库的 DATABASE
多租户,因此你不再需要配置用于处理 tenantId
的列,因此可以删除 tenantId
属性
package demo
import grails.gorm.MultiTenant
class Plan implements MultiTenant<Plan> { (1)
String title
}
1 | 实施MultiTenant特性将此领域类视为多租户。 |
3.3 GORM 事件
创建一个名为 UserInsertedListener
的类。它会在插入新用户时创建一个新的连接源。
它使用 @Listener
注释同步侦听来自 GORM 的事件。
package demo
import grails.events.annotation.gorm.Listener
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import org.grails.datastore.mapping.engine.event.PostInsertEvent
import org.grails.orm.hibernate.HibernateDatastore
import org.springframework.beans.factory.annotation.Autowired
@CompileStatic
@Slf4j
class UserInsertedListener {
@Autowired
HibernateDatastore hibernateDatastore
@Autowired
DatabaseProvisioningService databaseProvisioningService
@Listener(User) (1)
void onUserPostInsertEvent(PostInsertEvent event) { (2)
String username = event.getEntityAccess().getPropertyValue("username")
DatabaseConfiguration databaseConfiguration = databaseProvisioningService.findDatabaseConfigurationByUsername(username) (3)
hibernateDatastore.getConnectionSources().addConnectionSource(databaseConfiguration.dataSourceName, databaseConfiguration.configuration) (4)
}
}
1 | 同步侦听域类 User 的 GORM 事件 |
2 | 侦听 PostInsertEvent 事件 |
3 | 使用协作者来检索 DatabaseConfiguration 对象(见下文)。 |
4 | 使用 ConnectionSources API 来动态配置新数据源。 |
将 UserInsertedListener
定义为 Bean
...
import demo.UserInsertedListener
...
beans = {
...
userInsertedListener(UserInsertedListener)
...
}
上一个侦听器使用了几类作为协作者
package demo
import groovy.transform.CompileStatic
@CompileStatic
class DatabaseConfiguration {
String dataSourceName
Map configuration
}
package demo
import groovy.transform.CompileStatic
@CompileStatic
class DatabaseProvisioningService {
UserRoleService userRoleService
List<DatabaseConfiguration> findAllDatabaseConfiguration() {
List<String> usernames = userRoleService.findAllUsernameByAuthority(VillainService.ROLE_VILLAIN)
usernames.collect { findDatabaseConfigurationByUsername(it) }
}
DatabaseConfiguration findDatabaseConfigurationByUsername(String username) {
new DatabaseConfiguration(dataSourceName: username, configuration: configurationByUsername(username))
}
Map<String, Object> configurationByUsername(String username) {
[
'hibernate.hbm2ddl.auto':'none', (1)
'username': 'root', (2)
'password': 'root', (2)
'url':"jdbc:mysql://127.0.0.1:8889/$username" (2)
] as Map<String, Object>
}
}
1 | 等效于 dbCreate: none |
2 | 将这些配置设置更改为与你的系统匹配。 |
上一个设置未指定 MySQL 驱动器或方言。这些是从在 application.yml 中配置的默认数据源中继承的 |
3.4 功能测试
我们对 使用 JWT 的自定义承租人解析器 指南的功能测试进行了少量修改,以验证插入新 User
时创建新连接源。
package demo
import grails.gorm.multitenancy.Tenants
import grails.plugins.rest.client.RestBuilder
import grails.testing.mixin.integration.Integration
import org.grails.orm.hibernate.HibernateDatastore
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Specification
import spock.lang.IgnoreIf
@IgnoreIf( { System.getenv('TRAVIS') as boolean } )
@Integration
class PlanControllerSpec extends Specification {
PlanService planService
UserService userService
VillainService villainService
RoleService roleService
@Autowired
HibernateDatastore hibernateDatastore
RestBuilder rest = new RestBuilder()
String accessToken(String u, String p) {
def resp = rest.post("http://localhost:${serverPort}/api/login") {
accept('application/json')
contentType('application/json')
json {
username = u
password = p
}
}
if ( resp.status == 200 ) {
return resp.json.access_token
}
null
}
def "Plans for current logged user are retrieved"() {
when:
User vector = villainService.saveVillain('vector', 'secret')
then:
hibernateDatastore.connectionSources.size() == old(hibernateDatastore.connectionSources.size()) + 1 (1)
when:
User gru = villainService.saveVillain('gru', 'secret')
then:
hibernateDatastore.connectionSources.size() == old(hibernateDatastore.connectionSources.size()) + 1 (1)
Tenants.withId("gru") {
planService.save('Steal the Moon')
}
Tenants.withId("vector") {
planService.save('Steal a Pyramid')
}
when: 'login with the gru'
String gruAccessToken = accessToken('gru', 'secret')
then:
gruAccessToken
when:
def resp = rest.get("http://localhost:${serverPort}/plan") {
accept('application/json')
header('Authorization', "Bearer ${gruAccessToken}")
}
then:
resp.status == 200
resp.json.toString() == '[{"title":"Steal the Moon"}]'
when: 'login with the vector'
String vectorAccessToken = accessToken('vector', 'secret')
then:
vectorAccessToken
when:
resp = rest.get("http://localhost:${serverPort}/plan") {
accept('application/json')
header('Authorization', "Bearer ${vectorAccessToken}")
}
then:
resp.status == 200
resp.json.toString() == '[{"title":"Steal a Pyramid"}]'
cleanup:
Tenants.withId("gru") {
planService.deleteByTitle('Steal the Moon')
}
Tenants.withId("vector") {
planService.deleteByTitle('Steal a Pyramid')
}
userService.deleteUser(gru)
userService.deleteUser(vector)
roleService.delete(VillainService.ROLE_VILLAIN)
}
}
1 | 验证存在新连接源 |
3.5 在启动时添加连接源
当应用程序重新启动时,我们要为每个已注册用户连接数据源。修改 BootStrap.groovy
以实现此目标
package demo
import groovy.transform.CompileStatic
import org.grails.orm.hibernate.HibernateDatastore
@CompileStatic
class BootStrap {
HibernateDatastore hibernateDatastore
DatabaseProvisioningService databaseProvisioningService
def init = { servletContext ->
for (DatabaseConfiguration databaseConfiguration : databaseProvisioningService.findAllDatabaseConfiguration() ) { (1)
hibernateDatastore.getConnectionSources().addConnectionSource(databaseConfiguration.dataSourceName, databaseConfiguration.configuration)
}
}
def destroy = {
}
}
4 运行测试
要运行测试
./grailsw
grails> test-app
grails> open test-report
或
./gradlew check
open build/reports/tests/index.html