在使用 DATABASE 分库分表时动态配置数据源
在为每个注册用户动态创建新的数据源连接时,了解如何使用 Grails 分库分表功能 DATABASE 模式。
作者:Sergio del Amo
Grails 版本 4.0.1
1 Grails 培训
Grails 培训 - 由创建并积极维护 Grails 框架的人员开发和提供!
2 开始
此指南从 基于 JWT 的自定义租户解析器 结束的地方开始。在开始本指南之前,请完成该指南以获得更好的理解。
2.1 需要了解知识
要完成此指南,您需要以下知识
-
一些时间
-
一个好的文本编辑器或 IDE
-
安装了 JDK 1.8 或更高版本,并已正确配置
JAVA_HOME
2.2 如何完成指南
要开始,请执行以下操作
-
下载并解压源文件
或者
Grails 指南仓库包含两个文件夹
-
initial
初始项目。通常是一个简单的 Grails 应用程序,其中包含一些额外的代码,以便您快速入门。 -
complete
一个已完成的示例。它是完成指南介绍的步骤并将这些更改应用于initial
文件夹的结果。
要完成指南,请转到 initial
文件夹
-
cd
到grails-guides/grails-dynamic-multiple-datasources/initial
并遵循下一部分中的说明。
如果你 cd 到 grails-guides/grails-dynamic-multiple-datasources/complete ,你可以直接转到已完成的范例 |
3 编写应用程序
本指南使用多租户数据库模式。要了解更多信息,请阅读每个租户数据库多租户指南。 |
本指南显示了使用多租户DATABASE
模式创建 SaaS(软件即服务)应用程序时会看到的典型流程。
为了简化本指南,数据库 provisioning 过程被简化了。
使用以下模式创建两个 MySQL 数据库。
CREATE TABLE `plan` (
`id` bigint(20) NOT NULL AUTO_INCREMENT primary key,
`version` bigint(20) NOT NULL,
`title` varchar(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第一个数据库应该称为gru
,第二个应称为vector
。
在指南应用程序中,当用户注册时,便会为该用户按其用户名开通一个数据库,因此每个注册用户将拥有自己独特的数据库。
在实际应用程序中,你可能需要设置更复杂的流程,其中可能涉及创建数据库、创建模式、将每个用户数据库的数据库 URL 和凭据安全地保存在默认数据源的数据库表中,等等。
3.1 配置
本指南将使用 MySQL,因此应将 MySQL 依赖添加到build.gradle
。
runtime 'mysql:mysql-connector-java:5.1.48'
在 MySQL 数据库中创建一个名为minions
的数据库。
在application.yml
中只配置默认数据源以指向此数据库。此数据库的模式将由 Hibernate 生成,因为配置了dbCreate: update
。
每个应用程序用户的 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)
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.testing.mixin.integration.Integration
import grails.testing.spock.OnceBefore
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import org.grails.orm.hibernate.HibernateDatastore
import org.springframework.beans.factory.annotation.Autowired
import spock.lang.Shared
import spock.lang.Specification
import spock.lang.IgnoreIf
@IgnoreIf( { System.getenv('CI') as boolean } )
@Integration
class PlanControllerSpec extends Specification {
PlanService planService
UserService userService
VillainService villainService
RoleService roleService
@Autowired
HibernateDatastore hibernateDatastore
@Shared HttpClient client
@OnceBefore
void init() {
String baseUrl = "http://localhost:$serverPort"
this.client = HttpClient.create(baseUrl.toURL())
}
String accessToken(String u, String p) {
HttpRequest request = HttpRequest.POST('/api/login', [username: u, password: p])
HttpResponse<Map> resp = client.toBlocking().exchange(request, Map)
if ( resp.status == HttpStatus.OK ) {
return resp.body().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:
HttpRequest request = HttpRequest.GET('/plan').bearerAuth(gruAccessToken)
HttpResponse<String> resp = client.toBlocking().exchange(request, String)
then:
resp.status == HttpStatus.OK
resp.body() == '[{"title":"Steal the Moon"}]'
when: 'login with the vector'
String vectorAccessToken = accessToken('vector', 'secret')
then:
vectorAccessToken
when:
request = HttpRequest.GET('/plan').bearerAuth(vectorAccessToken)
resp = client.toBlocking().exchange(request, String)
then:
resp.status == HttpStatus.OK
resp.body() == '[{"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