diff --git a/src/chapter2/1-1.md b/src/chapter2/1-1.md index 84f33f9..9d70c1a 100644 --- a/src/chapter2/1-1.md +++ b/src/chapter2/1-1.md @@ -7,13 +7,39 @@ npm run make:domain 编译出来的数据字典声明在*src/oak-app-domain*目录下,同时也会编译出来一个数据的存储格式供框架引用,可以在代码中像这样去引用它们: ```typescript +// EntityDict是数据字典声明,StorageSchema是存储格式定义 import { EntityDict, StorageSchema } from '@project/oak-app-domain'; ``` 数据字典和存储格式是整个Oak框架最核心的内容,贯穿于使用框架的各个层面,因此需要深刻理解。本章节将使用上小节的*Address*和*Area*对象,介绍一些查询和操作的核心概念。 -## 查询相关概念 +### 编译后的对象结构 +编译后的对象原生结构称为*OpSchema*,其结构仅仅在用户定义的属性上增加了一些通用的属性类型,以及将引用对象转化成了外键。 + +> 每个Entity的*OpSchema*可以在编译后的*oak-app-domain/${Entity}/Schema.ts*中查看,本章下面的大多数数据结构都是如此 + +对象上增加的通用属性包括: + +属性 | 类型 | 含义 +------------|-----------------------|--------- +id | string<36> | 主键,uuid +$$createAt$$ | number | 创建时间戳(Date.now()) +$$updateAt$$ | number | 更新时间戳 +$$deleteAt$$ | number | 删除时间戳 +$$seq$$ | int | 递增序列 + +## 查询(Select) +当查询一个对象时,传入的Select结构如下: +```typescript +{ + data: Projection; + filter: Filter; + sorter: Sorter; + indexFrom: number; + count: number; +} +``` ### Projection -当查询对象时,通过projection可以定义要查询对象的哪些属性(projection的命名本身就借鉴了数据库中的“投影”概念)。例如,对上一节中所定义的*Address*对象,查询时可以指定投影为: +当查询对象时,通过*Projection*可以定义要查询对象的哪些属性(projection的命名本身就借鉴了数据库中的“投影”概念)。例如,对上一节中所定义的*Address*对象,查询时可以指定投影为: ```typescript { detail: 1, @@ -74,7 +100,8 @@ Oak框架还支持表达式查询和函数计算,例如如果想返回一个na ``` ### Schema -Schema是查询某对象后可能获得的数据格式。例如,在上面的例子里,查询到的*Address*数据结果就是: +Schema是对对象进行Select查询获得的数据结果格式。此时返回的对象除了自身的属性之外,还可能级联了其父对象与子对象的数据。 +例如,在上面的例子里,查询到的*Address*数据结果中包含了其父对象*Address*的数据: ```typescript { id: 'xxx', @@ -92,7 +119,8 @@ Schema是查询某对象后可能获得的数据格式。例如,在上面的 }, } ``` -而查询到的*Area*数据结果就是: + +而查询到的*Area*数据结果就会包含其子对象*Address*的数据: ```typescript { id: '310100', @@ -214,3 +242,114 @@ Sorter表示查询时的排序。例如,我们查询*Address*时要求结果 在查询中,如果不指定任何排序条件,则框架会自动添加一个$$createAt$$属性上的降序排序条件。 +## 操作(Operate) +当要操作一个对象时,传入的数据结构如下: +```typescript +{ + id: Uuid; + action: Action; + data: Data; + filter: Filter; +} +``` + +### Uuid +Operate的操作需要唯一编号,其作用是为了记录日志和在分布式环境下实现操作同步。可以使用下面两个函数来产生Uuid。 +```typescript +import { generateUuid, generateUuidAsync } from 'oak-domain/lib/utils/uuid'; +``` +一般来说,如果不是在有些前端环境中受到同步的限制,应优先使用异步产生函数。 + +### Action +声明Operate的类型。有三种Action是公共的:*create/update/remove*,除此之外,用户在定义Entity时,所声明的Action也是有效的Action。 + +所有用户定义的Action从广义上来说都是update。如果用户在定义Action时,也定义了相应的状态转换矩阵(见[编写对象](1.md)),则在执行该动作时,会自动进行对象相应属性的状态检查以及更新其状态。 + +一般来说,对一个对象的操作如果有业务层面上的语义,推荐尽量细化成不同的*Action*,而不直接使用*update*。这样一来可以使对对象的操作历史更加清晰,二来也便于后续进行细粒度的权限控制。 + +### Data +声明更新的数据。更新的数据可以是两种: +1. 自身的数据属性 + 例如要更新地址的phone和name: +```typescript + { + phone: '138xxxxxxxx', + name: '张小三', + } +``` +create操作必须传入id以及有效的所有声明非空属性,update只需要传入至少一个属性,而remove操作无需传入自身的属性。 + +2. 级联数据属性 +同Filter/Projection一样,更新的Data也支持级联更新,可以通过一次Operate请求,更新当前对象以及其级联对象上的属性。 +例如,我们可以在更新*Address*时,同时去更新其相关联的父对象*Area*的数据(不考虑这个请求是否有意义): +```typescript + { + phone: '138xxxxxxxx', + name: '张小三', + area: { + id: '{uuid}', + action: 'update', + data: { + name: '苏杭市', + } + }, + } + +``` +同样的,我们也可以在更新父对象*Area*时,更新其相关联的子对象*Address*的数据: +```typescript + { + name: '苏杭市', + address$area: { + id: '{uuid}', + action: 'update', + data: { + phone: '138xxxxxxxx', + }, + filter: { + name: '张小三', + } + } + } +``` +这条Operate在更新*Area*数据的同时,还会将“指向它的且『name为张小三』的所有的*Address*”数据的phone属性更新成138xxxxxxxx。 + +我们还可以在更新父对象的同时,插入一条子对象,像下面这样: +```typescript + { + name: '苏杭市', + address$area: { + id: '{uuid}', + action: 'create', + data: { + id: '{uuid}', + phone: '138xxxxxxxx', + name: '张小四', + .... + }, + } + } +``` +新插入的*Address*会自动和当前*Area*关联。 + +下面列出了框架所支持的级联更新的情况: + +* 子对象级联父对象 + +子对象 | 父对象 | 效果 +------------|-----------------------|--------- +create | create | 父子对象和关联关系一起创建 +update | create | 更新子对象、创建父对象及关联关系(如果原来子对象上有关联的父对象关系会丢失) +update | update | 更新子对象,同时更新关联的父对象 +update | remove | 更新子对象,同时删除关联的父对象及关联关系 +remove | update | 移除子对象及关联关系,同时更新关联的父对象 +remove | remove | 同时移除父子对象,以及关联关系 + +* 父对象级联子对象 + +父对象 | 子对象 | 效果 +------------|-----------------------|--------- +create | create | 创建父子对象,并创建关联关系 +update | update | 更新父对象,并更新关联的子对象 +update | remove | 更新父对象,同时删除关联的子对象 + diff --git a/src/chapter2/1.md b/src/chapter2/1.md index ff9a7e9..8d12eca 100644 --- a/src/chapter2/1.md +++ b/src/chapter2/1.md @@ -29,7 +29,6 @@ export interface Schema extends EntityShape { 3. 可以引用其它Entity,并将之声明为当前Entity的某一属性(此时称之为定义了对象之间的**多对一关系**)。例如在*Address*对象中就声明了*area*属性是代表(地址所属的)地区。 > 对象的属性也可以指向其自身。例如,在Area对象的定义中,就有一个 parent 属性指向它自己(Schema),代表该地区的上级地区。 -4. 可以为对象设计动态多对一关系,若一个对象可能和不同的对象有多对一关系,则通过声明 ``` entity: String<32>; entityId: String<64> ```即可以表达动态的多对一关系,即当前对象可能和不同的其它对象具有多对一关系。 ## 动态多对一关系 在一些应用中,某对象*A*可能和不同的对象*B/C/D*……具有多对一关系(但这种关系是互斥的,不可能同时和多于一个对象具有这种关系)。此时,如果像这样设计*A*对象: @@ -73,10 +72,12 @@ export interface Schema extends User { addresses?: Address[]; }; ``` -用户这个对象,(以一对多的方式)关联了三个拥有动态多对一设计的对象:*ExtraFile*, *WechatQrCod*e, *Address*。这里我们不去深究其具体含义,只要知道,这三个对象中都设计有 ```entity/entityId``` 属性,同时我们在User对象的定义中声明了,User和它们具有一对多关系。这里用*Array*和[]的写法都是可以的。 +用户这个对象,(以一对多的方式)关联了三个拥有动态多对一设计的对象:*ExtraFile*, *WechatQrCod*e, *Address*。这里我们不去深究其具体含义,只要知道,这三个对象中都设计有 ```entity/entityId``` 属性,同时我们在*User*对象的定义中声明了,User和它们具有一对多关系。这里用*Array*和[]的写法都是可以的。 > 如果一个对象想拥有超过一个的动态多对一关系怎么办?很遗憾,Oak并不支持这种设计。而且根据我们的经验,在实际应用开发中,出现如此复杂的对象往往意味着你应该重新审视你的对象设计是否合理了。 +当一个对象*A*定义了与另一个对象*B*具有多对一关系时(无论是否是动态的),我们称*B*对象为*A*对象的**父对象**,同时称*A*对象为*B*对象的**子对象**。 + ## 更多属性相关 Oak当前支持的基本属性类型可以参见:```oak-domain/lib/types/DataType```。 > 其中,*Float*和*Double*类型已经不再推荐,请尽量使用*Decimal*来定义非整型数值 @@ -141,7 +142,7 @@ export const UserActionDef: ActionDef = { export type Action = UserAction | IdAction; ``` -在这里,我们声明了两组状态和动作,以及状态转换矩阵,同组中的状态/动作/转换矩阵的前缀必须要一致。状态转换矩阵规定了某个*Action*将把*User*的当前(对应的状态值)修改成什么,这种转换在规定之后应当是一目了然的。其中,```is```定义的是初始状态(若对象创建时没有赋初始状态则默认使用这种状态值)。 +在这里,我们声明了两组状态和动作,以及**状态转换矩阵**,同组中的状态/动作/转换矩阵的前缀必须要一致。状态转换矩阵规定了某个*Action*将把*User*的当前(对应的状态值)修改成什么,这种转换在规定之后应当是一目了然的。其中,```is```定义的是初始状态(若对象创建时没有赋初始状态则默认使用这种状态值)。 最后,我们通过``` export type Action ```来声明了当前对象上的所有*Action*。对当前对象的操作将被限制在这些自定义的*Action*和通用的*Action*当中。通用的*Action*包括: * create diff --git a/src/chapter2/2-4.md b/src/chapter2/2-4.md index da0b6d4..6ba33b4 100644 --- a/src/chapter2/2-4.md +++ b/src/chapter2/2-4.md @@ -10,17 +10,16 @@ ```typescript oakPath={`${oakFullpath}.${relative}`} ``` -当然,这里的*relative*并不能随便编写。如果父组件和子组件都有相关的Entity,它需要正确表达父子Entity之间的关系,您可以阅读并参见[相关章节](./1-1.md)。如果父组件是一个*Virtual*组件(不关联在任何Entity上),而子组件是一个*Entity*组件,则相对路径应该是: +当然,这里的*relative*并不能随便编写。如果父组件和子组件都有相关的Entity,它需要正确表达父子Entity之间的关系,您可以阅读并参见[相关章节](./1.md)。如果父组件是一个*Virtual*组件(不关联在任何Entity上),而子组件是一个*Entity*组件,则相对路径应该是: * 如果子组件是一个List组件,则relative为 ```${Entity}s```,Entity是子组件所属对象; -* 如果子组件是一个Detail/Upsert组件,则relative为```Entity```,Entity是子组件所属对象; +* 如果子组件是一个Detail/Upsert组件,则relative为```${Entity}```,Entity是子组件所属对象; ### 父子组件的关系 如果组件和其子组件都是Entity组件,则它们之间有三种典型的关系: -* 一对一:父亲是一个*detail/upsert*组件,其子组件也是一个*detail/upsert*组件。例如:假设我们设计了一个*Application*的详情页面,其中也要渲染其关联的*System*信息,则可以分成两个组件编写,并通过设置``````来指定相对关系。 -> 要注意,组件之间的一对一关系,代表的其实是对象之间的多对一关系。 -* 一对多:父亲是一个*detail/upsert*组件,其子组件是一个*list*组件。在[上一小节](2-3-3.md)的例子里就是这种关系,一个*System*的详情组件包含了一个*Application*的列表组件。 +* 一对一:父亲是一个*detail/upsert*组件,其子组件也是一个*detail/upsert*组件,此时父子组件所关联的Entity必然是多对一(子->父)。例如:假设我们设计了一个*Application*的详情页面,其中也要渲染其关联的父对象*System*信息,则可以分成两个组件编写,并通过设置``````来指定其相对关系。 +* 一对多:父亲是一个*detail/upsert*组件,其子组件是一个*list*组件,此时父子组件所关联的Entity是一对多(父->子)。在[上一小节](2-3-3.md)的例子里就是这种关系,一个*System*的详情组件包含了一个子对象*Application*的列表组件(一对多关系)。 * 多对一:父亲是一个*list*组件,其子组件是一个*detail/upsert*组件,此时,父子组件所属的*Entity*一定是相等的,它们之间的相对路径应是子组件对象的主键:`````` 在三种关系中,前两种关系都出现在父页面为*detail/upsert*页面时,此时,父子组件可以各自负责自身的取数和渲染,代码分离较为干净,而在第三种多对一关系中,如果子组件是无条件渲染(例如子组件渲染的是列表组件中的一行详情展示),则父组件的*projection*应当能覆盖子组件(甚至子孙组件)需要的*projection*,也就是说父组件应当帮助子组件取数。如果您未能保证这一点,当子组件渲染时发现数据不完整,则会自己发起请求去获取完整的数据,这可能会导致大量并发的网络请求。