XMeta 模型
- 文章作者: flytreeleft - flytreeleft@crazydan.org
- 文章链接: https://nop.crazydan.io/manual/xmeta
- 版权声明: 本文章采用许可协议《署名 4.0 国际 (CC BY 4.0)》,转载或商用请注明文章来源及作者信息。
本文还在编辑整理中,会存在较多错误,请谨慎阅读!
XMeta 为 Nop 平台内置的一种领域模型,主要用于描述业务对象的结构。其为
NopGraphQL 引擎的核心模型
IBizObject
的重要组成部分。
XMeta 解析后的模型类型即为
IObjMeta
。
XMeta 模型的结构定义在 /nop/schema/xmeta.xdef 中,通过分析该 xdef 文件,便可以了解和掌握 XMeta 完整的配置和结构信息。
在分析 xdef 文件时,需注意以下几点:
xdef:name
用于命名 XDef 节点,在 xdef 文件内的其他节点可以通过xdef:ref
引用该命名节点的定义。其名字也对应于为该节点所生成的 Java 类名;xdef:ref
用于引用内部或外部 xdef 定义,前者引用的是xdef:name
所指定的名字, 后者引用的则是 xdef 文件的虚拟文件系统(VFS)路径;xdef:ref
引用相当于继承,并且也可以在当前节点上添加其他属性或子节点;- XDef 之间通过
xdef:ref
实现扩展,而 XDSL 之间则通过x:extends
实现扩展;
XMeta 模型定义在 xmeta 文件(以 .xmeta 为后缀的文件)中, 该文件在 Nop 虚拟文件系统(VFS)中以
_vfs/{一级子目录名}/{二级子目录名}/model/{bizObjName}/{bizObjName}.xmeta
形式的路径存放(一二级子目录名中不能包含短横线 -,且字母需为小写形式),例如:
<meta xmlns:x="/nop/schema/xdsl.xdef"
x:schema="/nop/schema/xmeta.xdef"
displayName="部门"
>
<x:gen-extends>
<meta-gen:DefaultMetaGenExtends xpl:lib="/nop/core/xlib/meta-gen.xlib"/>
</x:gen-extends>
<x:post-extends>
<meta-gen:DefaultMetaPostExtends xpl:lib="/nop/core/xlib/meta-gen.xlib"/>
</x:post-extends>
<displayProp>name</displayProp>
<entityName>io.nop.demo.entity.DepartmentEntity</entityName>
<primaryKey>oid</primaryKey>
<keys>
<key name="UK_code" props="code"/>
</keys>
<orderBy>
<field name="code" desc="true"/>
</orderBy>
<tree parentProp="parentId" childrenProp="children"/>
<props>
<prop name="oid" propId="1" mandatory="true"
queryable="true" sortable="true"
insertable="true" updatable="false"
>
<schema type="java.lang.String" precision="32"/>
</prop>
<prop name="name" propId="2" mandatory="true"
queryable="true" sortable="true"
insertable="true" updatable="true"
>
<schema type="java.lang.String" precision="50"/>
</prop>
<prop name="code" propId="3" mandatory="true"
queryable="true" sortable="true"
insertable="true" updatable="false">
<schema type="java.lang.String" precision="50"/>
</prop>
<prop name="parentId" propId="4"
queryable="true" sortable="true"
insertable="true" updatable="true"
ext:relation="parent"
>
<schema type="java.lang.String" precision="32"/>
</prop>
<prop name="parent"
internal="true" queryable="true"
insertable="false" updatable="false" lazy="true"
ext:kind="to-one"
ext:joinLeftProp="parentId" ext:joinRightProp="oid"
>
<schema bizObjName="DepartmentEntity"/>
</prop>
<prop name="children"
internal="true"
insertable="false" updatable="false" lazy="true"
ext:kind="to-many"
ext:joinLeftProp="oid" ext:joinRightProp="parentId"
>
<schema>
<item bizObjName="DepartmentEntity"/>
</schema>
</prop>
</props>
</meta>
XMeta 结构
XMeta 模型的定义以 <meta />
为根节点。其第一层级的主要结构如下:
<meta xmlns:x="/nop/schema/xdsl.xdef"
x:schema="/nop/schema/xmeta.xdef"
displayName="xxxx"
>
<!-- ... -->
<displayProp>name</displayProp>
<description>xxxx</description>
<entityName>io.nop.demo.entity.XxxEntity</entityName>
<primaryKey>oid</primaryKey>
<keys> <key .../> </keys>
<filter>...</filter>
<orderBy> <field .../> </orderBy>
<tree .../>
<selections> <selection .../> </selections>
<props> <prop ...> </props>
</meta>
配置项 | 配置项类型 | 配置项名称 |
---|---|---|
displayName | string | 业务对象的名称 |
用于设置业务对象的显示名称,比如,为业务对象
| ||
biz:refsNeedToCheckWhenDelete | ||
biz:allowLeftJoinProps | ||
<displayProp /> | string | 显示数据名称的属性 |
用于显示业务对象数据名称的属性名,比如,在业务对象
在下拉列表、树形控件等需要显示数据名称的组件中需要引用 | ||
<description /> | string | 描述说明 |
用于阐述业务对象的作用、使用注意事项等,如:
| ||
<entityName /> | ORM 实体的名字 | |
用于设置与业务对象映射的 ORM 实体的名字(一般与实体的类名相同), 比如:
只有在创建了对应的 ORM 实体并启用 ORM 支持时才需要设置该值。 在集成了 NopORM 后,NopGraphQL
将为业务对象构造相应的 GraphQL 字段取值函数
注意: | ||
<primaryKey /> | ORM 实体的主键列表 | |
用于设置与 ORM 实体的主键相映射的属性名, 对于复合主键,则需要以逗号分隔多个属性名,例如:
NopGraphQL 对于映射了 ORM 实体的业务对象,将会为名字为 | ||
<keys><key /></keys> | ObjKeyModel | ORM 实体的唯一键 |
用于设置具有唯一性约束并与 ORM 实体的列名相映射的属性名, 例如:
在新增或修改 ORM 实体对象时,在 对 | ||
<filter /> | ORM 实体的默认过滤条件 | |
用于设置在查询该 ORM 实体时需始终附加的过滤条件。其附加逻辑见
此外,在获取、删除、更新、复制某个确定的 ORM 实体对象时,在 因此,其可用于限定用户只能查看和操作特定范围内的数据,例如:
| ||
<orderBy><field /></orderBy> | OrderFieldBean | ORM 实体的默认排序条件 |
用于设置在查询该 ORM 实体时需始终附加的排序条件,例如:
其附加逻辑见 对 | ||
<tree /> | ObjTreeModel | ORM 实体的树形结构配置 |
用于配置 ORM 实体的树形结构信息,例如:
在 对 | ||
<selections><selection /></selections> | ObjSelectionMeta | GraphQL 选择字段集 |
对 GraphQL 的选择字段集命名, 用于指定对业务对象的哪些属性做字段取值, 例如:
通过选择字段集的名字,可以以简单的方式引用常用的选择字段列表,不需要反复编写该列表。 对 | ||
<props><prop /></props> | ObjPropMetaImpl | 业务对象的属性定义 |
用于描述业务对象的属性信息,例如:
详细说明见《属性列表》。 |
ORM 实体唯一键
在新增或修改 ORM 实体对象时,CrudBizModel
将自动检查 XMeta 中受唯一性约束的属性(组合属性一起检查)在数据库中是否存在重复值。
若存在重复,则直接抛出异常,从而避免向数据库写入不唯一的数据,保证其唯一性。
其检查逻辑详见
CrudBizModel#checkUniqueForSave
、CrudBizModel#checkUniqueForUpdate
。
例如,配置 name
值唯一,code1
与 code2
的组合结果唯一:
<meta>
<!-- ... -->
<keys>
<key name="UK_name" props="name"/>
<key name="UK_code" props="code1,code2"/>
</keys>
<!-- ... -->
</meta>
注意,<keys />
本身没有配置属性,以下仅对其子节点 <key />
的结构进行说明:
配置项 | 配置项类型 | 配置项名称 | 是否必填 |
---|---|---|---|
name | string | 唯一键名 | 是 |
用于配置唯一键的名字,其对应数据库层面 在不同的 | |||
props | 唯一键属性名列表 | 是 | |
displayName | string | 唯一键的名称 | 否 |
用于设置唯一键的显示名称。一般显示 |
ORM 实体排序条件
在查询 ORM 实体时,CrudBizModel
将会始终向查询语句中附加在
XMeta 中配置的排序条件,从而保证查询结果始终是有序的。
其附加逻辑详见
CrudBizModel#prepareFindPageQuery
。
例如,配置按 status
降序排序,并按 name
升序排序:
<meta>
<!-- ... -->
<orderBy>
<field name="status" desc="true"/>
<field name="name" desc="false"/>
</orderBy>
<!-- ... -->
</meta>
注意,<orderBy />
本身没有配置属性,以下仅对其子节点 <field />
的结构进行说明:
ORM 实体树形结构
如果同一个 ORM 实体的对象之间存在父子或上下级关系,在逻辑上可构造成一棵或多棵树,
比如,父子部门,则可以在 XMeta 中配置 <tree />
节点,以指示如何映射对象之间的父子关系。
对于采用复合主键(在
<primaryKey />
中包含多个属性)的 ORM 实体,不适用于树形结构配置,在CrudBizModel
中尝试为其构造树形查询时将会抛出异常。
例如,配置以属性 parentId
指示父节点,并以属性 children
指示子节点的
ORM 实体对象树:
<meta>
<!-- ... -->
<tree parentProp="parentId" childrenProp="children"/>
<!-- ... -->
</meta>
配置项 | 配置项类型 | 配置项名称 | 是否必填 |
---|---|---|---|
parentProp | string | 指示父节点的属性名 | 是 |
用于设置与 ORM
实体的列名相映射的、
用于指示父节点的属性名,如 注意:该属性名对应的应该是父节点的主键值,而不是父节点的对象实例,也就是,应该使用
| |||
childrenProp | string | 指示子节点的属性名 | 否 |
用于设置与 ORM
实体的一对多关联属性名相映射的、
指示子节点对象集合的属性名,如
如果设置了该值,则在通过 | |||
levelProp | string | 指示节点级别的属性名 | 否 |
用于设置与 ORM
实体的列名相映射的、
用于指示节点级别的属性名,如 其可与 注意:该属性名对应的属性类型必须为整型,如 | |||
rootLevelValue | string | 根节点所对应的节点级别属性的值 | 否 |
用于设置在表示 ORM 实体对象树的根节点时,
则表示,只有满足 其必须与 注意:为其配置的值必须能够被正常转换为 | |||
rootParentValue | string | 根节点所对应的父节点属性的值 | 否 |
用于设置在表示 ORM 实体对象树的根节点时,
则表示,只有满足 若未设置其值,则缺省以 注意:以 | |||
sortProp | string | 指示用于节点排序的属性名 | 否 |
用于设置与 ORM
实体的列名相映射的、
用于指示节点排序的属性名,如 若设置了其值,则查询得到的节点将按其指定的属性名排序(未指定排序方向)。
而在其值未设定时,将缺省采用主键 | |||
isLeafProp | string | 指示叶子节点的属性名 | 否 |
预留配置,暂时未使用! |
在 CrudBizModel
中的以下 @BizQuery
查询函数与 XMeta 的 <tree />
配置相关:
#findRoots
: 根据levelProp
+rootLevelValue
或rootParentValue
的配置做根节点的过滤查询,返回根节点所对应的业务对象列表,如List<DepartmentEntity>
;#findTreeEntityPage
: 分页查询业务对象树的节点对象StdTreeEntity
,并返回PageBean<StdTreeEntity>
;#findTreeEntityList
: 与#findTreeEntityPage
处理相同,只是返回结果为List<StdTreeEntity>
,不包含分页信息;#findPageForTree
: 分页查询业务对象树的节点所对应的业务对象,并返回业务对象的PageBean<?>
结果,如PageBean<DepartmentEntity>
;#findListForTree
: 与#findListForTree
处理相同,只是返回结果为业务对象的List<?>
数据,如List<DepartmentEntity>
,不包含分页信息;
以上查询函数除 CrudBizModel#findRoots
以外,均会通过
TreeEntityHelper#buildTreeEntityBaseSql
构造以下形式的
Recursive EQL
查询语句以对业务对象树做过滤查询:
with recursive tree_page as (
select
b.oid as id, b.oid as joinId, b.parentId as parentId,
b.name as displayName, b.level as level, b.order as sortProp
from DepartmentEntity b
where ${query.filter} and b.parentId is null
union all
select
o.oid as id, o.oid as joinId, o.parentId as parentId,
o.name as displayName, o.level as level, o.order as sortProp
from DepartmentEntity o
inner join tree_page p on o.parentId = p.joinId
)
select
t.id, t.displayName, t.parentId, t.level, t.joinId
from tree_page t
order by t.sortProp
详细说明见文档《树形结构相关》。
在以上 EQL 语句中的 ${query.filter}
表示需插入的过滤条件,其由查询函数的参数 query: QueryBean
提供。从其插入位置可知,客户端指定的过滤条件仅对根节点有效,也就是最终的查询结果是满足过滤条件的根节点,
以及这些根节点在各个层级的全部子节点。
若是要返回指定节点及其子节点数据,则客户端可以在 GraphQL 服务调用数据中构造以下形式的过滤条件:
{
"query": "query ($query: QueryBeanInput) { ... }",
"variables": {
"query": {
"filter": {
"$type": "or",
"$body": [{
"name": "oid", "$type": "eq", "value": "目标节点 id"
}, {
// Note: 只有显式指定对父节点的过滤条件
// 才能替换默认的过滤条件:parentId is null
"name": "parentId", "$type": "eq", "value": "任何一个无效值"
}]
}
}
}
}
该方式仅适用于不是以
levelProp
+rootLevelValue
组合过滤根节点的情况。
需要注意的是,以上查询函数得到的结果都是平面结构的,而不是树形结构。
若是需要直接得到树形结构的数据,则可以尝试采用 NopGraphQL 内置的 GraphQL 指令
@TreeChildren
,以递归获取各个层级的子节点数据:
query ($query: QueryBeanInput) {
DepartmentEntity_findList(query: $query) {
value: oid
label: displayName
children @TreeChildren(max: 5)
}
}
该方式会比树形数据查询函数更加灵活, 只不过该方式在查询深度较大且子节点数量较多时会存在比较明显的性能问题, 需要权衡利弊后再选择合适的方案。
虽然可以采用批量加载机制降低性能影响,但依然需要逐个层级依次加载,而不像执行 Recursive EQL 那样可以直接获得各个层级的子节点数据。
GraphQL 选择字段集
在做 GraphQL 服务调用时,需要为 GraphQL 对象类型的字段指定 GraphQL SelectionSet, 也即选择字段集, 用于对父字段的取值结果做字段选择。
而针对业务对象的选择字段集可能出现在多个调用位置,并且存在多个相同的字段, 在此种情况下,便可以将这些相同字段组合起来并命名,再通过其名字进行引用, 从而实现对其的复用目的。
例如,定义一个 id
为 F_moreFields
的选择字段集:
<meta>
<!-- ... -->
<selections>
<selection id="F_moreFields">
oid, name, relatedRoleList{ oid, name }
</selection>
</selections>
<!-- ... -->
</meta>
注意,<selections />
本身没有配置属性,以下仅对其子节点 <selection />
的结构进行说明:
配置项 | 配置项类型 | 配置项名称 | 是否必填 |
---|---|---|---|
id | string | 选择字段集的标识 | 是 |
用于设置选择字段集的唯一标识,在 GraphQL Document 中以该标识引用其字段集合, 可以视为选择字段集的引用名字 | |||
displayName | string | 选择字段集的名称 | 否 |
用于设置选择字段集的显示名称 |
<selection />
标签的 body 内容为
GraphQL SelectionSet
结构,只是第一层字段不加花括号 {
、}
:
<meta>
<selections>
<selection id="F_defaults">
oid, name, status
</selection>
<selection id="F_moreFields">
oid, name, status
relatedRoleList {
oid, name
permissionList {
oid, name
}
}
</selection>
<selection id="copyForNew">
status, description
</selection>
</selections>
</meta>
对于选择字段集的标识 id
有如下使用约定:
id
为copyForNew
: 特定用于 GraphQL 变更函数CrudBizModel#copyForNew
中, 以指定在对 ORM 实体对象做复制新增时,默认可被复制的属性。 注:为了安全性,可复制的内容是不允许由前端指定的- 若是未定义该标识的选择字段集,则
CrudBizModel#copyForNew
将复制源对象的全部属性
- 若是未定义该标识的选择字段集,则
id
以F_
为前缀:表示在 GraphQL Document 中可以被引用的选择字段集。其中,F_defaults
表示默认的选择字段集,若未定义该id
的选择字段集,则将以 XMeta 中所有非 lazy 的属性作为选择字段;id
为其余形式:此类选择字段集的用途可根据业务需求自行确定。比如,先通过getObjMeta().getFieldSelection("my_selection")
获得id
为my_selection
的选择字段集合,再调用CrudBizModel#doSave
仅保存这些字段对应的属性数据;
下面列举一些选择字段集的使用样例:
- 引用
F_defaults
选择字段集:
// 等价于 REST 调用:/r/Book__get?id=123
// 或 /r/Book__get?id=123&@selection=...F_defaults
query {
Book__get(id: 123) {
...F_defaults
}
}
// 等价于 REST 调用:/r/NopAuthUser__findList?@selection=...F_defaults,groupMappings
// 或 /r/NopAuthUser__findList?@selection=...F_defaults,groupMappings{...F_defaults}
query ($query: QueryBeanInput) {
NopAuthUser__findList(query: $query) {
...F_defaults
groupMappings { ...F_defaults }
}
}
仅 REST 调用可以省略
...F_defaults
,在 GraphQL Document 中不可省略。
- 引用其他
F_
前缀的选择字段集:
// 等价于 REST 调用:/r/NopAuthUser__findList?@selection=...F_moreFields
query ($query: QueryBeanInput) {
NopAuthUser__findList(query: $query) {
...F_moreFields
}
}
REST 调用中也不可省略选择字段集的标识,必须明确指定,否则将按照
...F_defaults
做字段选择。
属性列表
目前仅关注与后端处理相关的 <prop />
配置项,对于仅在 XView 层面使用的配置项(如
ui:maxUploadSize
等)暂时未做整理。
在 XMeta 中,业务对象的属性由 <prop />
节点定义,如下所示:
<meta>
<!-- ... -->
<props>
<prop name="oid" ...>
<schema type="java.lang.String"/>
</prop>
<prop name="name" ...>
<schema type="java.lang.String"/>
</prop>
<!-- ... -->
</props>
</meta>
<prop />
的结构定义在 /nop/schema/schema/obj-schema.xdef 中。
通过 <prop />
节点可以完整描述业务对象属性的基本信息,并且可附加与 GraphQL、ORM
等相关的配置信息。下面将对该节点的各个配置项进行详细说明:
配置项 | 配置项类型 | 配置项名称 | 是否必填 |
---|---|---|---|
name | 属性名 | 是 | |
用于设置业务对象属性的名字,如
通过属性名可以直接与 ORM 实体对象中的列属性、关联属性、别名属性、组件属性等属性做同名映射, 也同样可以与 GraphQL 中的字段做同名映射。 也即,通过属性的名字便可获得其在 ORM 和 GraphQL 领域中的配置信息。 设置的属性名一般为 prop-name 形式,并且只有该形式的属性名才能做同名映射。 属性名也可以是以
为了安全性,NopGraphQL 默认是不允许对关联对象的属性直接做增改查等操作的。
只能为关联对象的相关属性定义相应的复合属性,如 反言之,若是没有对关联对象的属性做增改查的需求,则不需要定义复合形式的属性。 注意:复合属性不能作为 GraphQL 的字段。相关处理逻辑见
| |||
<schema /> | ISchema | 属性的类型模式 | 否 |
用于设置业务对象属性的数据类型、数据精度等信息,例如:
通过该配置项,可以自动进行与属性相关的数据转换和数据校验等方面的处理操作。 对其结构的详细说明见参考手册《基础 DSL:Schema》。 注意:若未设置该配置项,则默认将业务对象属性视为 | |||
displayName | string | 属性的名称 | 否 |
用于设置业务对象属性的显示名称,例如:
| |||
<description /> | string | 属性的描述内容 | 否 |
用于设置对业务对象属性的描述内容,如:
| |||
defaultValue | 属性的缺省值 | 否 | |
[ORM Only] 用于设置业务对象属性的缺省值,如:
在调用 GraphQL 变更函数
| |||
propId | int | 属性的唯一编号 | 否 |
用于设置属性的唯一编号,例如:
在与 gRPC 集成时,该编号对应于 Protocol Buffers
中的字段编号。
其映射逻辑见
当业务对象属性与 ORM
实体的列相映射时,
该属性编号与列的
属性编号的值必须大于 注意:在与 NopORM、gRPC 的列或字段映射以外的场景中,不需要设置该配置项。 | |||
mapToProp | 属性所映射的 ORM 实体对象属性名 | 否 | |
[ORM Only] 用于为 ORM 实体对象的属性定义一个别名,例如:
其表示为 ORM 实体对象的 比如,通过别名
还可以通过别名 定义 GraphQL 变更文档
通过别名设置待变更属性的值
最终,为别名
注意:被映射的 ORM 实体对象属性并不要求在业务对象上也有相映射的属性,
例如上例中的 | |||
depends | 属性所依赖的 ORM 实体对象属性 | 否 | |
[ORM Only] 用于设置在对业务对象属性做 GraphQL 字段取值时,需要先获取 ORM 实体对象中的哪些关联属性的值。 也就是,配置对该属性的取值所要依赖的属性,例如:
可以看到,属性
该配置项的值是以 所配置的依赖属性必须为 ORM 实体对象中的关联属性,
但其可以在当前的 XMeta 中有相应的映射属性,也可以没有。若未定义相应的
XMeta 属性,则需为该属性名添加前缀
注意:若在 | |||
mandatory | boolean | 是否为必填属性 | 是 |
若设置其值为 在对 ORM 实体对象做新增或修改时,必须为必填属性设置非空值,
否则,将抛出属性值为空的异常
| |||
internal | boolean | 是否为内部属性 | 是 |
若设置其值为 在 Nop XView 中默认不会在生成的页面中为内部属性创建相应的组件。 但标记内部属性,并不影响其作为 GraphQL 的选择字段,也不影响对其所映射的 ORM 实体对象属性做新增或修改操作。
| |||
deprecated | boolean | 是否为已废弃属性 | 是 |
若设置其值为 | |||
insertable | boolean | 是否为可新增属性 | 是 |
若设置其值为
注意:在调用变更函数 | |||
updatable | boolean | 是否为可更新属性 | 是 |
若设置其值为
| |||
virtual | boolean | 是否为虚拟属性 | 是 |
若设置其值为
| |||
published | boolean | 是否为开放属性 | 是 |
若设置为
对于密码等敏感数据,需设置该配置项为 | |||
exportable | boolean | 是否为可导出属性 | 是 |
只有将其设置为
注意:该配置项目前还未真正使用。 | |||
sortable | boolean | 是否为可排序属性 | 是 |
只有将其设置为 声明属性可排序
指定属性的排序方向
其缺省值为
| |||
queryable | boolean | 是否为可查询属性 | 是 |
只有将其设置为 声明属性可查询
构造过滤条件
其缺省值为
该配置将与 注意:对于作为过滤器转换的业务对象属性,
则需要强制设置该配置项为 | |||
allowFilterOp | 可在属性上应用的过滤运算符 | 否 | |
用于设置可在业务对象属性上应用的过滤运算符,例如: 声明许可的过滤运算符
应用许可范围内的过滤运算符
该配置项的值是以 缺省只支持
注意:只有 | |||
lazy | boolean | 是否为懒加载属性 | 是 |
用于标记业务对象属性是否为懒加载的。缺省为 若业务对象属性映射的是 ORM 实体对象的关联属性,
则该配置项应始终为
| |||
tagSet | 特性标记列表 | 否 | |
[ORM Only] 该配置项的值与业务对象属性所映射的 ORM
实体对象的列属性、关联属性、别名属性、组件属性上的
| |||
graphql:type | string | 属性的 GraphQL 类型 | 否 |
用于显式设置业务对象属性的 GraphQL 类型,NopGraphQL 将按此设置对业务对象属性所对应的 GraphQL 字段做类型转换,例如:
其中,
该配置项的可选值为枚举类
默认通过 `
此外,对于映射到类型为 但在做 GraphQL 变更时,
客户端提供的输入数据则需由相应的业务操作函数进行转换处理,
如
注意:对于类型的转换,不是从任意类到任意类型均可支持的,
需要由转换的目标类型确定可被转换的源类型。对于 Nop 内置的标准类型可通过
| |||
graphql:mapper | string | 否 | |
预留配置,暂时未使用! | |||
graphql:labelProp | 作为属性的显示文本的属性 | 否 | |
[View Only] 当业务对象属性是对象类型(含对象列表)或数据字典时, 一般需要声明使用业务对象中的哪个属性的值作为其显示文本,例如:
其优先级高于 详细说明见《XMeta 属性显示文本》。 注意:该配置项不是强制性的,其主要在 Nop XView 中用于指定从哪个属性中获取业务对象属性的显示文本信息。 | |||
graphql:dictName | string | 属性所对应的数据字典 | 否 |
当业务对象属性的值对应的是数据字典的显示文本时, 需通过该配置项指定从哪个数据字典中获取该属性的值,例如:
该配置项必须与 也就是,
详细说明见《XMeta 属性显示文本》。 | |||
graphql:dictValueProp | 作为数据字典值的属性 | 否 | |
该配置项必须与 | |||
graphql:joinLeftProps | 否 | ||
预留配置,暂时未使用! | |||
graphql:joinRightProps | 否 | ||
预留配置,暂时未使用! | |||
graphql:datePattern | string | 日期转换格式 | 否 |
用于设置对业务对象属性对应的 GraphQL 字段的取值结果做日期格式化,例如:
若要支持对日期的格式化,需满足前提条件:应用配置项
该配置项支持的格式化形式如下:
| |||
<graphql:transFilter /> | xpl-fn: | 属性的过滤条件转换函数 | 否 |
若设置了该配置项,则在业务对象属性上构造的查询过滤条件, 将在做过滤查询前,被替换为该转换函数所构造的新的过滤条件,例如:
其可以将复杂的过滤条件以属性的形式进行引用,从而简化对过滤条件的构造逻辑。 但是,此类属性只能用于构造过滤条件,而不能被用于 GraphQL 字段选择。
并且,对其的使用也必须符合 详细说明见《XMeta 过滤条件转换》。 | |||
ui:maskPattern | string | 属性的掩码模式 | 否 |
用于设置对业务对象属性对应的 GraphQL 字段的取值结果做掩码处理,从而实现对敏感数据的脱敏,例如:
该配置项指定的掩码由函数
对
注意:若是要设置自定义的输出函数 | |||
biz:moduleId | string | 属性对应的关联对象所属的模块 | 否 |
用于设置业务对象属性所关联的业务对象所属的 Nop 模块标识,例如:
在属性所关联的业务对象来自于外部模块时,需设置该配置项以标记其所属的模块。 注意:目前还未发现其实际用途,仅将其当作业务对象所属模块的识别标记即可。 | |||
biz:codeRule | string | 编码属性的生成规则 | 否 |
在业务对象属性是形式统一且具备唯一性的某种编码时,可以通过该配置项指定其生成规则, 可用于自动生成订单号、卡号等,例如:
该配置项指定的编码规则由接口 对
在引入了 Nop 模块 注意:若已设置了配置项 | |||
ext:kind | string | 属性类别 | 否 |
用于设置业务对象属性映射的 ORM 实体对象属性所属的类别,例如:
该配置项可设置的值如下:
通过对属性所属类别的标记,并配合
注意:对于与 ORM 文件组件相对应的业务对象属性,会通过
| |||
ext:relation | string | 关联映射到的属性 | 否 |
用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》 | |||
ext:joinLeftProp | string | 关联的源端对象的属性 | 否 |
用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》 | |||
ext:joinRightProp | string | 关联的目标端对象的属性 | 否 |
用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》 | |||
ext:joinRightDisplayProp | string | 关联的目标端对象的显示属性 | 否 |
用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》 | |||
orm:manyToManyRefProp | string | 多对多关联的中间模型中对端的属性 | 否 |
用于设置业务对象间的多对多关联关系。详细说明见《XMeta 对象关联配置:多对多》 | |||
graphql:queryMethod | GraphQLQueryMethod | 关联过滤查询所采用的方法名 | 否 |
用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》。
| |||
graphql:connectionProp | 关联过滤查询所对应的关联属性 | 否 | |
用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》。
| |||
graphql:maxFetchSize | int | 关联过滤查询一次所能取得的最大数据量 | 否 |
用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》。
| |||
<graphql:orderBy /> | OrderFieldBean | 关联过滤查询的默认排序条件 | 否 |
用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》。
| |||
<graphql:filter /> | 关联过滤查询的默认过滤条件 | 否 | |
用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》。
注意:若在 XMeta 根节点上设置了配置项
| |||
graphql:authObjName | string | 关联过滤查询所应用的数据权限模型 | 否 |
用于设置在关联过滤查询时所应用的数据权限模型的名字, 用于控制关联对象的查询结果。缺省采用被过滤的业务对象的名字。
注意:该配置项仅在设置了 | |||
<graphql:inputType /> | GraphQL 字段参数的类型 | 否 | |
用于设置业务对象属性对应的 GraphQL 字段的参数类型, 从而便于对 GraphQL 字段参数进行数据校验和类型转换,例如:
对 GraphQL 字段参数的强类型定义,需满足以下条件:
该配置项与
注意:在设置了 | |||
<arg /> | ObjPropArgModel | GraphQL 字段的参数项 | 否 |
用于定义业务对象属性对应的 GraphQL 字段的参数项,例如:
该配置项与 详细说明见《GraphQL 字段参数项定义》。 | |||
<transformOut /> | 属性的输出转换函数 | 否 | |
用于定义对业务对象属性的输出转换函数,例如:
所配置的输出转换函数将被构造为业务对象属性对应的 GraphQL 字段的取值函数,
以便于向客户端返回其所接受的数据形式。比如上例中,将属性值
详细说明见《XMeta 输出转换》。 注意: | |||
<getter /> | 属性的 getter 函数 | 否 | |
用于定义业务对象属性的 getter 函数,例如:
若设置了该配置项,则其将作为业务对象属性对应的 GraphQL 字段的默认取值函数,
在该函数内可通过父字段的取值结果 客户端字段取值
定义 getter 函数
该配置项与 该 getter 函数的可用参数如下:
注意:该配置项可以与 | |||
<transformIn /> | 属性输入转换函数 | 否 | |
用于定义对业务对象属性的输入转换函数,例如:
通过输入转换函数,可以在新增(
详细说明见《XMeta 输入转换》。 注意:配置项 insertable
或 updatable 为 | |||
<setter /> | 属性的 setter 函数 | 否 | |
[ORM Only] 用于定义业务对象属性的 setter 函数,例如:
通过 setter 函数,
可以在新增(
该配置项与 该 setter 函数的可用参数如下:
注意:该配置项可以与 注意:在配置项 virtual 为 | |||
<autoExpr /> | ObjConditionExpr | 属性的缺省值计算函数 | 否 |
[ORM Only] 用于定义在新增、更新或复制新增时,业务对象属性映射的 ORM 实体对象属性的缺省值计算函数,例如:
以上表示根据当前属性的配置项
该计算函数的可用参数如下:
详细说明见《XMeta 属性自动计算》。 注意:在配置项 virtual 为 | |||
<auth /> | ObjPropAuthModel | 字段级别的访问控制 | 否 |
用于配置对业务对象属性的读写权限,从而实现对业务对象属性的访问权限控制。 详细说明见《XMeta 属性访问控制》 | |||
<graphql:selection /> | 默认的 GraphQL 选择字段集 | 否 | |
[View Only]
若业务对象属性映射的是一个对象,则可以配置该属性,用于指定默认的
GraphQL Field Selection。
注:目前仅在 XView 中构造 GraphQL Field Selection 会时用到,具体处理逻辑见
| |||
graphql:jsonComponentProp | string | 业务对象属性对应的 JSON 组件属性名 | 否 |
[View Only]
在业务对象属性为 JSON 文本时会自动构造一个 <schema /> 为
|
XMeta 属性显示文本
在业务对象的属性对应的是数据字典,或者为关联对象的 ID 列表时,
可以在业务对象中定义一个作为其显示文本的属性,以便于在前端显示,再通过
graphql:labelProp
指向该显示文本属性,以建立二者之间的引用关系,比如:
<meta>
<props>
<prop name="status"
graphql:labelProp="status_label"
>
<schema type="Integer" dict="auth/user-status"/>
</prop>
<prop name="status_label"
graphql:dictValueProp="status"
graphql:dictName="auth/user-status"
>
<schema type="String"/>
</prop>
</props>
</meta>
在本例中,status
赋值的是数据字典 auth/user-status
的 value
值,其类型是 Integer
,若在前端直接显示该属性值,则不方便用户进行识别,
因此,可以为 status
构建一个相应的用于显示其对应数据字典的 label
文本的属性 status_label
。
NopGraphQL 引擎将会根据 graphql:dictValueProp
所指定的业务对象属性(即本例中的 status
)的实际值,从 graphql:dictName
对应的数据字典(auth/user-status
)中获取到字典枚举值的显示文本,
并将该文本内容作为 status_label
的值返回给前端。
前端仅需要在 GraphQL Field Selection 中包含 status_label
即可得到 status
对应的显示文本内容,从而避免前端单独处理对数据字典的文本回显:
query {
NopAuthUser_get(id: "1427826172") {
id
status
status_label
}
}
而对于一对多/多对多的对象关联场景中:
<meta>
<props>
<prop name="relatedRoleList"
ext:kind="to-many"
lazy="true"
>
<schema>
<item bizObjName="NopAuthRole"/>
</schema>
</prop>
<prop name="relatedRoleIdList"
ext:relation="relatedRoleList"
graphql:labelProp="relatedRoleList_label"
lazy="true"
>
<schema type="List<java.lang.String>"/>
</prop>
<prop name="relatedRoleList_label"
lazy="true"
>
<schema type="String"/>
</prop>
</props>
</meta>
可为关联目标端的主键(id
)列表 relatedRoleIdList
构造一个对应的显示文本列表
relatedRoleList_label
,而在 Nop Orm 层中将会在实体对象的
Java 代码中为属性 relatedRoleList_label
自动生成如下 getter 代码:
public String getRelatedRoleList_label() {
return io.nop.core.lang.utils.Underscore.pluckThenJoin(
getRelatedRoleList(),
io.nop.auth.dao.entity.NopAuthRole.PROP_NAME_roleName
);
}
也就是,从关联目标端列表 relatedRoleList
中依次取其显示属性(本例中为
roleName
)的值,再以逗号 ,
分隔组成字符串(Underscore#pluckThenJoin
)后返回。
如此,前端便可以在获取关联目标端 id 列表的同时获取对应的显示文本列表:
query {
NopAuthUser_get(id: "1427826172") {
id
relatedRoleIdList
relatedRoleIdList_label
}
}
当然,在实际使用时也没有必要分别获取 id 及其显示文本列表, 直接返回关联对象列表及其必要属性,应该作为优先选择方案:
query {
NopAuthUser_get(id: "1427826172") {
id
relatedRoleList {
id
roleName
}
}
}
XMeta 过滤条件转换
对业务对象的过滤可能会涉及较为复杂的组合条件(如,子查询等),或者是组合条件会出现多次, 亦或是直接拼接 SQL 片段,在这些情况下,便需要通过过滤器转换机制来实现。
过滤器转换机制就是通过一个转换函数,对过滤器(其为 TreeBean
类型)的树形结构上的节点(子过滤器)进行替换。
具体的转换逻辑见
io.nop.api.core.beans.query.QueryBean#transformFilter
。
在 XMeta 中仅需要定义一个配置了过滤器转换函数 graphql:transFilter
的对象属性即可,例如:
<meta>
<x:gen-extends>
<meta-gen:DefaultMetaGenExtends xpl:lib="/nop/core/xlib/meta-gen.xlib"/>
</x:gen-extends>
<props>
<!-- Note:只有可查询(queryable = true)的属性才能参与过滤运算 -->
<prop name="hasResourceStatus" queryable="true">
<graphql:transFilter>
<filter:sql>
exists (
select o2
from NopAuthResource o2
where
o2.siteId = o.id
and o2.status >= ${ filter.getAttr('value') }
)
</filter:sql>
</graphql:transFilter>
</prop>
</props>
</meta>
在该例中,为对象属性 hasResourceStatus
配置了过滤器转换函数,其通过 Xpl 函数
filter:sql
构造了一个包含 SQL 片段的子过滤器,用于替换以 hasResourceStatus
作为运算条件的过滤器,并且,在该 SQL 片段中还以 ${ filter.getAttr('value') }
形式引用了被替换过滤器的属性 value
的值。
注意,Xpl 函数
meta-gen:DefaultMetaGenExtends
会在 XMeta 解析前全局引入filter:sql
所在的函数库/nop/core/xlib/filter.xlib
(即<c:import from="/nop/core/xlib/filter.xlib"/>
), 因此,不需要再在<filter:sql/>
节点上配置xpl:lib
属性(即<filter:sql xpl:lib="/nop/core/xlib/filter.xlib"/>
)。
在调用对应的 GraphQL 接口时,可以构造如下形式的根过滤器 filter
:
{
"query": {
"filter": {
"$type": "and",
"$body": [
{
"$type": "gt", "name": "orderNo", "value": 100
},
{
"$type": "eq", "name": "hasResourceStatus", "value": 1
}
]
}
}
}
以上调用最终生成的 SQL 如下:
select o
from NopAuthSite o
where
o.orderNo > 100 and
exists (
select o2
from NopAuthResource o2
where o2.siteId = o.id
and o2.status >= 1
)
也就是,以 hasResourceStatus
作为运算条件的过滤器均会被替换为
graphql:transFilter
所构造出的过滤器。
graphql:transFilter
的类型是 xpl-fn
,即,一个 Xpl 函数,其函数签名为
(filter, query, forEntity) => any
,函数参数分别为:
filter
:类型为TreeBean
,表示将要被替换的过滤器,即上例中的{"$type": "eq", "name": "hasResourceStatus", "value": 1}
;query
:类型为QueryBean
,对应于 GraphQL 接口中的query
参数;forEntity
:类型为Boolean
,始终为false
;
graphql:transFilter
的执行逻辑见io.nop.biz.crud.BizQueryHelper#transformFilter
。
根据 TreeBean#transformChild
的处理逻辑可以发现 graphql:transFilter
函数的返回值可以是 Boolean
、XNode
、null
或者 Collection<XNode>
,
因此,该函数的签名中指定的返回值类型为 any
,并未直接限定返回 XNode
。
但实际开发中,该函数一般只会返回 XNode
节点,以构造过滤条件,
而在构造过程中可以通过 ${...}
引用该函数的参数,比如,前例中的
${ filter.getAttr('value') }
表示从参数 filter
中取其属性名为
value
的值(即,1
)。
在
TreeBean
中除了属性$type
是通过TreeBean#getTagName
获取值以外,其余的属性均通过TreeBean#getAttr
获取属性值。
由于 graphql:transFilter
函数的返回值是 XNode
节点,因此,除了通过
filter:sql
构造 SQL 过滤节点以外,还可以直接构造 EQL 过滤节点,甚至是二者共用:
<prop name="withOrderNoAndResourceStatus" queryable="true">
<graphql:transFilter>
<and xpl:outputMode="node">
<gt name="orderNo" value="${ filter.getAttr('orderNo') }"/>
<filter:sql>
exists (
select o2
from NopAuthResource o2
where
o2.siteId = o.id
and o2.status >= ${ filter.getAttr('resourceStatus') }
)
</filter:sql>
</and>
</graphql:transFilter>
</prop>
graphql:transFilter
本身不支持输出,所以,需要在其标签内通过输出模式xpl:outputMode
为node
的 Xpl 脚本输出一个XNode
节点;
在前端仅需要构造一个如下的过滤器即可得到与前面例子相同的过滤条件:
{
"query": {
"filter": {
"$type": "eq",
"name": "withOrderNoAndResourceStatus",
"orderNo": 100,
"resourceStatus": 1
}
}
}
注意,在该过滤器中不再设置 value
属性,而是分别指定了两个混合过滤器所需的参数
orderNo
和 resourceStatus
,并调用 TreeBean#getAttr
获得了过滤器的传入值。
可以发现,虽然上面的过滤器传入了两个附加参数,但依然采用的是 eq
运算符。
这是因为,在属性定义上,默认的 allowFilterOp
(允许的过滤运算)仅有 eq
和 in
。
若是需要采用其他运算符,则需要显式设置 allowFilterOp
,比如:
<prop name="withOrderNoAndResourceStatus"
queryable="true"
allowFilterOp="with"
>
<graphql:transFilter>
<!-- ... -->
</graphql:transFilter>
</prop>
然后,构造过滤器为:
{
"query": {
"filter": {
"$type": "with",
"name": "withOrderNoAndResourceStatus",
"orderNo": 100,
"resourceStatus": 1
}
}
}
此外,filter
实际所使用的运算符可以通过 filter.getTagName()
得到,
所以,在某些动态场景下,还可以根据实际使用的运算符来构造不同的过滤器:
<prop name="resourceStatus"
queryable="true"
allowFilterOp="eq,in"
>
<graphql:transFilter>
<c:choose xpl:outputMode="node">
<when test="${ filter.getTagName() == 'eq' }">
<filter:sql>
exists (
select o2
from NopAuthResource o2
where
o2.siteId = o.id
and o2.status = ${ filter.getAttr('value') }
)
</filter:sql>
</when>
<when test="${ filter.getTagName() == 'in' }">
<filter:sql>
exists (
select o2
from NopAuthResource o2
where
o2.siteId = o.id
and o2.status in (${ filter.getAttr('value') })
)
</filter:sql>
</when>
<otherwise>
<!-- Note:若不做任何处理,则会删除待替换的过滤器 filter -->
<c:throw
errorCode="nop.err.xmeta.trans-filter.not-supported-op"
params="${ {name: filter.getAttr('name'), op: filter.getTagName()} }"
/>
<!-- 直接返回待替换的过滤器 filter,不做替换或删除 -->
<!--<c:script>filter</c:script>-->
</otherwise>
</c:choose>
</graphql:transFilter>
</prop>
在涉及多分支判断时,不能采用
c:if
或xpl:if
做分支处理,否则graphql:transFilter
将返回最后一个 if 分支的结果,若该分支不满足判断条件,则实际将返回null
,而不是满足判断条件的分支结果。
如此,便可以按需使用 eq
或 in
过滤器来进行过滤查询:
{
"query": {
"filter": {
"$type": "eq", "name": "resourceStatus", "value": 1
}
}
}
// 或者
{
"query": {
"filter": {
"$type": "in", "name": "resourceStatus", "value": [1, 2, 3]
}
}
}
XMeta 对象关联配置
一对一
根据以上图例所生成的 XMeta 为:
<meta>
<props>
<prop name="rightId" ext:relation="right">
<schema type="java.lang.Integer"/>
</prop>
<prop name="right"
ext:kind="to-one"
ext:joinLeftProp="rightId"
ext:joinRightProp="id"
ext:joinRightDisplayProp="displayName"
lazy="true"
>
<schema bizObjName="Right"/>
</prop>
</props>
</meta>
ext:relation
用在Left
(关联的源端)直接与关联目标端(Right
)建立关联的属性上, 其指向在Left
中与映射到关联目标端对象的属性上,如,rightId -> right
;- 在与关联目标端对象映射的属性上声明关联关系,包括:
ext:kind
、ext:joinLeftProp
、ext:joinRightProp
等; ext:kind
设置为to-one
(一对一)模式关联Right
;ext:joinLeftProp
表示在Left
(关联的源端)中用于与Right
(关联的目标端)建立关联的属性;ext:joinRightProp
表示在Left
(关联的源端)中对应的ext:joinLeftProp
所指向的Right
(关联的目标端)的属性;ext:joinRightDisplayProp
表示关联目标端(Right
)用于显示对象名称的属性(显示名),如,displayName
;- 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,
lazy="true"
;
一对多
注意,一对多和一对一是互为反方向的关联配置,因此,二者是分别配置在关联的源端和目标端中的。
根据以上图例所生成的 XMeta 为:
<meta>
<props>
<prop name="leftList"
ext:kind="to-many"
ext:joinLeftProp="id"
ext:joinRightProp="rightId"
ext:joinRightDisplayProp="displayName"
lazy="true"
>
<schema>
<item bizObjName="Left"/>
</schema>
</prop>
</props>
</meta>
ext:kind
设置为to-many
(一对多)模式关联Left
;ext:joinRightProp
表示在Left
(关联的目标端)中用于与Right
(关联的源端)建立关联的属性;ext:joinLeftProp
表示在Left
(关联的目标端)中对应的ext:joinRightProp
所指向的Right
(关联的源端)的属性;ext:joinRightDisplayProp
表示关联目标端(Left
)用于显示对象名称的属性(显示名),如,displayName
;- 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,
lazy="true"
;
多对多
在 Nop 中是通过中间模型来建立多对多的关联,并通过中间模型将多对多分解为中间模型与关联双方的一对多关联:
详细的说明文件见文档《多对多关联》。
根据以上图例所生成的 XMeta 分别为:
- 配置
Left
与Ref
的一对多关联,也就是,通过Ref
可以获取到关联上的多个Right
<meta>
<props>
<prop name="rightMappings"
ext:kind="to-many"
ext:joinLeftProp="id"
ext:joinRightProp="leftId"
orm:manyToManyRefProp="rightId"
lazy="true"
>
<schema>
<item bizObjName="Ref"/>
</schema>
</prop>
<prop name="relatedRightIdList"
ext:relation="relatedRightList"
graphql:labelProp="relatedRightList_label"
lazy="true"
>
<schema type="List<java.lang.Integer>"/>
</prop>
<prop name="relatedRightList" ext:kind="to-many" lazy="true">
<schema>
<item bizObjName="Right"/>
</schema>
</prop>
<prop name="relatedRightList_label" lazy="true">
<schema type="String"/>
</prop>
</props>
</meta>
ext:kind
设置为to-many
(一对多)模式关联Ref
;ext:joinRightProp
设置为在Ref
(关联的目标端)中用于与Left
(关联的源端)建立关联的属性;ext:joinLeftProp
设置为在Ref
(关联的目标端)中对应的ext:joinRightProp
所指向的Left
(关联的源端)的属性;orm:manyToManyRefProp
设置为在Ref
中用于与Right
(即,多对多的目标端模型)建立关联的属性;relatedRightList
和relatedRightIdList
为根据orm:manyToManyRefProp
在Left
模型上自动生成的属性,以便于直接获取多对多关联中的对端的对象和对象id
列表;ext:relation
参考一对一的说明;graphql:labelProp
的配置说明见《XMeta 属性显示文本》;- 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,
lazy="true"
;
- 配置
Right
与Ref
的一对多关联,也就是,通过Ref
可以获取到关联上的多个Left
<meta>
<props>
<prop name="leftMappings"
ext:kind="to-many"
ext:joinLeftProp="id"
ext:joinRightProp="rightId"
orm:manyToManyRefProp="leftId"
lazy="true"
>
<schema>
<item bizObjName="Ref"/>
</schema>
</prop>
<prop name="relatedLeftIdList"
ext:relation="relatedLeftList"
graphql:labelProp="relatedLeftList_label"
lazy="true"
>
<schema type="List<java.lang.Integer>"/>
</prop>
<prop name="relatedLeftList" ext:kind="to-many" lazy="true">
<schema>
<item bizObjName="Left"/>
</schema>
</prop>
<prop name="relatedLeftList_label" lazy="true">
<schema type="String"/>
</prop>
</props>
</meta>
ext:kind
设置为to-many
(一对多)模式关联Ref
;ext:joinRightProp
设置为在Ref
(关联的目标端)中用于与Right
(关联的源端)建立关联的属性;ext:joinLeftProp
设置为在Ref
(关联的目标端)中对应的ext:joinRightProp
所指向的Right
(关联的源端)的属性;orm:manyToManyRefProp
设置为在Ref
中用于与Left
(即,多对多的目标端模型)建立关联的属性;relatedLeftList
和relatedLeftIdList
为根据orm:manyToManyRefProp
在Right
模型上自动生成的属性,以便于直接获取多对多关联中的对端的对象和对象id
列表;ext:relation
参考一对一的说明;graphql:labelProp
的配置说明见《XMeta 属性显示文本》;- 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,
lazy="true"
;
- 配置
Ref
与Left
和Right
的一对一关联
<meta>
<props>
<prop name="leftId" ext:relation="left">
<schema type="java.lang.Integer"/>
</prop>
<prop name="rightId" ext:relation="right">
<schema type="java.lang.Integer"/>
</prop>
<prop name="left"
ext:kind="to-one"
ext:joinLeftProp="leftId"
ext:joinRightProp="id"
lazy="true"
>
<schema bizObjName="Left"/>
</prop>
<prop name="right"
ext:kind="to-one"
ext:joinLeftProp="rightId"
ext:joinRightProp="id"
lazy="true"
>
<schema bizObjName="Right"/>
</prop>
</props>
</meta>
ext:kind
设置为to-one
(一对一)模式关联Left
或Right
;ext:joinLeftProp
表示在Ref
(关联的源端)中用于与关联目标端(Left
或Right
)建立关联的属性;ext:joinRightProp
表示在Ref
(关联的源端)中对应的ext:joinLeftProp
所指向的 关联目标端(Left
或Right
)的属性;ext:relation
用在Ref
(关联的源端)直接与关联目标端(Left
或Right
)建立关联的属性上, 其指向在Ref
中与映射到关联目标端对象的属性上,如,leftId -> left
、rightId -> right
;- 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,
lazy="true"
;
关联过滤查询
详细的说明请参考:
NopGraphQL 的 DataFetcher
机制会在获得主查询的结果后,
再逐条进行子查询,因此可能会出现明显的性能问题,在性能问题较明显时,需考虑在 @SqlLibMapper
中做自定义查询或者采用按需加载机制。
相关配置项如下:
graphql:queryMethod
: ;graphql:connectionProp
: ;graphql:authObjName
: ;graphql:maxFetchSize
: ;<graphql:orderBy />
: 始终附加的排序条件;<graphql:filter />
: 始终附加的过滤条件;
NopGraphQL 引擎提供 DataFetcher
机制,可以通过 OrmEntityPropConnectionFetcher
实现按需对关联对象进行过滤和排序,比如,按指定条件 filter
过滤出 NopAuthSite
的资源列表 resourcesList
:
query($filter: Map) {
NopAuthSite_get(id: "main") {
id
displayName
resourcesList(filter: $filter, limit: 10, offset: 0) {
total
items {
id
displayName
}
}
}
}
variables:
filter: {
"$type": "or",
"$body": [
{ "$type": "eq", "status", 1},
{ "$type": "eq", "status", 2}
]
}
则只需要在 NopAuthSite.xmeta
中为其对象属性 resourcesList
设置
graphql:queryMethod
,将其定义为关联查询属性:
<meta>
<props>
<prop name="id"/>
<prop name="resourcesList"
graphql:queryMethod="findPage"
lazy="true"
>
<schema bizObjName="NopAuthResource"/>
<graphql:filter>
<eq name="siteId" value="@prop-ref:id"/>
</graphql:filter>
<graphql:orderBy>
<field name="orderNo" desc="false"/>
</graphql:orderBy>
</prop>
</props>
</meta>
<schema bizObjName="NopAuthResource"/>
指示了关联对象(即,资源列表)的类型为NopAuthResource
;graphql:filter
则用于指定关联查询的过滤条件,@prop-ref:
前缀表示从业务对象上获取属性值, 本例表示,过滤出NopAuthResource#siteId
与业务对象上的属性id
的值相等的数据;graphql:orderBy
则指定了查询结果的排序条件,本例表示,按属性NopAuthResource#orderNo
升序排序;- 前端传入的
filter
和orderBy
参数不会覆盖对graphql:filter
与graphql:orderBy
的默认配置,而是会被组合在一起后,再进行过滤和排序;
属性 graphql:queryMethod
的可选值如下(具体实现参考 io.nop.graphql.orm.fetcher.OrmEntityPropConnectionFetcher#get
):
findCount
:返回long
类型数据,表示符合过滤条件的数据总量;findFirst
:返回关联对象类型数据,表示查询结果中的第一条对象数据;findList
:返回List
类型数据,表示查询结果中的全部对象数据;findPage
:返回PageBean
类型数据,表示指定分页的对象数据;findConnection
:返回GraphQLConnection
类型数据,表示指定分页的对象数据;
虽然关联查询的返回结果类型与指定的 graphql:queryMethod
相关,但其输入参数类型都是
GraphQLConnectionInput
,如,resourcesList(filter: $filter, limit: 10, offset: 0)
中的括号内的部分既是 GraphQLConnectionInput
的各项属性配置。
关联过滤查询并不需要业务对象和关联对象在 ORM 层面存在确切的关联关系,
即使二者没有直接关联关系,甚至可以不在同一数据库中,也能够进行关联过滤查询,只需要通过
graphql:filter
指定相应的关联过滤条件即可。
而若是二者在 ORM 层面定义了一对一(ext:kind="to-one"
)或一对多(ext:kind="to-many"
)的关联关系,
则可以设置属性 graphql:connectionProp
指向对应的关联属性,从而按二者的关联关系自动推导
graphql:filter
的配置,如:
<meta>
<props>
<prop name="resources"
ext:kind="to-many"
ext:joinLeftProp="id"
ext:joinRightProp="siteId"
lazy="true"
>
<schema bizObjName="NopAuthResource"/>
</prop>
<prop name="resourcesConnection"
graphql:queryMethod="findPage"
graphql:connectionProp="resources"
lazy="true"
>
<schema bizObjName="NopAuthResource"/>
<graphql:orderBy>
<field name="orderNo" desc="false"/>
</graphql:orderBy>
</prop>
</props>
</meta>
也就是,resourcesConnection
在查询时会根据 graphql:connectionProp
指向的 resources
属性的一对多关联自动推导得到过滤条件 NopAuthResource#siteId = ${id}
。
而若是在 resourcesConnection
中再配置 graphql:filter
,
则表示在已推导得到的过滤条件的基础上再补充额外的过滤条件。
此外,定义的关联查询属性(前例中的 resourcesList
或 resourcesConnection
)是可以复用的,利用
GraphQL 的别名机制,
可以实现用同一个关联查询属性返回不同的查询结果:
query ($filter1: Map, $filter2: Map) {
NopAuthSite_get(id: "main") {
id
displayName
activeResources: resourcesList(filter: $filter1, limit: 10, offset: 0) {
items {
id
displayName
}
}
inactiveResources: resourcesList(filter: $filter2, limit: 10, offset: 20) {
items {
id
displayName
}
}
}
}
XMeta 输入/输出转换
输入转换
相关处理逻辑见
ObjMetaBasedValidator#validateForSave
/ObjMetaBasedValidator#validateForUpdate
->ObjMetaBasedValidator#validateAndConvert
->ObjMetaBasedValidator#transformIn
。
为了适配组件规范或者方便用户输入等原因,客户端可能会将本身为列表类型的数据, 采用分隔符拼接为字符串后再提交给服务端,在这种情况下, 屏蔽客户端与服务端之间数据结构差异的最好方式就是对提交数据做输入转换, 从而确保在处理业务逻辑时无需关注客户端的变化。
在 XMeta 中可以通过 <transformIn />
配置相应的输入转换函数,
从而将客户端的输入数据转换为服务端需要的结构,例如,将以 ,
分隔的
types
字符串转换为字符串数组:
<meta>
<props>
<prop name="types">
<transformIn>
<c:script><![CDATA[
return value?.split(',');
]]]></c:script>
</transformIn>
</prop>
</props>
</meta>
该输入转换函数的可用参数如下(该函数的调用逻辑见 ObjMetaBasedValidator#transformIn
):
data
:Map
类型,其为客户端提交的全部输入数据;value
: 其为业务对象属性对应的待转换输入数据。可能为null
;transData
:Map
类型,其包含当前已处理的输入数据,其将被用于构造出业务对象;propMeta
:IObjPropMeta
类型,其为业务对象属性的结构;
在 <transformIn />
标签内可以编写任意 Xpl
标签,或者直接编写 XScript
脚本,仅需要确保最后一段的执行逻辑会返回转换后的值即可:
<meta>
<props>
<prop name="types">
<transformIn>
return value?.split(',');
</transformIn>
</prop>
</props>
</meta>
输出转换
相关处理逻辑见
ObjectDefinitionExtProcessor#provideFetchers
->EvalActionTransformFetcher#transform
。
输出转换可以视为输入转换的逆过程, 也就是,将服务端输出的数据转换为客户端所接受的数据格式:
<meta>
<props>
<prop name="types">
<transformOut>
<c:script><![CDATA[
import java.lang.String;
return value ? String.join(',', value) : null;
]]]></c:script>
</transformOut>
</prop>
</props>
</meta>
不过,与 <transformIn />
不同的是,<transformOut />
采用的是
NopGraphQL 的 DataFetcher
机制进行调用的,其转换逻辑由
EvalActionTransformFetcher
执行。
输出转换函数可访问的参数如下:
entity
: 其为业务对象自身;value
: 其为业务对象属性的值,也就是待转换的输出数据。可能为null
;
自动转换
得益于 Nop 内置的 x:post-extends
元编程机制,在 XMeta 中可以引入 Xpl 函数
meta-gen:DefaultMetaPostExtends
:
<meta>
<x:post-extends>
<meta-gen:DefaultMetaPostExtends
xpl:lib="/nop/core/xlib/meta-gen.xlib" />
</x:post-extends>
</meta>
该函数将会根据 <schema />
上设置的 domain
或 stdDomain,从
/nop/core/xlib/meta-prop.xlib
中查找名称为 domain-{domain}
或 domain-{stdDomain}
的 Xpl 函数,并自动将该函数生成的 XNode
合并到业务对象属性节点上(处理逻辑见 Xpl 函数 meta-gen:GenPropForDomain
)。
假设,将前面的例子中的属性 types
的 Schema Domain 设置为 comma-list
:
<meta>
<x:post-extends>
<meta-gen:DefaultMetaPostExtends
xpl:lib="/nop/core/xlib/meta-gen.xlib" />
</x:post-extends>
<props>
<prop name="types">
<schema domain="comma-list" />
</prop>
</props>
</meta>
然后,通过 Nop delta 机制,在 /nop/core/xlib/meta-prop.xlib
中添加函数 domain-comma-list
(即命名为 domain-{domain}
形式):
<lib xmlns:x="/nop/schema/xdsl.xdef"
x:schema="/nop/schema/xlib.xdef"
x:extends="super"
>
<tags>
<domain-comma-list outputMode="node">
<attr name="propNode"/>
<source>
<prop name="${propNode.getAttr('name')}">
<!-- type 为客户端所接受的类型 -->
<schema type="String"/>
<transformIn>
<c:script><![CDATA[
return value?.split(',');
]]]></c:script>
</transformIn>
<transformOut>
<c:script><![CDATA[s
import java.lang.String;
return value ? String.join(',', value) : null;
]]]></c:script>
</transformOut>
</prop>
</source>
</domain-comma-list>
</tags>
</lib>
最终,生成的 types
属性的结构如下:
<meta>
<props>
<prop name="types">
<schema type="String"/>
<transformIn>
<c:script><![CDATA[
return value?.split(',');
]]]></c:script>
</transformIn>
<transformOut>
<c:script><![CDATA[
import java.lang.String;
return value ? String.join(',', value) : null;
]]]></c:script>
</transformOut>
</prop>
</props>
</meta>
此外,对于配置了掩码模式 ui:maskPattern
的属性,也会由 Xpl 函数 meta-gen:GenMaskingExpr
自动构造并生成如下输出转换函数:
<meta>
<props>
<prop name="mobile" ui:maskPattern="3*4">
<transformOut>
return value?.toString()?.$maskPattern("3*4");
</transformOut>
</prop>
</props>
</meta>
XMeta 属性自动计算
属性自动计算在如下情况将被忽略:
- 客户端 已提交 业务对象属性的值;
- 客户端的提交 已被禁用(即,insertable
或 updatable 为
false
); - 业务对象属性 已配置 缺省值;
- 业务对象属性 已被设置 为虚拟字段;
<setter />
和 <transformIn />
只在客户端有提交业务对象属性的值时,才会被调用,因此,二者与
<autoExpr />
的调用是互斥的,不会同时被调用。
若在业务对象属性上配置了 biz:codeRule,
但未配置 <autoExpr />
时,则会自动构造该属性缺省值的计算函数:
<meta>
<props>
<prop name="code" biz:codeRule="D{@year}{@month}{@seq:5}">
<autoExpr when="save">
<c:script><![CDATA[
const codeRuleGenerator = inject('nopCodeRuleGenerator');
return codeRuleGenerator.generate(propMeta['biz:codeRule'], $scope);
]]></c:script>
</autoExpr>
</prop>
</props>
</meta>
若该函数返回
undefined
,则其结果将被忽略。
XMeta 属性访问控制
配置项 | 配置项类型 | 配置项名称 | 是否必填 |
---|---|---|---|
for | 权限类型名 | 是 | |
权限类型名的可选值如下:
| |||
roles | 拥有 for 权限的角色列表 | 否 | |
以 | |||
permissions | 拥有 for 权限的操作权限列表 | 否 | |
以 操作权限的格式为 | |||
publicAccess | boolean | 是否可公开访问? | 是 |
若该值为 |
对 XMeta 属性的访问由 ObjMetaBasedValidator#doCheckAuth
进行控制,
并且,roles 的设置优先于
permissions,
仅当 roles
未配置时,permissions
的配置才会起作用,
若二者均未配置,则视为无读写权限,将抛出无访问权限的异常。
permissions
的检查由 IActionAuthChecker
的实现类进行验证,
若是操作权限控制被禁用(即,配置 nop.auth.enable-action-auth
为 false
),
或者在应用中未提供 IActionAuthChecker
的实现,则同样视为无读写权限。
IActionAuthChecker
的实现实例需绑定到GraphQLEngine
的实例上。
此外,由于在 nop-file 模块中需要建立 IFileRecord
与 BizObject
属性的关联,
因此,在上传或下载文件时,在 NopFileStoreBizModel
中也会通过
IBizAuthChecker
检查关联模型属性的访问权限,其检查逻辑与前面描述的一致,
具体见 GraphQLActionAuthChecker#checkAuth
。
GraphQL 字段参数项定义
配置项 | 配置项类型 | 配置项名称 | 是否必填 |
---|---|---|---|
name | 参数名 | 是 | |
mandatory | boolean | 参数是否必填? | 是 |
指示当前参数是否为必填项。缺省为 | |||
displayName | string | 参数显示名称 | 否 |
当前参数的显示名称,方便人阅读 | |||
<description /> | string | 参数说明 | 否 |
对参数作用、使用等进行说明 | |||
<schema /> | ISchema | 参数 Schema | 否 |
对当前参数值类型、值精度等的约束定义。 详细说明见《基础 DSL: Schema》 |
NopGraphQL 引擎在通过 DataFetcher
获取关联数据时,会根据客户端指定的
GraphQL 输入参数
进行关联数据的动态查询。
为了确保输入参数的完整性和准确性,并支持自动的数据转换和数据校验, 因此,需要对 GraphQL 输入参数进行类型定义。
在 Nop 中,除了通过 <arg />
进行输入参数定义,还可以通过
<graphql:inputType />
以强类型方式进行定义。
而对于 graphql:queryMethod
指定的关联过滤查询则会使用
GraphQLConnectionInput
作为缺省的输入参数类型。
从 ObjMetaToGraphQLDefinition#toFieldDefinition
的实现中可以确定三种输入参数类型定义的优先级如下:
- 若设置了
<graphql:inputType />
,则<arg />
将被忽略; - 若配置了
<arg />
,则GraphQLConnectionInput
将不会被使用; - 若配置了
graphql:queryMethod
,但未配置<graphql:inputType />
和<arg />
,则使用GraphQLConnectionInput
为输入参数缺省类型;
实际开发中,可以在自定义的 <getter />
函数中获取到客户端回传的输入参数(详见 PropGetterFetcher
),并以此进行相应的数据加载处理。