查询和操作对象

编译

在编写或更新了Entity定义后,都需要执行命令来编译完整的对象数据字典

npm run make:domain

编译出来的数据字典声明在src/oak-app-domain目录下,同时也会编译出来一个数据的存储格式供框架引用,可以在代码中像这样去引用它们:

// EntityDict是数据字典声明,StorageSchema是存储格式定义
import { EntityDict, StorageSchema } from '@project/oak-app-domain';

数据字典和存储格式是整个Oak框架最核心的内容,贯穿于使用框架的各个层面,因此需要深刻理解。本章节将使用上小节的AddressArea对象,介绍一些查询和操作的核心概念。

编译后的对象结构

编译后的对象原生结构称为OpSchema,其结构仅仅在用户定义的属性上增加了一些通用的属性类型,以及将引用对象转化成了外键。

每个Entity的OpSchema可以在编译后的oak-app-domain/${Entity}/Schema.ts中查看,本章下面的大多数数据结构都是如此

对象上增加的通用属性包括:

属性类型含义
idstring<36>主键,uuid
$$createAt$$number创建时间戳(Date.now())
$$updateAt$$number更新时间戳
$$deleteAt$$number删除时间戳
$$seq$$int递增序列

查询(Select)

当查询一个对象时,传入的Select结构如下:

{
    data: Projection;
    filter: Filter;
    sorter: Sorter;
    indexFrom: number;
    count: number;
}

Projection

当查询对象时,通过Projection可以定义要查询对象的哪些属性(projection的命名本身就借鉴了数据库中的“投影”概念)。例如,对上一节中所定义的Address对象,查询时可以指定投影为:

{
    detail: 1,
    name: 1,
    phone: 1,
}

可以根据对象之间的关系将projection扩展到多对一的父对象上,实现级联查询

{
    detail: 1,
    name: 1,
    phone: 1,
    area: {     // 查询相关联的Area
        id: 1,
        name: 1,
        parent: {       // 查询Area所相关的更高层Area
            id: 1,
            name: 1,
        },
    },
}

也可以扩展到一对多的子对象上的级联查询,例如我们查询Area对象时,可以将之关联的Address一并查找出来:

{
    id: 1,
    name: 1,
    address$area: { // 查询和area相关的所有address对象
        $entity: 'area',
        data: {
            id: 1,
            name: 1,
            phone: 1,
        },
    },
}

通过这样的扩展,可以很方便的通过一次查询获得一个对象相关联的所有数据,查询结果会是一个复杂的数据对象(Schema

Oak框架还支持表达式查询和函数计算,例如如果想返回一个name和phone连接的字符串,可以这样写:

{
    id: 1,
    $expr: {
        $concat: [
            "姓名:",
            {
                '#attr': "name",
            },
            "手机号:",
            {
                '#attr': "phone",
            }
        ]
    }
}

Schema

Schema是对对象进行Select查询获得的数据结果格式。此时返回的对象除了自身的属性之外,还可能级联了其父对象与子对象的数据。 例如,在上面的例子里,查询到的Address数据结果中包含了其父对象Address的数据:

{
    id: 'xxx',
    name: 'xxxxx',
    phone: '139xxxxxxxx',
    areaId: 'xxxx',
    area: {
        id: '310100',
        name: '杭州市',
        parentId: '330000',
        parent: {
            id: '330000',
            name: '浙江省',
        },
    },
}

而查询到的Area数据结果就会包含其子对象Address的数据:

{
    id: '310100',
    name: '杭州市',
    address$area: [
        {
            id: 'xxx',
            name: 'xxxxx',
            phone: '139xxxxxxxx',
        },
        {
            id: 'xxx',
            name: 'xxxxx',
            phone: '139xxxxxxxx',
        }
    ]
}

Filter

Filter代表查询某个对象的条件,例如,我们要查询Area为杭州市,手机号码以139开头的Address,就可以这样写:

{
    areaId: '310100',
    phone: {
        $startsWith: '139',
    },
}

可见Filter的算子语法比较接近MongoDB,Oak框架在此基础上做了大量的扩展,例如,如果不知道杭州市的areaId,我们也可以这样写:

{
    area: {     // 把filter扩展到Area父对象上
        name: '杭州市
    },
    phone: {
        $startsWith: '139',
    },
}

同样的,Filter也可以被扩展到子对象上,比方说我们查询Area,条件是“该Area上至少有一条相关的Address,其手机号码以139开头”(不用考虑这个查询是否合理):

{
    address$area: {
        phone: {
            $startsWith: '139',
        },
    }
}

甚至我们可以查询“该Area上不能有任何一条手机号以139开头的ddress”:

{
    address$area: {
        phone: {
            '#sqp': 'not in',
            $startsWith: '139',
        },
    }
}

具体的Filter算子的语法比较丰富,我们罗列在下方,部分复杂的语法用户可以查阅oak-domain/src/types/Demand.ts

算子参数类型作用
$gtnumber | string大于
$gtenumber | string大于等于
$ltnumber | string小于
$ltenumber | string小于等于
$eqnumber | string | boolean等于
$nenumber | string | boolean不等于
$in(number | string)[]在……中
$nin(number | string)[]不在……中
$mod[number, number]取模
$between[number, number]在……之间(包括等于)
$startsWithstring以……开头
$endsWithstring以……结尾
$includesstring包含……
$existsboolean是否为空
$search
$language
string
'zh_CN'|'en_US'
全文检索(对象上必须声明了全文索引)
$andFilter[]
$orFilter[]

此外,Filter还能支持表达式计算和非结构化数据查询。todo

Sorter

Sorter表示查询时的排序。例如,我们查询Address时要求结果按手机号排序,可以这样写sorter:

[
    {
        $attr: {
            phone: 1,
        },
        $direction: 'ASC',
    },
]

写成数组的形式意味着我们可以按序支持多个sort条件,同时也支持将排序的属性扩展到多对一的父对象上:

[
    // 先按phone升序排,再按area.name降序排
    {
        $attr: {
            phone: 1,
        },
        $direction: 'ASC',
    },
    {
        $attr: {
            area: {
                name: 1,
            },
        },
        $direction: 'DESC',
    }
]

在查询中,如果不指定任何排序条件,则框架会自动添加一个$$createAt$$属性上的降序排序条件。

操作(Operate)

当要操作一个对象时,传入的数据结构如下:

{
    id: Uuid;
    action: Action;
    data: Data;
    filter: Filter;
}

Uuid

Operate的操作需要唯一编号,其作用是为了记录日志和在分布式环境下实现操作同步。可以使用下面两个函数来产生Uuid。

import { generateUuid, generateUuidAsync } from 'oak-domain/lib/utils/uuid';

一般来说,如果不是在有些前端环境中受到同步的限制,应优先使用异步产生函数。

Action

声明Operate的类型。有三种Action是公共的:create/update/remove,除此之外,用户在定义Entity时,所声明的Action也是有效的Action。

所有用户定义的Action从广义上来说都是update。如果用户在定义Action时,也定义了相应的状态转换矩阵(见编写对象),则在执行该动作时,会自动进行对象相应属性的状态检查以及更新其状态。

一般来说,对一个对象的操作如果有业务层面上的语义,推荐尽量细化成不同的Action,而不直接使用update。这样一来可以使对对象的操作历史更加清晰,二来也便于后续进行细粒度的权限控制。

Data

声明更新的数据。更新的数据可以是两种:

  1. 自身的数据属性
    例如要更新地址的phone和name:
    {
        phone: '138xxxxxxxx',
        name: '张小三',
    }

create操作必须传入id以及有效的所有声明非空属性,update只需要传入至少一个属性,而remove操作无需传入自身的属性。

  1. 级联数据属性 同Filter/Projection一样,更新的Data也支持级联更新,可以通过一次Operate请求,更新当前对象以及其级联对象上的属性。 例如,我们可以在更新Address时,同时去更新其相关联的父对象Area的数据(不考虑这个请求是否有意义):
    {
        phone: '138xxxxxxxx',
        name: '张小三',
        area: {
            id: '{uuid}',
            action: 'update',
            data: {
                name: '苏杭市',
            }
        },
    }

同样的,我们也可以在更新父对象Area时,更新其相关联的子对象Address的数据:

    {
        name: '苏杭市',
        address$area: {
            id: '{uuid}',
            action: 'update',
            data: {
                phone: '138xxxxxxxx',
            },
            filter: {
                name: '张小三',
            }
        }
    }

这条Operate在更新Area数据的同时,还会将“指向它的且『name为张小三』的所有的Address”数据的phone属性更新成138xxxxxxxx。

我们还可以在更新父对象的同时,插入一条子对象,像下面这样:

    {
        name: '苏杭市',
        address$area: {
            id: '{uuid}',
            action: 'create',
            data: {
                id: '{uuid}',
                phone: '138xxxxxxxx',
                name: '张小四',
                ....
            },
        }
    }

新插入的Address会自动和当前Area关联。

下面列出了框架所支持的级联更新的情况:

  • 子对象级联父对象
子对象父对象效果
createcreate父子对象和关联关系一起创建
updatecreate更新子对象、创建父对象及关联关系(如果原来子对象上有关联的父对象关系会丢失)
updateupdate更新子对象,同时更新关联的父对象
updateremove更新子对象,同时删除关联的父对象及关联关系
removeupdate移除子对象及关联关系,同时更新关联的父对象
removeremove同时移除父子对象,以及关联关系
  • 父对象级联子对象
父对象子对象效果
createcreate创建父子对象,并创建关联关系
updateupdate更新父对象,并更新关联的子对象
updateremove更新父对象,同时删除关联的子对象