显示导航

每个租户的数据库多租户

了解如何利用GORM的多租户功能使用每个租户的唯一数据库来构建应用程序

格雷姆·罗彻(Graeme Rocher)

365bet地区版本 3.3.0

365bet地区培训

365bet地区培训由创建并积极维护365bet地区框架的人们开发和交付

入门

365bet地区在本指南中,您将构建一个多租户应用程序,该应用程序通过365bet地区和GORM利用每个租户的数据库

每个租户的数据库允许您使用唯一的租户标识符将不同的租户用户重定向到不同的物理数据库

您将需要什么

要完成本指南,您将需要以下内容

  • 花些时间在你手上

  • 体面的文本编辑器或IDE

  • 安装了JDK或更高版本JAVA首页适当配置

如何完成指南

要开始,请执行以下操作

要么

365bet地区指南存储库包含两个文件夹

  • 初始初始项目通常是一个简单的365bet地区应用程序,其中包含一些其他代码,可以帮助您快速入门

  • 完成一个完整的示例它是按照指南中介绍的步骤进行操作并将这些更改应用于文档的结果。初始

要完成指南,请转到初始

  • 光盘进入grails指导每个租户初始的数据库

您可以直接前往如果你光盘进入grails指导每个租户完成的数据库

编写申请

由于该应用程序需要GORM x,因此第一步是在gradle属性:

gradle属性
grails版本3.3.0gormVersion6.1.6发布gradleWrapperVersion3.5

设置多租户

多租户模式

为了使用多租户,您需要设置GORM使用的多租户模式,因为它支持三种不同的模式

  • 数据库对每个租户使用不同的数据库连接

  • 施玛使用单个数据库,但每个租户使用不同的物理架构

  • 判别器使用单个数据库,但使用区分符列对数据进行分区

通常数据库施玛模式都可以认为是物理上分离的,而判别器模式需要更多的注意,因为不同的租户数据存储在同一物理数据库中

多租户模式

在这种情况下,所需的多租户模式为数据库可以使用grails gorm多租户模型设置

grails应用配置会议应用yml
ils:
    蓝色的:
        多租户:
            模式数据库tenantResolverClass组织grails数据存储区映射多租户Web SessionTenantResolver

租户解析器

请注意,除了上述模式外,以上示例还配置了tenantResolverClass用于解决租户

tenantResolverClass是实现租户解析器接口

GORM中包含一些内置租户解析器实现包括

表可用的TenantResolver实现
类型 描述

SessionTenantResolver

使用名为的属性从HTTP会话解析租户IDgorm tenantId

CookieTenantResolver

使用名为的属性从HTTP cookie解析租户IDgorm tenantId

SubDomainTenantResolver

解析当前子域中的租户ID,例如,如果子域为foo mycompany com租户ID为

SystemPropertyTenantResolver

通过名为的系统属性解析租户IDgorm tenantId主要用于测试

上面的实现非常有用,但是GORM是灵活的,您可以通过实施租户解析器接口

例如,如果您使用的是Spring Security,则可以编写一个租户解析器从当前登录的用户那里解析租户ID

在这个例子中,我们将使用SessionTenantResolver并在当前用户会话中存储租户ID

多个数据源

除了默认值数据源我们配置了两个附加数据源奥迪福特汽车对于每个租户

grails应用配置会议应用yml
数据源:
    派对: 真正
    jmxExport: 真正
    driverClassNameorg h驱动程序用户名她的密码: ''
    dbCreate创建放置: 数据库:H:记忆devDb MVCC真锁定超时10000DB退出时为假数据源:
    福特汽车:
      dbCreate创建放置: 数据库:H:记忆fordDb MVCC真锁定超时10000DB退出时为假奥迪:
      dbCreate创建放置: 数据库:H:记忆audiDb MVCC真锁定超时10000DB退出时为假

数据源的名称与配置的租户ID对应租户解析器应该回来

如果默认数据源也可以视为租户,则值ConnectionSources默认应该作为租户ID返回

创建域类

在为您的应用程序创建域类时,通常将具有多租户域域,而其他域域则不是

对于不会使用多租户的域类,只需像通常那样简单地定义它们,它们将被映射到默认值数据源.

对于这个例子制造商将是租户ID的提供者制造商将用作租户ID允许访问每个已配置的数据库

grails应用程序域示例制造商groovy
 制造商 {
    那么静态的约束名称空白: 
    }
}

下一步是定义只能由给定租户访问的域类

365bet地区应用程序域示例Engine groovy
进口 grails gorm多租户

 发动机 实施多租户 { (1)
    整数气瓶静态的约束圆柱可为空: 
    }
}
grails应用程序域示例Vehicle groovy
进口 grails gorm多租户

 车辆 实施多租户 { (1)
    模型整数静态的有很多引擎发动机静态的约束模型空白::1980
    }
}
1 两个域类都实现了多租户特征

车辆发动机域类都实现多租户特质,导致GORM解析数据库以使用从已配置的配置返回的结果租户ID中使用租户解析器.

设置测试数据

要设置一些测试数据,您可以修改应用类来实现ApplicationRunner启动时运行事务逻辑的界面

grails应用程序初始化示例应用程序groovy
进口 grails启动365bet地区App
进口 grails启动配置365bet地区AutoConfiguration
进口 org springframework boot ApplicationArguments
进口 org springframework boot ApplicationRunner
进口 grails gorm Transactions事务性
进口 常规转换CompileStatic

静态编译
 应用 延伸365bet地区365bet地区AutoConfiguration实施ApplicationRunner(1)

    静态的 虚空主要[]365bet地区args 365bet地区App运行应用程序args覆写
    交易性
    虚空运行ApplicationArguments args抛出 例外 { (2)制造商saveAll(3)
            制造商那么: '奥迪'),
            制造商那么: '福特汽车')
        )
    }
}
1 实施ApplicationRunner接口
2 标记与之交易的方法交易性
3 采用保存全部节省两个制造商实例

在有关两个的示例中制造商保存的实例将与此应用程序支持的两个租户相对应。奥迪福特汽车用于并与在中配置的数据源名称相对应grails应用配置会议应用yml.

实施租户选择

在应用程序中支持多租户的第一步是实现某种形式的租户选择。这可能是通过DNS子域解决租户,或者如果您通过Spring Security使用身份验证,那么这可能是您的应用程序注册过程的一部分

为了使示例简单,我们将实现一个简单的机制,该机制提供了一个UI来存储tenantId在用户HTTP会话中

首先创建一个新的制造商控制器采用创建控制器或您首选的IDE

grails创建控制器制造商

接下来修改UrlMappings常规文件将应用程序的根目录映射到指数行动

grails应用程序控制器示例UrlMappings groovy
'/'(控制者: '制造商')

然后定义一个指数列出所有制造商并呈现365bet地区应用程序观看次数索引gsp视图

365bet地区应用程序控制器示例ManufacturerController groovy
进口 组织grails数据存储区映射多租户Web SessionTenantResolver
进口 grails gorm交易ReadOnly
进口 常规转换CompileStatic

静态编译
 制造商控制器 {
    只读
    定义 指数渲染视图: '指数', 模型: [厂商制造商清单

365bet地区应用程序观看次数索引gsp文件只需遍历每个结果并创建指向选择行动

365bet地区应用程序观看次数索引gsp
<> ID="控制器" 角色="导航">
    

可用的制造商
    <> ="" ="厂商"> <> ="控制者"> <> 控制者="制造商" 行动="选择" ID="m名">m名

    选择操作选择当前租户并将该租户存储在当前用户的HTTP会话中

    365bet地区应用程序控制器示例ManufacturerController groovy
    只读
    定义 选择(id Manufacturer m制造商,名称ID优先(1)
        如果m session setAttribute SessionTenantResolver ATTRIBUTE m名称为LowerCase(2)重新导向控制者: '车辆' (3)
        }
        其他渲染状态: 404
        }
    }
    1 获取由供应商标识的制造商ID
    2 选定的租户存储在会话属性中

    选择动作会找到一个制造商并存储名称制造商小写,因此它对应于配置的数据源,作为HTTP会话中的当前租户

    这引起SessionTenantResolver从HTTP会话解析正确的租户ID

    最后,为了改善错误处理,您可以映射每次发生的TenantNotFoundException重定向回到制造商列表

    grails应用程序控制器示例UrlMappings groovy
    '500' (控制者: '制造商', 例外TenantNotFoundException

    完成这些更改后,您将可以从主页中选择每个租户

    可用的租户

    现在可以选择一个租户,让我们创建一个能够使用当前活动租户的逻辑

    编写多租户感知数据逻辑

    构建每个租户使用唯一数据库连接的应用程序时面临的挑战之一是,您需要以可伸缩的方式管理多个持久性上下文

    将每个租户的Hibernate会话绑定到应用程序中的每个请求都不会扩展,因此您必须能够编写逻辑,考虑到您正在使用Hibernate会话访问当前租户的事实。数据当前未绑定到当前控制器动作的执行

    为了使这一挑战变得更简单,GORM提供了一组多租户转换包含

    表多租户转换
    类型 描述

    当前租户

    解决当前租户并为该方法的范围绑定一个Hibernate会话

    承租人

    解决一个特定的租户并为该方法的范围绑定一个Hibernate会话

    没有租客

    在没有租户的情况下在方法中执行一些逻辑

    365bet地区这些通常应应用于365bet地区应用程序中的服务,并且当与GORM数据服务GORM中引入的概念

    实现保存和检索的逻辑车辆实例创建一个新的grails应用程序服务示例VehicleService groovy文件并在当前租户服务注解

    grails应用程序服务示例VehicleService groovy
    进口 grails gorm多租户CurrentTenant
    进口 grails gorm服务加入
    进口 grails gorm services服务
    进口 grails gorm Transactions事务性
    进口 常规转换CompileStatic
    
    服务车辆(1)
    当前租户 (2)
    静态编译
    抽象  车辆服务 {
    }
    1 服务转换将确保可以实施GORM可以实现的任何抽象方法
    2 当前租户转换将确保在服务上执行的任何方法都首先解析当前的租户,并为解析的数据库连接绑定一个Hibernate会话
    这个班是抽象因为GORM将为您实施许多方法

    现在让我们看一下如何为多租户应用程序实现查询逻辑

    执行多租户感知查询

    要在GORM数据服务中实现多租户查询,只需添加对应于GORM中受支持的约定之一:

    grails应用程序服务示例VehicleService groovy
    加入('引擎') (1)
    抽象 清单清单地图args(2)
    
    抽象 整数计数(3)
    
    加入('引擎')
    抽象车辆找可序列化ID(4)
    1 每个查询方法都用注释加入
    2 清单方法返回的列表车辆实例并接受可选参数作为执行分页的映射
    3 计数方法计数车辆实例
    4 方法找到一个车辆通过ID

    的用法加入需要进一步的解释回想一下,在多租户应用程序中,将为找到的当前租户ID的连接创建一个新的Hibernate会话。

    一旦方法完成,此会话将关闭,这意味着查询未加载的任何关联都可能导致LazyInitializationException由于闭门会议

    因此,至关重要的是,您的查询始终返回呈现视图所需的数据。这通常无论如何都会导致性能更好的查询,并且实际上将帮助您设计性能更好的应用程序

    加入批注是实现联接查询的一种简单方法,但是在某些情况下,使用起来可能更简单JPA QL查询.

    现在是时候编写一个可以使用这些新定义的方法的控制器了,首先创建一个新的grails应用程序控制器示例VehicleController groovy创建控制器命令或您首选的IDE

    车辆控制器应该定义一个引用先前创建的属性车辆服务:

    grails应用程序控制器示例VehicleController groovy
    进口 静态的 org springframework http HttpStatus未找到
    进口 grails gorm多租户CurrentTenant
    进口 验证ValidationException
    进口 常规转换CompileStatic
    
    静态编译
     车辆控制器  {
    
        静态的allowedMethods保存: '开机自检', 更新: '', 删除: ''车辆服务车辆服务

    现在运行grails产生视图生成一些默认的GSP视图,可以呈现车辆实例

    grails生成视图示例车辆

    接下来,将条目添加到UrlMappings常规文件映射汽车URI

    grails应用程序控制器示例UrlMappings groovy
    '汽车'(资源: '车辆')

    现在您准备添加查询逻辑以读取车辆每个租户更新的实例车辆控制器具有以下读取操作

    grails应用程序控制器示例VehicleController groovy
    定义 指数(整数最大参数最大数学最小最大10, 100响应车辆服务清单参数模型: [车辆数量车辆服务计数定义 节目(longid车辆车辆id vehicleService查找ID空值响应车辆

    这俩节目动作使用车辆服务定位车辆实例车辆服务将确保解决正确的租户并为每个租户返回正确的数据

    执行多租户更新

    要添加执行写操作的逻辑,您只需修改车辆服务并添加新的抽象方法保存删除:

    grails应用程序服务示例VehicleService groovy
    抽象节省车辆模型整数抽象车辆删除可序列化ID

    以上保存删除方法将为您自动实现

    GORM数据服务很聪明,例如可以为每种方法添加适当的事务语义只读用于读取操作但是,您可以通过添加交易性注释自己

    要实现更新,您可以添加调用现有抽象的新方法方法

    grails应用程序服务示例VehicleService groovy
    交易性车辆更新可序列化ID(5)
                    模型整数年份车辆车辆ID如果车辆空值车辆型号车辆年份年份车辆节省failOnError:真正车辆

    这表明了GORM数据服务的重要概念。可以轻松地将定义的方法与GORM自动为您实现的方法混合使用

    相应的控制器动作调用车辆服务并公开这些写操作也是微不足道的

    grails应用程序控制器示例VehicleController groovy
    定义 保存(模型整数尝试车辆车辆车辆保存模型年份Flash消息'车辆已创建'重定向车辆抓住ValidationException e响应e错误视图: '创建'
        }
    }
    
    定义 更新(longID模型整数尝试车辆车辆服务更新编号型号年如果车辆空值未找到其他即时消息'车辆更新'重定向车辆抓住ValidationException e响应e错误视图: '编辑'
        }
    }
    
    受保护的 虚空notFound Flash消息'找不到车辆'重新导向小号: '汽车', 状态未找到定义 删除(longid Vehicle vehicle vehicleService删除ID如果车辆空值未找到其他即时消息'车辆已删除'重新导向行动: '指数', 方法: '得到'
        }
    }

    多租户单元测试

    测试使用多租户的控制器逻辑需要特殊考虑

    幸运的是,GORM使编写单元测试相对简单

    为该单元编写单元测试车辆控制器类创建一个新的src测试groovy示例VehicleControllerSpec groovySpock规格

    src测试groovy示例VehicleControllerSpec groovy
    逐步地
     VehicleControllerSpec 延伸休眠规范实施ControllerUnitTest {
            ...
    }

    如您所见,测试扩展了休眠规范.

    为了简化测试,请覆盖tenantResolverClass通过覆盖getConfiguration的方法休眠规范:

    src测试groovy示例VehicleControllerSpec groovy
    覆写
    地图getConfiguration设置设置多租户解析类SystemPropertyTenantResolver

    这将允许您使用SystemPropertyTenantResolver用于在测试中更改租户ID

    下一步是提供设定配置车辆服务对于控制器

    src测试groovy示例VehicleControllerSpec groovy
    车辆服务车辆服务(1)
    定义 设定() {
        系统setProperty SystemPropertyTenantResolver属性名称'奥迪') (2)vehicleService hibernateDatastore getService VehicleService(3)控制器车辆服务车辆服务(4)
    }
    1 定义一个vehicleService作为单元测试的属性
    2 将租户ID设置为奥迪为了测试的目的
    3 查找车辆服务从GORM实施
    4 分配车辆服务到被测控制器

    为了确保适当的清理,您还应该在清理方法

    src测试groovy示例VehicleControllerSpec groovy
    定义 清理() {
        系统setProperty SystemPropertyTenantResolver属性名称'')
    }

    完成后,测试控制器逻辑(例如测试指数没有数据的行动

    src测试groovy示例VehicleControllerSpec groovy
    虚空 '测试索引动作是否返回正确的模型'() {
    
        什么时候: '执行索引动作'控制器索引然后: '型号正确'型号车辆列表型号车辆计数0
    }

    您还可以通过清除租户ID编写测试以测试不存在租户ID的情况

    src测试groovy示例VehicleControllerSpec groovy
    虚空 '测试没有租户ID的索引操作'() {
        什么时候: '没有房客ID'
        系统setProperty SystemPropertyTenantResolver属性名称''控制器索引然后抛出TenantNotFoundException

    也可以测试更复杂的交互,例如保存数据

    src测试groovy示例VehicleControllerSpec groovy
    虚空 '测试保存操作是否正确保留实例'() {
    
        什么时候: '使用无效实例执行保存操作'请求contentType FORM CONTENT TYPE请求方法'开机自检'控制器保存'', 1900)
    
        然后: '使用正确的模型再次渲染创建视图'模型车空值视图'创建'
    
        什么时候: '使用有效实例执行保存操作'响应重置控制器保存'一种', 2011)
    
        然后: '将重定向发送到show操作'响应redirectedUrl'汽车'控制器刷新消息空值车辆服务计数1
    }

    请注意,在上述测试的断言中,我们使用vehicleService这可以确保在进行断言时使用正确的数据库连接

    功能测试

    我们借助Geb Pages映射CRUD页面

    src集成测试groovy示例ManufacturersPage groovy
    进口 给页面
    
     厂商页面 延伸静态的$('H'文字包含'可用的制造商') }
    
        静态的内容audiLink$('一种', 文本: '奥迪'福特$('一种', 文本: '福特汽车') }
        }
    
        虚空选择奥迪audiLink请点击虚空选择福特福特链接,请单击
    src集成测试groovy示例NewVehiclePage groovy
    进口 给页面
    
     新车页面 延伸静态的标题中包含'创建车辆') }
    
        静态的内容输入模型$('输入', 那么: '模型'输入年份$('输入', 那么: ''createButton$('输入', 那么: '创建') }
        }
    
        虚空新车模型整型年输入模型模型输入年创建按钮
    src集成测试groovy示例ShowVehiclePage groovy
    进口 给页面
    
     ShowVehiclePage 延伸静态的标题中包含'表演车') }
    
        静态的内容列表按钮$('一种', 文本: '车辆清单') }
        }
    
        虚空车辆列表列表按钮单击
    src集成测试groovy示例VehiclesPage groovy
    进口 给页面
    
     车辆页面 延伸静态的标题中包含'车辆清单') }
    
        静态的内容newVehicleLink$('一种', 文本: '新车'$('身体') }
        }
    
        虚空新车新车链接请点击整型车辆数量行大小

    我们借助功能测试来测试租户选择

    src集成测试groovy示例TenantSelectionFuncSpec groovy
    进口 geb spock GebSpec
    进口 grails测试mixin集成集成
    
    积分
     租户选择功能规格 延伸GebSpec定义 "可以更改租户并获得不同的车辆清单"() {
    
            什么时候'/'
    
            然后在制造商页面什么时候选择奥迪然后在VehiclesPage什么时候新车然后在NewVehiclePage什么时候新车'一种', 2000)
    
            然后在ShowVehiclePage什么时候车辆清单然后在VehiclesPage numberOfVehicles1
    
            什么时候新车然后在NewVehiclePage什么时候新车'一种', 2001)
    
            然后在ShowVehiclePage什么时候车辆清单然后在VehiclesPage numberOfVehicles2
    
            什么时候'/'
    
            然后在制造商页面什么时候选择福特然后在VehiclesPage什么时候新车然后在NewVehiclePage什么时候新车'KA', 1996)
    
            然后在ShowVehiclePage什么时候车辆清单然后在VehiclesPage numberOfVehicles1
        }
    
    }

    运行应用程序

    要运行该应用程序,请使用gradlew bootRun命令将在端口上启动应用程序

    现在执行以下步骤

    1. 导航到主页并选择Audi

    2. 输入数据车辆创建一个新的车辆

    3. 请注意,数据将在奥迪数据库

    如果您随后返回首页并选择“福特”,则当前租户已切换,您可以看到,如果您查看“福特汽车”的数据,则该应用程序现在正在使用福特汽车数据库有效隔离了两个租户之间的数据

    您需要365bet地区帮助吗

    OCI赞助了本指南的创建OCI提供了几种365bet地区服务:

    免费咨询

    OCI 365bet地区团队包括365bet地区联合创始人Jeff Scott Brown和Graeme Rocher检查我们的365bet地区课程并向发展和维护365bet地区的工程师学习

    Grails OCI团队