作为业务系统技术开发同学,面向当下:首先应该是快速搭建业务通路,让线上业务跑起来,快速试错,解决生存问题;第二步是在链路通了,业务基本跑起来的基础上如何支撑业务跑更快,解决快速增长问题;第三步,在完成支撑业务快速增长的基础上,要进行精细化提升,通过在支撑业务快跑间隙挤时间打磨系统功能和体验,踏踏实实花时间,抽象能力,沉淀产品,提升效能。
同时,我们也必须面向未来,如何在抽象能力以及沉淀了产品的基础上,如何把所承载和沉淀的业务能力快速输出,贡献给整个行业,抑或为整个社会商业生态提供基座支撑。那么面向未来,将平台产品进行SAAS化升级真正将能力进行有价值开放输出是我们提前要布局的核心方向。
那么将平台产品进行SAAS输出,需要解决那些问题呢?这里尝试把核心问题列举一下:
1. 如何根据不同用户需求进行计算能力按需调度分配?(IAAS/PAAS)
2. 如何满足用户数据安全性要求,严格隔离不同用户的数据,使用户只能看到自己的数据?(PAAS)
3. 如何支持不同用户在标准的数据对象/数据模型上按需添加定义自定义的数据对象/扩展模型?(PAAS & SAAS)
4. 如何按照不同用户进行按需功能搭配组合,满足不同用户从基础到专业级不同业务场景需求?(SAAS)
5. 如何统一对平台产品进行升级而不影响用户已有数据及功能?(IAAS、PAAS、SAAS)
通过以上问题,我们可以看出产品SAAS化输出的关键是如何对不同的用户通过标准+扩展能力按需进行算力、数据、安全、功能有效定制,支持多用户共性和个性的问题,也暨多租户的问题,同时也涉及到计费和服务水平等相关问题。我们下面来聊下上述问题的解题关键和解题思路
1. 第1个算力的问题核心是调度问题,弹性计算提供在IAAS层的统一算力调度能力,而Serverless则可以在PAAS层提供更高层次的算力调度能力。
2. 第4个问题的核心是业务流程的抽象和业务功能的拆分,领域驱动的设计以及服务化(微服务)在平台功能抽象拆分提供了相对成熟的思路,催化了以纵向业务功能细分作为域划分的依据的服务化方案以及组织结构,主要诉求是在细分的业务功能服务基础上,能按需快速灵活的组合支撑不同的业务模式,提供业务敏捷性,支撑业务创新求变。
当然放过来,由于纵向功能细分,业务功能域增多,整个业务链条上的咬合点越来越多,随之产生越来越多的数据来源冗余重复或者缺失,功能或者重合且各自发散,或者缺失,最终给整体业务带来较多数据和功能的不一致性风险,这样一来不仅横向端到端的业务串联成本高,而且关键路径的风险收敛成本比较高,矛盾冲突点集中在各纵向域功能和数据咬合处,具体表现为:
数据上:1.无主数据,有数据需求无owner,2.大量重复且不一致数据,
功能上:3.部分业务功能缺失,4.域之间存在业务功能重复且行为不一致。
到底是纵向切分域还是横向分业务模式拉平来做,这个问题没有标准答案,更没有最佳答案,只有根据不同的业务发展阶段及时动态调整试错,是一个不断寻找相对最优解的动态过程
3. 弹性计算和Serverless解决了算力的问题,领域驱动服务化设计解决了功能的拆分和按需搭配组合的问题,那么剩下核心问题就是数据了:如何以一套统一的数据架构即能支撑多租户的数据安全性需求以及通用的数据存储,也能支撑用户扩展的自定义数据对象定义和模型变更,同时也要保证数据定义层面的扩展和变更不会影响自身和其他租户的业务功能的可用性。我们来分析下可能的方案(暂不考虑按服务边界进行数据库拆分):
1. 统一的数据库,标准数据模型和扩展数据模型直接映射到物理表和索引:很显然对于不同的租户自定义的数据对象和数据模型要求是无法支撑的,物理数据模型会相互干扰,相互冲突直到无以为继。即使是对于所有租户完全标准的功能和数据存储,平台自身的标准模型的升级的DDL也会对用户的可用性造成较大影响,所以显然是行不通的。
2. 如果为每个租户创建各自的数据库呢?各自租户拥有各自的数据库,可以满足用户数据安全隔离的需求,也可以满足各租户自定义的数据需求,看上去像是一种合理的SAAS数据方案,但是仔细分析,发现有两个明显的问题:
1. 如果用户需要修改或者扩展现有物理数据模型而进行的DDL操作,必然会影响线上业务的整体可用性,也可能会影响到标准数据模型,从而影响到线上功能使用。
2. 如果用户可自定义对物理模型进行扩展和定制,当平台进行模型升级的时候,极容易产生物理模型的冲突,导致新旧功能异常。
3. 由于用户在各自数据库存在各自定义的扩展和定制,则平台数据模型和功能升级,需要针对不同的租户进行分别验证,存在着极大的升级验证工作量和风险。
以上两种方案可行性低,我们从其中发现的问题是平台业务系统的逻辑模型到物理模型的直接映射是造成问题的主要因素。既然物理模型的变更是平台不稳定的动因,那么我们是否通过解耦业务逻辑模型和物理模型的映射关系来尝试解决这个问题呢?
既然问题已经定义清楚了,如何解决这个问题呢?通常我们解决架构问题的一个“万能”的方法是:增加一个层次,我们也来套用一次,增加一个层次(元数据层)来解耦逻辑模型到物理模型强映射的问题:
1. 首先,我们需要对业务进行建模,对业务进行抽象,定义出业务逻辑模型,然后对模型进行二次抽象,定义出逻辑模型的定义数据,实现业务模型的数据化,也暨模型的元数据(the metadata of the logic model ),将模型结构存储为数据,而不是直接对应的物理存储结构。
2. 其次根据定义出的元数据,也就是对数据对象定义数据,数据对象数据内容数据的存储结构进行统一抽象,形成元数据逻辑模型
3. 将元数据逻辑模型映射到元数据物理模型,对应实际存储结构
4. 通过对业务模型的变更变成了对元数据层的数据的变更,而不是物理结构的变更,来实现业务逻辑模型同物理模型的解耦。

很多事情说起来好像挺简单,实际上是一个非常巨大的系统工程,将其付诸实践是挑战非常大的事情,而取得踏踏实实的成功更难,上述的解题思路是Salesforce的解题思路,而且Salesforce 不仅取得了成功,而且接近将其做到的极致,下面我们站在巨人的肩膀上来看看Salesforce如何通过元数据驱动的架构(核心是基础数据架构)来支撑多租户的SAAS业务平台的。注意:由于Salesforce并未有对核心实现逻辑进行完全公开和说明,所以本文所整理的部分核心逻辑包含了作者的逻辑推理和解读,但是确实进行了逻辑验证和场景验证,如有纰漏和不尽全的地方,欢迎讨论及指正。
元数据驱动的多租户架构
Salesforce将Force.com定义为PAAS平台,Force.com的基础就是元数据驱动的软件架构来支撑多租户应用。下面将元数据驱动的软件架构作为核心进行介绍。
一、多租户意味着什么?
多租户的含义用一句话来描述就是:一个云平台,无数多个客户。
一个云平台的含义是:一个代码库,一个数据库,一整套共享的可扩展服务包括数据服务、应用服务以及Web服务。
无数多个客户的含义是:每个客户都被分配一个唯一的租户OrgID,所有的数据存储都是按照租户OrgID隔离的,所有的数据访问必须包含OrgID,所有的操作也都是包含租户OrgID的,也就是所有的客户数据和行为都是被安全的通过唯一的租户Org进行严格的隔离的。
每个租户/组织只能看到和定义按照自己租户OrgID隔离的它自己版本的元数据和数据,而且只能执行自己租户OrgID所授权的行为,这样每个租户就拥有各自版本的SAAS方案。
二、元数据驱动意味着什么
元数据对于平台意味着平台数据的数据,对于租户意味着是关于租户数据的数据,
当用户定义一个新的用户表的时候,用户创建的不是数据库中的物理表,而是在系统态的元数据表中添加了一条记录,这个记录描述的是用户表的逻辑定义,是虚拟的,这个表并不在数据库中物理存在,而这条记录代表就是用户态的数据表。
当用户定义了用户表的一个新的字段时,用户并没有在物理表中创建物理字段,而是在系统态的元数据表中添加了一个记录,这个记录描述的用户表的字段组成的逻辑结构,是虚拟的,这个字段也不再数据库中表结构中物理存在,而这条记录代表的就是用户态的用户表字段。
也就是通过存储在系统态的元数据表的元数据记录来作为虚拟用户的数据库结构。
三、元数据驱动的多租户整体架构
我们先来大概了解下元数据驱动的多租户架构的整体架构,整体架构大概分为5个大的逻辑层次:
1. 底层数据架构分为三个层次:
1. 最底层是数据层,存储了离散的系统和用户的业务数据,业务日常运营的数据存储在这里。
2. 公共元数据层,存储了应用系统标准的对象和标准的字段定义,对底层数据的结构进行定义说明
3. 租户特定元数据,存储了租户自动的对象和自定义的字段的定义,用于对底层的数据的结构进行定义说明。
2. 通用数据字典UDD(Universal Data Dictionary)运行引擎层实现了应用对象到底层数据存储的映射,包含对象模型操作、SOQL语言解析、查询优化,全文搜索等功能,我们常说的ORM功能也是其核心功能,但比其复杂的多。
3. 平台服务层,提供PAAS层平台服务,提供应用对象模型的创建,权限模型创建,逻辑和工作流程创建以及用户界面的创建包括屏幕布局,数据项,报表等
4. 标准应用层,提供端到端的标准的业务应用功能。
5. 租户虚拟应用层,用户可以在标准应用层或者平台服务层之上定义自己特有的业务应用功能,来满足自己特定的业务场景需要。

其中底层数据架构为最为关键的平台基石(the corner stone),其核心运行引擎也是基于强大的底层数据架构基础上构建的。本文则以元数据驱动的多租户数据架构为核心来一一展开。
四、元数据驱动的多租户数据架构
下面我们具体来看下系统态的数据模型,基于Salesforce加上个人推理的元数据驱动的多租户数据模型。注意:由于Salesforce并未有对核心逻辑进行完全公开和说明,所以本文所整理的部分核心模型包含了个人的逻辑推理和解读,但是确实进行了逻辑验证和场景验证,如有纰漏和不尽全的地方,欢迎讨论及指正。
Salesforce云服务平台遵循的是面向对象的设计理念,所有的实体、实体关系以及实体的CRUD均是以对象的视角来进行的,所以其元数据驱动的多租户数据模型的存储基本元素也是按照对象的颗粒度进行存储,源自与OO的对象间引用,同普通关系数据库主外键关系异曲同工,只是细节处理上不尽相同,请大家注意这一点。
1. 元数据驱动的多租户数据架构概览
首先,我们先来大概了解下元数据驱动的多租户模型的核心内容,元数据驱动的多租户的数据模型主要分为三个部分:元数据表、数据表和功能透视表。
1. 元数据表(Metadata Tables)
元数据表用于存放系统标准对象以及用户自定义对象和字段的定义的元数据,也就是系统和用户对象的逻辑结构暨对应于关系数据库中的虚拟表结构。元数据表主要包括Objects表以及Fields表,是系统标准对象和用户对象定义数据的仓库,元数据仓库。
2. 数据表(Data Tables)
数据表用户存放系统以及用户对象和字段的实际数据,实际的用户业务数据以及应用系统相关数据存放在这里。数据表包括Data表和存放大文本数据的Clob表。数据表存储了绝大部分用户的实际数据,是一个巨大的用户业务数据仓库。
3. 功能透视表(Specialized Pivot Tables)
功能透视表包含了非常关键的关系表、索引表、关系表以及其他特定用途表。例如关系表定义了对象间的关系,索引表解决虚拟结构索引的问题,后续进行详尽的叙述。

2. 元数据驱动的多租户数据架构详解
上一节粗略地描述了元数据驱动的多租户模型三大部分模型实体和基本作用,大家可能会比较疑惑这么简单一个实体模型,怎么就起了这么个牛逼的名字,而且支撑了“一个云平台,无数个客户”,我们下面就对此模型的核心逻辑进行详细展开和推理说明,同时详细阐述以此模型为中心的服务来说明整个元数据层或者说UDD(Universal Data Dictionary)层的设计。
土话说:“没有对比,就没有伤害”,道理是想通的,用相似的事物进行对比是对理解客观事物进行了解比较好的方法,找出其相同点,共性的地方,找出其不同点,异样的地方,同时识别出是否有不可对比的方面,从各个方面去对比,则能更全面、更深入的了解客观事物。
下面我按照普通应用设计思路方式来定义一个简单直观的多租户SAAS数据架构方案示例,作为元数据驱动多租户数据架构方案的对比基准方案,用对比来更好的帮大家了解元数据驱动多租户数据模型及架构的设计逻辑。
1. 普通多租户SAAS数据架构方案示例(仅做示例)
1. 多租户基本思路:每个租户一个数据库,提供数据库级别的租户数据隔离,平台提供标准应用功能模型,用户可以在各自数据库内定义以及修改各自的定义模型,所有模型采用数据库物理表、索引,主外键实现。不同的租户通过路由到不同的数据库来实现隔离。
2. 域模型样例采用大家都熟悉的最小集的订单模型实现,包含商品、用户、订单和订单详情表。注意:此简化模型仅用做示意说明,和意图无关大多数字段均省略,非严谨定义。

3. 示例模型数据
1. 数据库物理表数据:Customer
Demo Table |
Customer |
||||
---|---|---|---|---|---|
CustomerID |
CustomerNo |
FirstName |
LastName |
NickName |
LoginName |
CI00000000000000000001 |
CI200903091014A0000001 |
Yan |
Cheng |
cy |
chengyan |
CI00000000000000000002 |
CI200903091014A0000002 |
Jun |
Ling |
lj |
lingjun |
CI00000000000000000003 |
CI200903091014A0000003 |
Tommy |
Valdles |
tom |
Tommy |
CI00000000000000000004 |
CI200903091014A0000004 |
Dorothy |
Franklin |
doro |
Dorothy |
2. 数据库物理表数据:Product
Demo Table |
Product |
|||||
---|---|---|---|---|---|---|
ProductID |
ProductNo |
ProductName |
ProductPrice |
Currency |
ProductStatus |
CreatedTime |
PI00000000000000000001 |
PI201901060930A0000001 |
IPhone8 256G Golden |
6000 |
CNY |
Online |
2018/7/9 19:14 |
PI00000000000000000002 |
PI201901060930A0000002 |
IPhoneX 256G Golden |
10000 |
CNY |
Online |
2018/7/10 19:14 |
PI00000000000000000003 |
PI201901060930A0000003 |
IPhoneXR 256G Golden |
8000 |
CNY |
Online |
2018/7/11 19:14 |
3. 数据库物理表数据:Order
Demo Table |
Order |
|||
---|---|---|---|---|
OrderID |
OrderNo |
CustomerID |
OrderStatus |
OrderTime |
OI00000000000000000001 |
ON201903091914A0000001 |
CI00000000000000000001 |
Completed |
2019/3/9 19:14 |
OI00000000000000000002 |
ON201903091914A0100003 |
CI00000000000000000002 |
Completed |
2019/3/9 19:14 |
OI00000000000000000003 |
ON201903091914A0200005 |
CI00000000000000000004 |
Canceld |
2019/3/9 19:14 |
4. 数据库物理表数据:OrderItem
Demo Table |
OrderItem |
|||||
---|---|---|---|---|---|---|
OrderItemID |
OrderID |
ItemID |
ItemPrice |
ItemCurrency |
ItemQuantity |
OrderItemStatus |
OII0000000000009835101 |
OI00000000000000000001 |
PI00000000000000000001 |
5888 |
CNY |
2 |
InProduction |
OII0000000000009835102 |
OI00000000000000000001 |
PI00000000000000000002 |
9888 |
CNY |
3 |
Canceled |
OII0000000000009997401 |
OI00000000000000000002 |
PI00000000000000000003 |
7888 |
CNY |
1 |
Completed |
OII0000000000009998702 |
OI00000000000000000003 |
PI00000000000000000003 |
7888 |
CNY |
1 |
Canceled |
4. 实体表关系
Order表同OrderItem为父子表,通过OrderID进行主外键关联;Customer表同Order表为父子表,通过CustomerID进行主外键关联;Product表同OrderItem表为父子表,通过ProductID进行主外键关联。
5. 用户自定制
用户有执行DDL权限,可以在自己租户数据库内在进行扩展模型自定义,建立自定义的物理表,索引,关系等。
6. 问题和风险
1. 用户具有执行DDL权限,可以自定义数据库物理模型,会带来各租户的自定义数据模型大爆炸,会给后续平台模型定义升级冲突,造成模型升级的巨大的障碍
2. 同时由于系统标准模型和用户模型均为物理模型,未有做系统标准和自定义数据的有效隔离,如何保证平台应用的每一次升级必然会考虑对现有用户自定义模型的稳定性和可用性的影响,在自定义物理模型的情况下,不仅挑战巨大,而且包含巨大的回归验证的工作量,很难收敛。
3. 当用户执行DDL时,通常会锁定数据库物理资源,当数据库数量非常巨大时可能会带来不可控的downtime,对应用系统的可用性造成加大影响。如果数据库是每个租户各自独占的还只会影响到单个租户,但是如果是多租户共享数据库,则可能会影响到其他租户,影响是灾难性的。作为云平台服务商,不管是用户操作还是系统行为,我们都不期望我们的设计对用户系统的可用性造成影响,所以用户执行DDL的行为是否允许确实有待商榷,但是如果不允许,用户可扩展性在这种设计环境中必然受到一定程度的限制。
2. 元数据驱动的多租户数据模型(Metadata Tables)
前面章节描述了元数据驱动的多租户模型简单模型图,本小节详细解说下每个核心实体表的核心结构,同时已知资料部分较为简略,无法描述模型全貌和核心细节,为了模型完整性,整体数据模型包含了作者思路推理部分,用以来完整清晰地定义模型。当然由于所有模型都是主观的(subjective),仅代表个人观点,欢迎大家的不同的观点,一起讨论改进。
正如前面”一个云平台“的提到的通过一个统一的数据库来支撑无数个租户,所以元数据驱动的多租户模型是基于一个共享数据库的前提下。当然多租户实现设计多种多样,大家可以不拘泥此种。
1. 元数据表之对象定义表:Objects表

Object系统表存储了每个租户为它的扩展应用对象定义的元数据,包含如下核心字段:
1. ObjID:应用对象唯一标识,具有固定长度和格式。
2. OrgID: 应用对象所归属的租户ID,用于统一共享数据库内的多租户数据隔离,通常和租户定义的域名对应。
3. ObjName/Name: 对象名称,用于系统配置和开发(developer name)。
4. Label: 对象的显示名称。
除了用户自定义对象,系统的标准对象也是采用相同的方式进行定义的。
2. 元数据表之字段与关系定义表:Fields表

Fields系统表存储了每个租户为他的扩展应用对象字段定义的元数据,包含了其所归属的应用对象的租户OrgID,字段所属对象的ObjID,字段定义标识FieldID,字段名称FieldName,字段存储位置定义FieldNum,数据类型DataType,数据类型重要补充关联字段(DigitLeft,Scale,TextLength,RelatedTo,ChildRelationshipName)以及是否必选、唯一、索引标记,还有部分标准字段。Fields表非常关键,其不仅定义了普通的应用对象字段定义包括基本信息和数据类型信息,而且通过特殊关系字段对不同应用对象之间的关系进行了定义,详细说明如下:
1. FieldID: 此对象字段的唯一标识,具有固定长度和格式
2. OrgID: 其所归属的应用对象所归属的租户OrgID
3. ObjID:字段所属对象的ObjID
4. FieldName/Name: 字段名,用于系统配置和开发(developer name)。
5. Label: 字段展示名称,用以展示给最终用户。
6. FieldNum:对应到Data数据表的数据存储字段映射,暨Data表中ValueX字段中的X。
7. DataType:指定此对象字段的数据类型包含普通类型:Number、TEXT、Auto Number、Date/Time、Email、Text Area等,也包含特殊的关系类型如:Look up关系类型、Master-Detail关系类型等。
8. DigitLeft和Scale:用于Number、Currency、Geolocation等数字数据类型的关联设定,例如定义了一个字段的DateType为Number,则需要指定其整数部分的最大位数DigitLeft和小数部分的最大位数Scale,两部分长度总和不超过18位。
9. TextLength:当数据类型为TEXT时启用,用于指定TEXT类型的字符的长度限制。
10. RelatedTo和ChildRelationshipName:这两个字段当DateType为关系类型(Look up,Master-Detail等)时会启用,其中RelatedTo保存关联的应用对象ID,ChildRelationshipName用于保存父子关系中子方的关系名称,同一个父对象的子方的关系名称唯一,用于关系的反向查询。
11. IsRequired:此字段数据保存时,是否校验值的存在
12. IsUnique: 是否允许重复值
13. IsIndexed:此字段是否需要建索引
14. 其他字段:此处仅列举了说明模型所需要的字段,其他字段暂不进行列举,不列举原因和其重要性并无直接关联。
3. 数据表(Data Tables)之关系数据表:Data表

MT_Data系统表存储了MT_Objects和MT_Fields元数据表内定义的数据对象(表)所对应的数据,一一映射到不同的租户各自定义的表和表中的字段(对象和对象字段)。
1. GUID:数据表的主键,用于存放每个应用对象实例的标识ID。
2. ObjID: 其所归属的应用对象所归属的租户OrgID
3. Name: 应用对象实例名称
4. Value0…Value500: 用于存放对象实例字段的数据,其ValueX中X值对应到Fields表中FieldNum定义,ValueX存放的数据,不管原始数据类型,存储格式均为变长字符串格式。
4. 数据表(Data Tables)之非结构化数据表:CLobs
MT_Clobs用于存储大字符段的存储CLOB,同时CLOB的也存储在数据库外的索引结构中,用于快速的Full-Text文本检索。
3. 元数据模型核心实体关系图
我们在应用系统开发中,通常我们定义的数据结构包括数据表、表字段,索引通常都会直接定义在物理数据库中,创建物理的表和字段以及索引等。
但是在元数据驱动平台数据模型中,我们定义的用户表包括系统表都是逻辑表,其结构是虚拟的,用户表的定义存储在Objects表,对应的字段定义存储在Fields表中,实际用户数据存储在Data表中,特别注意的是,对象的引用关系定义也定义在Fields表中,以特殊数据类型方式来定义。(另:Relationships表后面章节进行描述)。
从每个租户视角来看,每个租户都在一个共享数据库内拥有一个基于租户标识OrgID来隔离的虚拟的租户数据库。
元数据实体包括Objects和Fileds实体以及实际数据Data实体都包含租户OrgID,这样就可以通过租户OrgID来天然隔离各租户的数据,当然不止这些实体,包括索引相关等透视表实体也使如此。

4. 标准对象与标准字段
前面整体架构层次里提到了公共元数据层和标准应用层,公共元数据层提供了标准对象和标准字段的定义。
其中标准对象为每个租户提供公共端到端的应用的标准应用功能。
Standard Objects |
---|
Account |
Contact |
Lead |
Opportunity |
Case |
… |
同时用户可以在标准的对象基础上扩展自定义的应用对象,满足自己的特定业务场景。__c后缀代表自定义,后续详解。
Custom Objects |
---|
Product__c |
Customer__c |
Order__c |
OrderItem__c |
… |
而标准字段则提供给每个对象包括自定义对象的共同的字段,包含部分业务字段和非业务字段。
Standard Fields |
---|
ID |
Name |
CreatedBy |
CreatedDate |
LastModifiedBy |
LastModifiedDate |
OwnerID |
IsDeleted |
… |
用户也可以在标准对象和自定义对象内自定义不同的字段,以满足业务需要。__c后缀代表自定义,后续详解。
Custom Fields |
---|
First_Name__c |
Last_Name__c |
Nick_Name__c |
Login_Name__c |
Custome__Status__c |
Product_Status__c |
Product_No__c |
Order_No__c |
ItemID__c |
Item_Quantity__c |
Order_Time__c |
5. 对象关系类型
应用对象关系类型主要分为Look up 和 Master-Detail两种关系类型,其中Look up为弱的父子关系类型,Master-Detail为强的父子关系类型,其特性对比如下。
特性 |
Look up |
Master-Detail |
---|---|---|
1.非空性 |
可选 |
不可以 |
2.删除行为 |
方式可选:Clear/Block/Cascade |
Cascade级联 |
3.记录关系 |
相互独立父子关系 |
不可分父子关系 |
6. 元数据驱动的多租户数据架构示例
同样采用普通多租户SAAS数据架构方案中相同的域模型和示例数据作为参照进行说明,只不过在这里域模型不再对应到数据库的物理模型,而是对应到元数据所定义的虚拟数据库的逻辑模型。请前后对比两种模型对用户业务模型承载的差异和联系,以便深入了解元数据驱动的多租户数据架构。

对于Tenant租户 A00001,需要支撑相同的业务逻辑,需要定义相同的域模型,和普通的方案不同的是,这里采用元数据驱动的多租户数据模型来定义订单域模型和对应示例数据,其中域模型定义在元数据表(Metadata Tables)中,数据存储在Data Tables表中。
1. 用户自定义对象Product的定义
Product对象的基本信息定义在Objects表,作为Objects表的一条记录,通过OrgID进行不同租户数据隔离。Object中的每一条记录都代表一个不同的对象。Objects表的定义非常清晰,这里不做过多的解释,请参考Objects表介绍。
Objects |
||||
---|---|---|---|---|
ObjID |
OrgID |
ObjName |
ObjLabel |
Description |
01I2v000002zTEU |
A00001 |
Product |
Product |
Product |
Product对象的字段结构定义在Fields表,同时通过ObjID同Order对象定义进行关联,通过OrgID进行多租户数据隔离。
FieldID格式为字段定义的标识ID,用于区分每个字段定义,对于标准字段,则采用标准字段ID,如Name,则直接采用Name作为字段标识ID,对于自定义字段,则元数据引擎自动生成15位的标准格式的FieldID。其他字段定义请参考前面的Fields元数据表详细介绍。
下面详细描述一下Product对象中每个字段定义:
1. 产品名称Name 字段 为标准字段,数据格式为TEXT,长度为80
2. 产品编号ProductNo 为自定义字段,数据格式为TEXT,长度为22,FieldNum为1对应Data表存储字段Value1,存储格式为变长字符串。
3. 产品价格ProductPrice为自定义字段,数据格式为Currentcy(此格式类似Number,不同是带币种),整数最大长度DigitLeft:16位,小数位最大精度Scale:2位,FieldNum为2对应Data表存储字段Value3,存储格式为变长字符串。。
4. 状态ProductStatus为自定义字段,数据格式为TEXT,长度为20,FieldNum为3对应Data表存储字段Value3,存储格式为变长字符串。
Fields |
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FieldID |
OrgID |
ObjID |
FieldName |
FieldLabel |
FieldNum |
DataType |
DigitLeft |
Scale |
TextLength |
RelatedTo |
ChildRelationshipName |
IsRequired |
IsUnique |
IsIndexed |
… |
Name |
A00001 |
01I2v000002zTEU |
Name |
Product Name |
(标准字段) |
TEXT |
80 |
Y |
|||||||
00N2v00000OHwTL |
A00001 |
01I2v000002zTEU |
ProductNo |
Product No |
1 |
TEXT |
22 |
Y |
Y |
Yes |
|||||
00N2v00000OHwTQ |
A00001 |
01I2v000002zTEU |
ProductPrice |
Product Price |
2 |
Currency |
16 |
2 |
Y |
||||||
00N2v00000OHwTV |
A00001 |
01I2v000002zTEU |
ProductStatus |
Product Status |
3 |
TEXT |
20 |
Y |
|||||||
00N2v00000PDTIn |
A00001 |
01I2v000002zTEU |
Category |
Category |
4 |
TEXT |
20 |
||||||||
00N2v00000PDTIx |
A00001 |
01I2v000002zTEU |
Brand |
Brand |
5 |
TEXT |
20 |
2. 用户自定义对象Customer的定义
Customer对象的基本信息定义在Objects表,作为Objects表的一条记录,通过OrgID进行不同租户数据隔离。Object中的每一条记录都代表一个不同的对象。Objects表的定义非常清晰,这里不做过多的解释,请参考Objects表介绍。
Objects |
||||
---|---|---|---|---|
ObjID |
OrgID |
ObjName |
ObjLabel |
Description |
01I2v000002zTEZ |
A00001 |
Customer |
Customer |
Customer |
Customer对象的字段结构定义在Fields表,同时通过ObjID同Order对象定义进行关联,通过OrgID进行多租户数据隔离。
下面详细描述一下Customer对象中每个字段定义:
1. 用户名称Name,必选标准字段,不过多解释
2. 用户编号CustomerNo为自定义字段,数据类型为TEXT,长度为22,FieldNum为1对应Data表存储字段Value1,存储格式为变长字符串。
3. FirstName和LastName为自定义字段,数据类型为TEXT,长度均为20,FieldNum为2,3对应Data表存储字段Value2和Value3,存储格式为变长字符串。
4. 用户昵称Nick Name为自定义字段,数据类型为TEXT,长度均为20,FieldNum为4对应Data表存储字段Value4,存储格式为变长字符串。
5. 用户登录名LoginName为自定义字段,数据类型为TEXT,长度均为20,FieldNum为5对应Data表存储字段Value5,存储格式为变长字符串。
6. 用户状态CustomerStatus为自定义字段,数据类型为TEXT或者PickList,长度为20,FieldNum为6对应Data表存储字段Value6。为简化起见,状态字段暂定义为TEXT,对应Data表存储字段Value4,存储格式为变长字符串。
Fields |
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FieldID |
OrgID |
ObjID |
FieldName |
FieldLabel |
FieldNum |
DataType |
DigitLeft |
Scale |
TextLength |
RelatedTo |
ChildRelationshipName |
IsRequired |
IsUnique |
IsIndexed |
… |
Name |
A00001 |
01I2v000002zTEZ |
Name |
Customer Name |
(标准字段此项无) |
TEXT |
80 |
Y |
|||||||
00N2v00000OHwmn |
A00001 |
01I2v000002zTEZ |
CustomerNo |
Customer No |
1 |
TEXT |
22 |
Y |
Y |
Yes |
|||||
00N2v00000OHwms |
A00001 |
01I2v000002zTEZ |
FirstName |
First Name |
2 |
TEXT |
20 |
Y |
|||||||
00N2v00000OHwmx |
A00001 |
01I2v000002zTEZ |
LastName |
Last Name |
3 |
TEXT |
20 |
Y |
|||||||
00N2v00000OHwoF |
A00001 |
01I2v000003zTEZ |
NickName |
Nick Name |
4 |
TEXT |
20 |
Y |
|||||||
00N2v00000OHwoK |
A00001 |
01I2v000004zTEZ |
LoginName |
Login Name |
5 |
TEXT |
20 |
Y |
|||||||
00N2v00000OHwoP |
A00001 |
01I2v000005zTEZ |
CustomerStatus |
Customer Status |
6 |
TEXT |
20 |
Y |
3. 用户订单Order逻辑表的定义
Order对象的基本信息定义在Objects表,作为Objects表的一条记录,通过OrgID进行多租户数据隔离。Objects表中的每一条记录都代表一个不同的对象。
Objects |
||||
---|---|---|---|---|
ObjID |
OrgID |
ObjName |
ObjLabel |
Description |
01I2v000002zTEj |
A00001 |
Order |
Order |
Order |
Order对象的字段结构定义在Fields表,同时通过ObjID同Order对象定义进行关联,通过OrgID进行多租户数据隔离。
下面详细描述一下Order对象中每个字段定义:
1. 订单编号OrderNo为自定义字段,DataType数据格式为TEXT,长度为22,FieldNum为1,对应Data表存储字段Value1,存储格式为变长字符串。
2. 关系字段Customer为自定义关系字段,DataType类型为弱类型Look up 关系,关联到父对象Customer,则RelatedTo列存储Customer的ObjID:01I2v000002zTEZ,对应的FieldNum为2,则Customer对象实例GUID存储在Data表的Value2列。ChildRelationshipName列存储对象父子关系中子关系名称:orders,用于对象关系中从父对象实例数据反查子对象实例数据。
3. 订单状态OrderStatus为自定义字段,DataType类型为TEXT,长度为20,FieldNum为3,则状态存储在Data表的Value3列。为简化起见,状态字段暂定义为TEXT。
4. 下单时间OrderTime为自定义字段,DataType类型为Date/Time,FieldNum为4,则下单时间存储在Data数据表的Value4列。
Fields |
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FieldID |
OrgID |
ObjID |
FieldName |
FieldLabel |
FieldNum |
DataType |
DigitLeft |
Scale |
TextLength |
RelatedTo |
ChildRelationshipName |
IsRequired |
IsUnique |
IsIndexed |
… |
00N2v00000OHwsb |
A00001 |
01I2v000002zTEj |
OrderNo |
Order No |
1 |
TEXT |
22 |
Y |
Y |
Yes |
|||||
00N2v00000OHwsl |
A00001 |
01I2v000002zTEj |
Customer |
Customer |
2 |
Lookup |
01I2v000002zTEZ |
orders |
Y |
Yes |
|||||
00N2v00000OHwt0 |
A00001 |
01I2v000002zTEj |
OrderStatus |
Order Status |
3 |
TEXT |
20 |
Y |
|||||||
00N2v00000OHwtt |
A00001 |
01I2v000002zTEj |
OrderTime |
Order Time |
4 |
Date/Time |
Y |
4. 用户订单行OrderItem逻辑表定义 同样的,OrderItem对象的基本信息也以一条记录的信息定义在Objects表,通过OrgID进行多租户数据隔离。Objects表中的每一条记录都代表一个不同的对象。
Objects |
||||
---|---|---|---|---|
ObjID |
OrgID |
ObjName |
ObjLabel |
Description |
01I2v000002zTEo |
A00001 |
OrderItem |
Order Item |
Order Item |
OrderItem的字段结构也定义在Fields表,通过ObjID同OrderItem对象关联,通过OrgID进行多租户数据隔离。
下面详细描述一下Order对象中每个字段定义:
1. 关系字段Order为自定义关系字段,DataType类型为强类型的Master-Detail关系,关联到父对象Order,则RelatedTo列存储Order对象的ObjID:01I2v000002zTEj,对应的FieldNum为1,则Order对象实例GUID存储在Data表的Value1列。ChildRelationshipName列存储对象父子关系中子关系名称:OrderItem(s),用于对象关系中从父对象Order实例数据反查子对象实例数据。
2. 关系字段Product为自定义关系字段,DataType类型为弱类型的Look up关系,关联到父对象Product,则RelatedTo列存储Product对象的ObjID:01I2v000002zTEU,对应的FieldNum为2,则Product对象实例GUID存储在Data表的Value2列。ChildRelationshipName列存储对象父子关系中子关系名称:OrderItem(s),用于对象关系中从父对象Product实例数据反查子对象实例数据。
3. 商品实际售价ItemPrice为自定义字段,DateType类型为Currentcy(此格式类似Number,不同是带币种),整数最大长度DigitLeft:16位,小数位最大精度Scale:2位,FieldNum为2对应Data表存储列Value3,存储格式为变长字符串。
4. 商品购买数量Item Quantity为自定义字段,DataType类型为Number,整形长度为18位,无小数位数,FieldNum为4,对应Data数据表存储列Value4。
5. 订单明细状态OrderItemStatus为自定义字段,Datetype类型为TEXT,长度为20,对应FieldNum为5,对应Data数据表存储列Value5。为简化起见,状态字段暂定义为TEXT。
Fields |
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
FieldID |
OrgID |
ObjID |
FieldName |
FieldLabel |
FieldNum |
DataType |
DigitLeft |
Scale |
TextLength |
RelatedTo |
ChildRelationshipName |
IsRequired |
IsUnique |
IsIndexed |
… |
00N2v00000OHwtF |
A00001 |
01I2v000002zTEo |
Order |
Order |
1 |
Master-Detail |
01I2v000002zTEj |
OrderItem |
Y |
||||||
00N2v00000OHwtZ |
A00001 |
01I2v000002zTEo |
Product |
Product |
2 |
Lookup |
01I2v000002zTEU |
OrderItem |
Y |
||||||
00N2v00000OHwte |
A00001 |
01I2v000002zTEo |
ItemPrice |
ItemPrice |
3 |
Currency |
16 |
2 |
Y |
||||||
00N2v00000OHwtj |
A00001 |
01I2v000002zTEo |
ItemQuantity |
Item Quantity |
4 |
Number |
18 |
0 |
Y |
||||||
00N2v00000OHwto |
A00001 |
01I2v000002zTEo |
OrderItemStatus |
Order Item Status |
5 |
TEXT |
20 |
Y |
5. 对象Schema
定义好的用户应用对象Schema如下图

6. 数据表Data表用户数据存储
前面提到了用户自定义的应用对象以虚拟结构的方式存储在Objects和Fields表中,那么用户定义的应用对象Product、Customer、Order和OrderItem里的数据存储在哪里呢?答案是Data表,用户定义的对象的数据均会存储在Data表中,每个用户定义对象实例(或者近似称为用户表记录)数据以Data表中一条记录的形式存在。Product、Customer、Order表的数据记录均存储在Data表,OrderItem也亦是如此。
其中,GUID作为每条数据记录暨是每个对象实例的全局唯一标识,OrgID进行多租户数据隔离,ObjID同Objects表关联代表具体哪个对象定义。 这里重点提一下,Fields中定义的对象字段在Data表中的存储,其中Fields表中FieldNum非常关键,它对应了对象实例字段在Data表中的具体存储位置,FieldNum对应数字决定着数据存储在Data表中的哪个ValueX列。前面每个对象结构定义都对FieldNum对应Data的进行了说明,对象字段FieldNum可以不按照顺序来,只要FieldNum没有占用,可以任意对应,当然按照顺序是比较好的实践。
再举例来说:
1. Order对象的Customer关系字段定义在Fields表中,其FieldNum为1,则其在Data表中存储的位置,就是是Order对象实例在Data对应的记录中Value1这个字段所存储的值,存储的值为Customer对象实例GUID,也就是:a062v00001YXEKuAAP、a062v00001YXEKzAAP等。
2. OrderItem对象的Product、ItemQuantity字段定义在Fields表中,其对应的FieldNum分别为2、4,则其在Data表中存储的位置,就是OrderItem对象在Data对应的记录中Value2、以及Value4所存储的数据,也就是:a052v00000jbgEQAAY、2以及a052v00000jbgMqAAI、3等记录。
Data |
|||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
GUID |
OrgID |
ObjID |
Name |
Value1 |
Value2 |
Value3 |
Value4 |
Value5 |
Value6 |
… |
Value500 |
CreatedBy |
CreatedDate |
… |
… |
a052v00000jbgEQAAY |
A00001 |
01I2v000002zTEU |
IPhone8 256G Golden |
PI201901060930A0000001 |
6000 |
Online |
Mobile Phone |
Apple |
|||||||
a052v00000jbgMqAAI |
A00001 |
01I2v000002zTEU |
IPhoneX 256G Golden |
PI201901060930A0000002 |
10000 |
Online |
Mobile Phone |
Apple |
|||||||
a052v00000jbgMvAAI |
A00001 |
01I2v000002zTEU |
IPhoneXR 256G Golden |
PI201901060930A0000003 |
8000 |
Online |
Mobile Phone |
Apple |
|||||||
a052v00000jc4ViAAI |
A00001 |
01I2v000002zTEU |
HUAWEI P30 256G |
PI201901060930A0000004 |
5000 |
Online |
Mobile Phone |
HUAWEI |
|||||||
a062v00001YXEKuAAP |
A00001 |
01I2v000002zTEZ |
Cheng Yan |
CI200903091014A0000001 |
Yan |
Cheng |
cy |
chengyan |
Valid |
||||||
a062v00001YXEKzAAP |
A00001 |
01I2v000003zTEZ |
Ling Jun |
CI200903091014A0000002 |
Jun |
Ling |
lj |
lingjun |
Valid |
||||||
a062v00001YXEL0AAP |
A00001 |
01I2v000004zTEZ |
Tommy Valdels |
CI200903091014A0000003 |
Tommy |
Valdels |
tom |
Tommy |
Valid |
||||||
a062v00001YXEL1AAP |
A00001 |
01I2v000005zTEZ |
Dorothy Franklin |
CI200903091014A0000004 |
Dorothy |
Franklin |
doro |
Dorothy |
Valid |
||||||
a072v000016DxY0AAK |
A00001 |
01I2v000002zTEj |
Order |
ON201903091914A0000001 |
a062v00001YXEKuAAP |
Completed |
2019-08-01T04:00:00.000+0000 |
||||||||
a072v000016DxYPAA0 |
A00001 |
01I2v000002zTEj |
Order |
ON201903091914A0100003 |
a062v00001YXEKzAAP |
Completed |
2019-03-09T11:00:00.000+0000 |
||||||||
a072v000016DxYFAA0 |
A00001 |
01I2v000002zTEj |
Order |
ON201903091914A0200005 |
a062v00001YXEL1AAP |
Completed |
2019-03-08T23:30:00.000+0000 |
||||||||
a082v00002mD8EoAAK |
A00001 |
01I2v000002zTEo |
Order Item |
a072v000016DxY0AAK |
a052v00000jbgEQAAY |
5888 |
2 |
InProduction |
|||||||
a082v00002mD8EpAAK |
A00001 |
01I2v000002zTEo |
Order Item |
a072v000016DxY0AAK |
a052v00000jbgMqAAI |
9888 |
3 |
Canceled |
|||||||
a082v00002mD8EtAAK |
A00001 |
01I2v000002zTEo |
Order Item |
a072v000016DxYPAA0 |
a052v00000jbgMvAAI |
7888 |
1 |
InTransfer |
|||||||
a082v00002mD8EuAAK |
A00001 |
01I2v000002zTEo |
Order Item |
a072v000016DxYFAA0 |
a052v00000jbgMvAAI |
7888 |
1 |
InTransfer |
|||||||
… |
7. 通用的存储,按需转换—Data表数据类型与存储
我们看了元数据驱动的多租户模型的核心关系,明白了用户自定义表(包括应用系统表)以及表结构是在Objects和Fields进行虚拟定义的,也清楚的知道了系统以及用户表的数据是作为一条条记录存储在Data表中的,那么我们下面来看下不同的数据类型如何在Data中进行存储的呢?
在Fields表中,可以采用任何一种标准的结构化的数据类型,如text,number,date,以及date/time对用户表字段进行定义,也可以采用特殊结构的数据类型对字段类型进行定义,如下拉框picklist,系统自增字段autonumber,公式列(只读的公式推导列),布尔多选框,email,URL以及其他的类型,当然也可以通过系统应用来对Fields中的自定义字段进行强制约束包括是否必须非空以及掐校验规则(如符合特定格式,符合特定值范围等)。
上述的各种不同字段格式数据都是存储在Data表中的ValueX列中的,Data表中包含500个数据列,称为弹性列,用来存储用户数据和系统数据,也就是对应到Objects表和Fields表对应的虚拟表结构所要承载的数据。
特别的,所有弹性列都用了一个可变长度的字符串类型,以便于他们可以存储任何结构化类型的应用和用户数据(字符串,数字,日期等)。
正是因为弹性列把所有不同的数据类型拉平来存储,所以任一弹性列可以对存储任何对象的任何类型的属性来存储,用户可以指定不同的对象的不同属性对应的不同的存储弹性列,当然同属于相同对象的实例的属性对应的弹性列是一致的。一个弹性列可以存储来不同的格式的数据,前提条件是这些数据属于不同的对象的不同属性。例如:上一节示例中,Data表的Value2列可以存储Order表的日期格式的OrderTime数据,也可以存储OrderItem表的格式为字符串的OrderID数据。

如上所述,弹性列用通用数据类型暨可变长字符串来存储所有类型的数据,这样就可以在不同的用户表字段间共享相同弹性列,即便它们的数据类型各异。
既然所有的数据全部用通用的可变长字符串来存储,那么应用逻辑处理需要不同的数据格式时候怎么办呢?具体做法如下:
当应用系统需要从弹性列读取和写入数据时候,UDD(Universal Data Dictionary)层暨元数据运行引擎会用底层数据库系统数据转换函数(如Oracle数据库的TO_NUMBER,TO_DATE,TO_CHAR函数)按需对数据格式进行转换,将字符串格式转换成对应的数据格式(数字,日期等)。
如果存储非结构化的大文本块数据怎么办呢?模型支持对Clob大字段的定义,对于在Data表中具有CLob数据的每一行数据,系统将其存储在Clobs透视表中,并按照需要同Data表的对应数据对象实例记录进行关联。
8. 多租户索引透视表(Pivot Tables)
1. Indexes透视表
大多数结构化的数据存储在Data表内,如前面提到的,所有这些不同类型数据都是以可变字符串的形式存在ValueX列里面如各种数字以及日期等全部都是以可变字符存储的,这样虽然对于对象实例各种字段的存储确实非常灵活,不同的列可以存储不同类型的数据,即使同一ValueX列不同的对象也可以存储类型的数据,但是这样带来一个巨大的问题,由于不同的数据类型以可变字符串的方式存储在同一列内,你没办法利用底层数据库索引的能力对其进行排序,ValueX列的数据都是一种按照离散的顺序来存储的。传统的数据库依赖原生的数据库索引来快速在数据表内定位到符合查询条件的记录。而按照Data表ValueX列的数据存储情况,在Data表建立ValueX列的索引来支撑数据快速查询是不现实的。
所以解决办法就是建立另外的透视表叫做Indexes索引表,并把数据拷贝出数据表并转换成原始的的数据类型,并存储到Indexes索引表列内,如原来是整形的数据以可变字符串的格式存储在ValueX列中,拷贝到Indexes表之前通过函数将其转换为原始的数据类型,在存储到Indexes对应的NumValue列内,以方便建立索引,Indexes表包含强类型的索引类,像StringValue,NumValue,DataValue,用来定位对应数据类型的字段数据。

Indexes透视表的字段说明如下:
1. OrgID:其所归属的应用对象所归属的租户OrgID
2. ObjID:字段所属应用对象唯一标识
3. FieldNum:对象字段存储位置
4. ObjInstanceGUID:对象实例唯一标识
5. StringValue:强类型的字符串列
6. NumValue:强类型的数字列
7. DateValue:强类型的日期列
下面的Indexes表示例包含对字符、数字和日期性数据的索引需求支持,数据来源于前面的Data表数据。
Indexes |
||||||
---|---|---|---|---|---|---|
OrgID |
ObjID |
FieldNum |
GUID |
StringValue |
NumValue |
DateValue |
A00001 |
01I2v000002zTEU |
1 |
a052v00000jbgEQAAY |
PI201901060930A0000001 |
||
A00001 |
01I2v000002zTEU |
1 |
a052v00000jbgMqAAI |
PI201901060930A0000002 |
||
A00001 |
01I2v000002zTEU |
1 |
a052v00000jbgMvAAI |
PI201901060930A0000003 |
||
A00001 |
01I2v000002zTEU |
1 |
a052v00000jc4ViAAI |
PI201901060930A0000004 |
||
A00001 |
01I2v000002zTEU |
4 |
a052v00000jbgEQAAY |
Mobile Phone |
||
A00001 |
01I2v000002zTEU |
4 |
a052v00000jbgMqAAI |
Mobile Phone |
||
A00001 |
01I2v000002zTEU |
4 |
a052v00000jbgMvAAI |
Mobile Phone |
||
A00001 |
01I2v000002zTEU |
4 |
a052v00000jc4ViAAI |
Mobile Phone |
||
A00001 |
01I2v000002zTEU |
5 |
a052v00000jbgEQAAY |
Apple |
||
A00001 |
01I2v000002zTEU |
5 |
a052v00000jbgMqAAI |
Apple |
||
A00001 |
01I2v000002zTEU |
5 |
a052v00000jbgMvAAI |
Apple |
||
A00001 |
01I2v000002zTEU |
5 |
a052v00000jc4ViAAI |
HUAWEI |
||
A00001 |
01I2v000002zTEZ |
1 |
a062v00001YXEKuAAP |
CI200903091014A0000001 |
||
A00001 |
01I2v000002zTEZ |
1 |
a062v00001YXEKzAAP |
CI200903091014A0000002 |
||
A00001 |
01I2v000002zTEZ |
1 |
a062v00001YXEL0AAP |
CI200903091014A0000003 |
||
A00001 |
01I2v000002zTEZ |
1 |
a062v00001YXEL1AAP |
CI200903091014A0000004 |
||
A00001 |
01I2v000002zTEj |
1 |
a072v000016DxY0AAK |
ON201903091914A0000001 |
||
A00001 |
01I2v000002zTEj |
1 |
a072v000016DxYPAA0 |
ON201903091914A0100003 |
||
A00001 |
01I2v000002zTEj |
1 |
a072v000016DxYFAA0 |
ON201903091914A0200005 |
||
A00001 |
01I2v000002zTEj |
4 |
a072v000016DxY0AAK |
2019-08-01T04:00:00.000+0000 |
||
A00001 |
01I2v000002zTEj |
4 |
a072v000016DxYPAA0 |
2019-03-09T11:00:00.000+0000 |
||
A00001 |
01I2v000002zTEj |
4 |
a072v000016DxYFAA0 |
2019-03-08T23:30:00.000+0000 |
||
A00001 |
01I2v000002zTEU |
2 |
a052v00000jbgEQAAY |
6000 |
||
A00001 |
01I2v000002zTEU |
2 |
a052v00000jbgMqAAI |
10000 |
||
A00001 |
01I2v000002zTEU |
2 |
a052v00000jbgMvAAI |
8000 |
||
A00001 |
… |
|||||
A00001 |
… |
Indexes表的底层索引是标准的,采用非唯一性的数据库索引。当做对象检索查询的时候,实际上不是在Data数据表上做查询,而是在Indexes索引表上做的查询,获取到OrgID,ObjectID以及GUID,然后再返回数据表获取数据。也就是当系统查询条件包含对象实例的结构化的字段时,系统查询优化器采用MT_Indexes来帮助优化相关的数据访问操作。
2. Unique Indexes透视表
由于Data数据表的多数据类型的无差别存储,无法在Data数据表建唯一性的索引供用户来使用对对象字段值进行唯一性校验。为了支持用户对象自定义字段的唯一性校验,解决办法是采用了Unique_Indexes 透视表;这个表非常类似于Indexes表,不过Unique_indexes采用底层原生的数据库索引来强制唯一性校验。当一个用户尝试来插入一个重复的值到具有唯一性约束的对象字段时,或者当用户尝试去在一个现存的包含唯一性的字段进行强制唯一性时,系统会给出唯一性校验失败的提示,阻止用户的下一步操作。

Unique Indexes透视表的核心字段说明如下:
1. UniqueStringValue:唯一的字符串列
2. UniqueNumValue:唯一的数字列
3. UniqueDateValue:唯一的日期列
4. 其他字段定义请参考Indexes透视表
Relationships索引透视表
在元数据驱动的多租户模型中,提到了在Objects表以及Fields表中保存了用户对象结构和对象关系的定义,对象关系的定义是通过元数据模型Fields表字段数据类型提供了一个特殊的数据类型:“关系”(Relationship), 来给用户用于声明不同的用户应用对象之间的关系,也就是我们通常说引用完整性。
对象之间的引用关系定义以及对象实例间的引用关系存储在元数据表Objects、Fields中和Data表中,关联查询关系复杂,为了提升对象之间查询的效率,特别是通过对象相互引用关系对对象实例数据进行检索,系统提供关系索引透视表Relationship来优化对象引用关联查询。

Relationships索引透视表的字段说明如下:
1. OrgID:其所归属的应用对象所归属的租户OrgID
2. ObjID:子对象的对象标识
3. GUID:子对象实例的唯一标识
4. RelationID:子对象内关系字段定义的标识
5. TargetObjInstanceID:父对象实例的唯一标识
关系透视表Relationship定义了两个底层数据库复合索引:
1. 第一个索引字段:OrgID + GUID,用于从子对象到父对象的关联查询
2. 第二个索引字段:OrgID + ObjID + RelationID + TargetObjInstanceID,用于父对象到子对象的关联查询
Relationships索引透视表会在后面SOQL章节进行进一步描述验证。
4. 其他索引透视表
其他索引透视表的逻辑类似,都是为了满足特定检索和查询需要,将数据同步到索引表,供应用系统使用。此处不再赘述,如确实有需要再补充。
五、SOQL与关系Relationships
SOQL是Salesforce Object Query Language的简称,具有SQL类似的语法结构,就像前面提到的一样,Salesforce是以应用对象(Salesforce Object,简称SObject)的视角管理业务数据和功能,SOQL类似对用于对应有对象数据进行查询的API。
1. 从SQL到SOQL
SOQL也是采用类似表查询的结构,同SQL非常相似,也通过底层数据库索引来提供查询优化支撑。不同点如下:
1. 没有 select *
2. 没有视图概念
3. SOQL是只读的
4. 由于底层元数据驱动的多租户数据模型的限制,索引是受限制的,没有原生数据库物理结构丰富的索引支持。
5. 对象到关系的映射(Object-Relational Mapping)是自动完成的.
6. SObjects 在多租户环境中并不是对应实际的物理数据表
7. SObjects包括SObjects之间的关系都是以元数据的方式存储在多租户环境中的。
2. SOQL示例&语法
下面我用示例来说明一下SOQL的用法,同时引出SOQL的特殊语法说明,SOQL大小写不敏感。
1. 单个对象的查询及语法说明
select id,productno__c,name,productprice__c,productstatus__c from product__c

前面提到过系统提供了标准应用对象和标准字段定义,更大的优势在于支持用户自行自定义对象和字段。这里__c
代表的使用户自定义的含义, product__c
代表的用户自定义对象Product,而非系统标准对象和字段,系统标准对象和字段在SOQL无需__c
后缀,如ID,Name,CreatedBy等字段则为系统提供给每个对象的标准字段,而字段ProductNo为用户自定义字段,则SOQL中的语法表示为productno__c
。这样的好处是讲标准和用户自定义对象和字段很容易区分开,系统可以定义标准Product对象,以product表示,用户也可以同样定义一个Product对象,不过SOQL用product__c
表示用于区分。
2. 子对象关联父对象(Child to Parent)查询及语法说明
select id,name,orderno__c, customer__c, customer__r.customerno__c,customer__r.name, orderstatus__c,ordertime__c from order__c order by orderno__c

select id,name,orderno__c, customer__c, customer__r.customerno__c,customer__r.name, orderstatus__c,ordertime__c from order__c where customer__r.name='Cheng Yan' order by orderno__c

这里是从子对象Order关联到父对象Customer进行查询,其中:
1. from后面的对象order__c
表示Order为用于自定义对象
2. Id,name为Order对象内系统定义的标准字段,
3. Orderno__c,customer__c,orderstatus__c,ordertime__c
为用户自定义字段,这里需要说明的是customer__c
自定义字段存储的是父对象实例ID
4. customer__r
就特别有意思,其中 __r
部分代表父对象关系引用,customer
部分对应关系字段名,customer__r
代表从Order对象到Customer对象的一个应用关系,并通过customer__r.customerno__c,customer__r.name
获取到Customer对象的字段值。
3. 父对象关联子对象(Parent to Child)查询及语法说明
select id,orderno__c,customer__r.name,ordertime__c,orderstatus__c, ( select id, product__r.productno__c,product__r.name,product__r.productprice__c from orderitem__r ) from order__c order by orderno__c

这个语句稍微有些复杂,从Order对象关联到OrderItem对象,又从OrderItem关联到Product,同时还包含了Order对象到Customer对象的关联。
这里着重说一下从父对象到子对象的关联,父到子的关联是在父对象的主查询语句中在查询字段中用()来封装到子对象的关联,其中
1. 子句中from orderitem__r
的 orderitem__r
代表的是对子对象OrderItem的引用,orderitem
对应的为前文关系字段中提到的ChildRelationshipName,并且同一个父对象的子方的关系名称唯一(父对象Name+ChildRelationshipName必须唯一),用作父对象到子对象的查询关联。
2. 子句中id,product__r.productno__c,product__r.name,product__r.productprice__c
的上下文为orderitem__r
代表的子对象
3. Relationships索引透视表
Relationships是为了SOQL的快速对象关联查询所定义的,子对象关联父对象(Child to Parent)查询,复合索引(OrgID+GUID)在Join中起到较大作用,而需要从父对象关联子对象(Parent to Child)查询,则复合索引(OrgID + ObjID + RelationID + TargetObjInstanceID)在Join中起到较大作用。
六、如何支撑多租户巨大数据量
前面我们提到Salesforce一个共享数据库的概念,那一个共享数据库怎么来支撑如此巨大的多租户数据库呢,同时不仅需要支持巨量数据,并且还可以支撑租户间的数据物理隔离,保证各租户的数据稳定性、可用性和数据安全?
Salesforce的做法是:分区。所有的Force.com的数据,元数据,透视表结构,包含底层数据库索引,都是通过对OrgID进行物理分区的,采用的是原生的数据库分区机制。所有的数据以及元数据通过你的OrgID(16digits)进行分片Hash。
数据分区是数据库系统提供的被验证过的技术,用以物理划分较大的逻辑数据结构到较小的可以管理的区块中。分区也可以帮助提升性能和扩展性,贴别是在多租户环境下一个巨大的数据系统的扩展性。根据定义,每一个SOQL的查询对应一个特别的租户信息,因此查询优化器,仅仅需要考虑访问包含对应租户的数据分区访问,而不是整个表或者索引。
七、无感的对象结构变更(No DDL)
当一个应用系统或者服务组件需要对其数据模型进行升级的时候,通常会通过数据库DDL语言对数据库物理结构进行操作,如果涉及的数据量较大,则可能会造成较长时间的数据库变更时效,造成对应时间内的系统不可用,如果是多租户系统还会可能其他租户的可用性造成影响,抑或造成诸多的底层模型不一致产生。
在元数据驱动的数据架构中,所有的DDL语言操作对应的使元数据层的元数据的记录的更新,不涉及
数据库物理结构的更新,不会造成变更期间的数据库物理结构耗时调整造成的不可用,同时系统平台提供了一个高效的机制来减少对平台多租户应用总体性能影响。
当用户修改了一个表字段列的数据结构,从一种数据类型改成另外一种不同存储格式的数据类型时候,系统会重新分派一个新的弹性列给到这个字段列的数据,将数据从原来的存储弹性列批量拷贝到新的弹性列,然后才会更新此字段列的元数据,暨在Fields表中更新这个字段列的元数据,将数据类型更改为新的数据类型,并将FieldNum更新为新的ValueX列对应的X值。
同时,在如上对用户逻辑表结构调整生效过程中,原来的数据结构和对应的数据访问正常进行,直到逻辑表结构变更生效,对应用系统可用性不会造成影响,用户对此无感知。
八、多租户架构对于研发人员意味着什么
对于研发人员来说,多租户结构最多意味着两个版本:当前版本,以及下一个版本。没有遗留版本需要维护。
所有人不用操心旧的技术,旧的版本,所有只有最新的版本,只需要关心最新的版本。
这样就给敏捷开发带来极大的好处,每年做个位的发布,每次发布几百个新的特性新的版本也不会改变用户的体验,新的特性可以根据用户需要开启,通过特性管理来开关。
新版本发布前,提供沙箱环境来允许用户提前试用新版本的系统。
如果做bug修复,则是在所有租户层面上进行统一修复的。
对于用户应用的发布进行严格管理,防止对其他租户产生影响,通过提供沙箱环境来让用户验证新应用发布,并通过成千上万的自动化测试保证用户的正常功能。
在运行期间,不作任何底层DDL操作,不会做表的创建,也不会做表的变更,只可能在极少数的更新周期时候进行。