跳到主要内容

XMeta 模型

提示
  • Nop 平台还处于开发阶段, 本文档中的实践方案可能会部分失效,但本人精力有限,无法及时跟进,请自行按照最新代码调整;
  • 您可以与智谱清言 AI 进行问答互动以了解 Nop 平台相关内容;
  • 若此文对您有很大帮助,请投币支持一下吧;
版权声明
危险

本文还在编辑整理中,会存在较多错误,请谨慎阅读!

XMeta 为 Nop 平台内置的一种领域模型,主要用于描述业务对象的结构。其为 NopGraphQL 引擎的核心模型 IBizObject 的重要组成部分。

XMeta 解析后的模型类型即为 IObjMeta

XMeta 模型的结构定义在 /nop/schema/xmeta.xdef 中,通过分析该 xdef 文件,便可以了解和掌握 XMeta 完整的配置和结构信息。

/nop/schema/xmeta.xdef/nop/schema/schema/obj-schema.xdef/nop/schema/schema/schema-node.xdefxdef:ref 引用xdef:ref 引用

在分析 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 形式的路径存放(一二级子目录名中不能包含短横线 -,且字母需为小写形式),例如:

_vfs/nop/demo/model/DepartmentEntity/DepartmentEntity.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>
配置项配置项类型配置项名称
displayNamestring业务对象的名称

用于设置业务对象的显示名称,比如,为业务对象 UserGroupEntity 设置显示名称为用户分组,即:

<meta displayName="用户分组">
<!-- ... -->
</meta>
biz:refsNeedToCheckWhenDelete

csv-set

biz:allowLeftJoinProps

csv-set

<displayProp />string显示数据名称的属性

用于显示业务对象数据名称的属性名,比如,在业务对象 RoleEntity 中的 name 属性的值便为角色数据的显示名称,则设置:

  <displayProp>name</displayProp>

在下拉列表、树形控件等需要显示数据名称的组件中需要引用 <displayProp /> 的设定值。

<description />string描述说明

用于阐述业务对象的作用、使用注意事项等,如:

  <description>用户分组:适用于对用户按能力、职能等进行分组的场景</description>
<entityName />

class-name

ORM 实体的名字

用于设置与业务对象映射的 ORM 实体的名字(一般与实体的类名相同), 比如:

  <entityName>io.nop.demo.entity.RoleEntity</entityName>

只有在创建了对应的 ORM 实体并启用 ORM 支持时才需要设置该值。

在集成了 NopORM 后,NopGraphQL 将为业务对象构造相应的 GraphQL 字段取值函数 OrmEntityColumnFetcherOrmEntityPropertyFetcher 等,用于从数据库中获取该 ORM 实体的指定列(Column)的值。详细逻辑见 io.nop.graphql.orm.OrmBizInitializer#initialize

注意OrmEntityColumnFetcher 等字段取值函数也将采用 GraphQL DataLoader 机制做 ORM 列的批量加载,因此,不会存在性能问题。

<primaryKey />

word-set

ORM 实体的主键列表

用于设置与 ORM 实体的主键相映射的属性名, 对于复合主键,则需要以逗号分隔多个属性名,例如:

  <!-- 单一主键 -->
<primaryKey>oid</primaryKey>

<!-- 复合主键 -->
<primaryKey>userId,groupId</primaryKey>

id 为 NopORM 的保留名字,一般要求不使用 id 作为主键属性的名字。但仍然可以通过名字 id 得到主键值(调用 IOrmEntity#get_id),如果是单一主键,则二者本质是相同的,而如果是复合主键,则通过 id 得到的是 OrmCompositePk 对象,其包含主键属性及其值,并且该对象的 toString 方法返回的是以 IOrmCompositePk#COMPOSITE_PK_SEPARATOR 作为分隔符将主键属性值拼接后的结果。

NopGraphQL 对于映射了 ORM 实体的业务对象,将会为名字为 id 的字段构造取值函数 OrmEntityIdFetcher 用于获取单一主键或复合主键的字符串结果。而若是在 GraphQL Document 的选择字段中未包含 id 字段,则会主动附加一个名为 id 的字段。该处理逻辑见 OrmFetcherBuilder#initFetchersOrmFetcherBuilder#buildFetcher

<keys><key /></keys>ObjKeyModelORM 实体的唯一键

用于设置具有唯一性约束并与 ORM 实体的列名相映射的属性名, 例如:

  <keys>
<key name="UK_name" props="name"/>
</keys>

在新增或修改 ORM 实体对象时,在 CrudBizModel 中将自动检查受唯一性约束的属性在数据库中是否存在重复值,若不唯一,则直接抛出异常。其检查逻辑见 CrudBizModel#checkUniqueForSaveCrudBizModel#checkUniqueForUpdate

<keys /> 列表的详细说明见《ORM 实体唯一键》

<filter />

filter-bean

ORM 实体的默认过滤条件

用于设置在查询该 ORM 实体时需始终附加的过滤条件。其附加逻辑见 CrudBizModel#prepareFindPageQuery

此外,在获取、删除、更新、复制某个确定的 ORM 实体对象时,在 CrudBizModel 中也会检查该实体对象是否满足该过滤条件,若不满足,则直接抛出异常。其检查逻辑见 CrudBizModel#checkMetaFilter

因此,其可用于限定用户只能查看和操作特定范围内的数据,例如:

  <filter>
<eq name="status" value="1" />
</filter>
<orderBy><field /></orderBy>OrderFieldBeanORM 实体的默认排序条件

用于设置在查询该 ORM 实体时需始终附加的排序条件,例如:

  <orderBy>
<field name="status" desc="true"/>
</orderBy>

其附加逻辑见 CrudBizModel#prepareFindPageQuery

<orderBy /> 列表的详细说明见《ORM 实体排序条件》

<tree />ObjTreeModelORM 实体的树形结构配置

用于配置 ORM 实体的树形结构信息,例如:

  <tree parentProp="parentId" childrenProp="children"/>

CrudBizModel 中将根据此结构查询和维护树形结构的 ORM 实体数据,及其父子关联关系(参考用例)。

<tree /> 的详细说明见《ORM 实体树形结构》

<selections><selection /></selections>ObjSelectionMetaGraphQL 选择字段集

对 GraphQL 的选择字段集命名, 用于指定对业务对象的哪些属性做字段取值, 例如:

  <selections>
<selection id="F_simpleFields">
oid, name
</selection>
</selections>

通过选择字段集的名字,可以以简单的方式引用常用的选择字段列表,不需要反复编写该列表。

<selections /> 的详细说明见《GraphQL 选择字段集》

<props><prop /></props>ObjPropMetaImpl业务对象的属性定义

用于描述业务对象的属性信息,例如:

  <props>
<prop name="oid" ...> ... </prop>
<prop name="name" ...> ... </prop>
<!-- ... -->
</props>

详细说明见《属性列表》

ORM 实体唯一键

在新增或修改 ORM 实体对象时,CrudBizModel 将自动检查 XMeta 中受唯一性约束的属性(组合属性一起检查)在数据库中是否存在重复值。 若存在重复,则直接抛出异常,从而避免向数据库写入不唯一的数据,保证其唯一性。

其检查逻辑详见 CrudBizModel#checkUniqueForSaveCrudBizModel#checkUniqueForUpdate

例如,配置 name 值唯一,code1code2 的组合结果唯一:

<meta>
<!-- ... -->

<keys>
<key name="UK_name" props="name"/>
<key name="UK_code" props="code1,code2"/>
</keys>

<!-- ... -->
</meta>

注意,<keys /> 本身没有配置属性,以下仅对其子节点 <key /> 的结构进行说明:

配置项配置项类型配置项名称是否必填
namestring唯一键名

用于配置唯一键的名字,其对应数据库层面 UNIQUE KEY 的名字,仅包含字母、数字和下划线。

在不同的 <key /> 标签之间的 name 需互不相同。

props

word-set

唯一键属性名列表

用于设置具有唯一性约束并与 ORM 实体的列名相映射的属性名。 多个属性之间采用逗号分隔

displayNamestring唯一键的名称

用于设置唯一键的显示名称。一般显示 name 会更容易区分

ORM 实体排序条件

在查询 ORM 实体时,CrudBizModel 将会始终向查询语句中附加在 XMeta 中配置的排序条件,从而保证查询结果始终是有序的。

其附加逻辑详见 CrudBizModel#prepareFindPageQuery

例如,配置按 status 降序排序,并按 name 升序排序:

<meta>
<!-- ... -->

<orderBy>
<field name="status" desc="true"/>
<field name="name" desc="false"/>
</orderBy>

<!-- ... -->
</meta>

注意,<orderBy /> 本身没有配置属性,以下仅对其子节点 <field /> 的结构进行说明:

配置项配置项类型配置项名称是否必填
namestring属性名

用于设置与 ORM 实体的列名相映射的、 参与排序的属性名

descboolean是否降序排序

用于设置排序属性值的排序方向。为 true 时,表示采用降序排序。 缺省为 false,即采用升序排序

nullsFirstbooleanNULL 值优先?

用于设置如何比较排序属性的 NULL 值:

  • true 时,表示 NULL 值小于所有非 NULL 值;
  • false 时,表示 NULL 值大于所有非 NULL 值;

ORM 实体树形结构

如果同一个 ORM 实体的对象之间存在父子或上下级关系,在逻辑上可构造成一棵或多棵树, 比如,父子部门,则可以在 XMeta 中配置 <tree /> 节点,以指示如何映射对象之间的父子关系。

对于采用复合主键(在 <primaryKey /> 中包含多个属性)的 ORM 实体,不适用于树形结构配置,在 CrudBizModel 中尝试为其构造树形查询时将会抛出异常。

例如,配置以属性 parentId 指示父节点,并以属性 children 指示子节点的 ORM 实体对象树:

<meta>
<!-- ... -->

<tree parentProp="parentId" childrenProp="children"/>

<!-- ... -->
</meta>
配置项配置项类型配置项名称是否必填
parentPropstring指示父节点的属性名

用于设置与 ORM 实体的列名相映射的、 用于指示父节点的属性名,如 parentId

注意:该属性名对应的应该是父节点的主键值,而不是父节点的对象实例,也就是,应该使用 parentId: String,而不是 parent: DepartmentEntity

childrenPropstring指示子节点的属性名

用于设置与 ORM 实体的一对多关联属性名相映射的、 指示子节点对象集合的属性名,如 children: List<DepartmentEntity>

如果设置了该值,则在通过 CrudBizModel#deleteCrudBizModel#deleteByQuery 删除 ORM 实体对象时,CrudBizModel 会在 #checkChildrenNotExistsWhenDelete(...) 函数中检查是否存在与其关联的子节点。若是存在关联的子节点对象, 并且在指示子节点的属性上未配置 tagSet 包含 cascade-delete,也即,未启用对关联对象的级联删除,则将抛出异常。

levelPropstring指示节点级别的属性名

用于设置与 ORM 实体的列名相映射的、 用于指示节点级别的属性名,如 level

其可与 rootLevelValue 配合使用, 用于设置根节点的过滤条件,也即,仅满足 o.{levelProp} = ${rootLevelValue} 的节点为树形结构的根节点。

注意:该属性名对应的属性类型必须为整型,如 level: Integer

rootLevelValuestring根节点所对应的节点级别属性的值

用于设置在表示 ORM 实体对象树的根节点时,levelProp 所指示的属性的值。例如:

  <tree ... levelProp="level" rootLevelValue="-1"/>

则表示,只有满足 o.level = -1 的对象才是对象树的根节点。

其必须与 levelProp 同时配置,否则,对二者的单一设置均不会对根节点的过滤起作用。

注意:为其配置的值必须能够被正常转换为 Integer 类型。

rootParentValuestring根节点所对应的父节点属性的值

用于设置在表示 ORM 实体对象树的根节点时,parentProp 所指示的属性的值。例如:

  <tree ... parentProp="parentId" rootParentValue="root"/>

则表示,只有满足 o.parentId = 'root' 的对象才是对象树的根节点。

若未设置其值,则缺省以 o.parentId is null 作为根节点的判断条件。

注意:以 levelProp + rootLevelValue 作为根节点过滤条件的优先级始终高于 rootParentValue,无论是否为其赋值。

sortPropstring指示用于节点排序的属性名

用于设置与 ORM 实体的列名相映射的、 用于指示节点排序的属性名,如 order

若设置了其值,则查询得到的节点将按其指定的属性名排序(未指定排序方向)。 而在其值未设定时,将缺省采用主键 <primaryKey /> 指示的属性进行排序(未指定排序方向)。

isLeafPropstring指示叶子节点的属性名

预留配置,暂时未使用!

CrudBizModel 中的以下 @BizQuery 查询函数与 XMeta 的 <tree /> 配置相关:

  • #findRoots: 根据 levelProp + rootLevelValuerootParentValue 的配置做根节点的过滤查询,返回根节点所对应的业务对象列表,如 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)
}
}

详细说明见《GraphQL 指令:@TreeChildren》

该方式会比树形数据查询函数更加灵活, 只不过该方式在查询深度较大且子节点数量较多时会存在比较明显的性能问题, 需要权衡利弊后再选择合适的方案。

虽然可以采用批量加载机制降低性能影响,但依然需要逐个层级依次加载,而不像执行 Recursive EQL 那样可以直接获得各个层级的子节点数据。

GraphQL 选择字段集

在做 GraphQL 服务调用时,需要为 GraphQL 对象类型的字段指定 GraphQL SelectionSet, 也即选择字段集, 用于对父字段的取值结果做字段选择

而针对业务对象的选择字段集可能出现在多个调用位置,并且存在多个相同的字段, 在此种情况下,便可以将这些相同字段组合起来并命名,再通过其名字进行引用, 从而实现对其的复用目的。

例如,定义一个 idF_moreFields 的选择字段集:

<meta>
<!-- ... -->

<selections>
<selection id="F_moreFields">
oid, name, relatedRoleList{ oid, name }
</selection>
</selections>

<!-- ... -->
</meta>

注意,<selections /> 本身没有配置属性,以下仅对其子节点 <selection /> 的结构进行说明:

配置项配置项类型配置项名称是否必填
idstring选择字段集的标识

用于设置选择字段集的唯一标识,在 GraphQL Document 中以该标识引用其字段集合, 可以视为选择字段集的引用名字

displayNamestring选择字段集的名称

用于设置选择字段集的显示名称

<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 有如下使用约定:

  • idcopyForNew: 特定用于 GraphQL 变更函数 CrudBizModel#copyForNew 中, 以指定在对 ORM 实体对象做复制新增时,默认可被复制的属性。 :为了安全性,可复制的内容是不允许由前端指定的
    • 若是未定义该标识的选择字段集,则 CrudBizModel#copyForNew 将复制源对象的全部属性
  • idF_ 为前缀:表示在 GraphQL Document 中可以被引用的选择字段集。其中,F_defaults 表示默认的选择字段集,若未定义该 id 的选择字段集,则将以 XMeta 中所有非 lazy 的属性作为选择字段;
  • id 为其余形式:此类选择字段集的用途可根据业务需求自行确定。比如,先通过 getObjMeta().getFieldSelection("my_selection") 获得 idmy_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

prop-path

属性名

用于设置业务对象属性的名字,如 DepartmentEntity 的属性 parent 将被定义为:

    <prop name="parent" .../>

通过属性名可以直接与 ORM 实体对象中的列属性关联属性别名属性组件属性等属性做同名映射, 也同样可以与 GraphQL 中的字段做同名映射。 也即,通过属性的名字便可获得其在 ORM 和 GraphQL 领域中的配置信息。

设置的属性名一般为 prop-name 形式,并且只有该形式的属性名才能做同名映射。

属性名也可以是以 . 分隔的复合形式,如 parent.name,其主要用于显式声明是否可对关联对象 parentname 属性做增改查或排序:

    <prop name="parent" .../>
<prop name="parent.name"
insertable="true" updatable="false"
queryable="true" sortable="true"
>
...
</prop>

为了安全性,NopGraphQL 默认是不允许对关联对象的属性直接做增改查等操作的。 只能为关联对象的相关属性定义相应的复合属性,如 parent.name,再在该复合属性上为其设置 queryableupdatable 等配置项,才能执行可被许可的操作。

反言之,若是没有对关联对象的属性做增改查的需求,则不需要定义复合形式的属性。

注意:复合属性不能作为 GraphQL 的字段。相关处理逻辑见 ObjMetaToGraphQLDefinition#toObjectDefinition

<schema />ISchema属性的类型模式

用于设置业务对象属性的数据类型、数据精度等信息,例如:

    <prop name="name" ...>
<schema type="java.lang.String"/>
</prop>

通过该配置项,可以自动进行与属性相关的数据转换和数据校验等方面的处理操作。 对其结构的详细说明见参考手册《基础 DSL:Schema》

注意:若未设置该配置项,则默认将业务对象属性视为 String 类型。 相关处理逻辑见 ObjMetaToGraphQLDefinition#toGraphQLType

displayNamestring属性的名称

用于设置业务对象属性的显示名称,例如:

    <prop name="parent" displayName="父部门"/>
<description />string属性的描述内容

用于设置对业务对象属性的描述内容,如:

    <prop name="parent" ...>
<description>父部门:当前部门的直接上级部门</description>
</prop>
defaultValue

any

属性的缺省值

[ORM Only] 用于设置业务对象属性的缺省值,如:

    <prop name="name" defaultValue="匿名" />

在调用 GraphQL 变更函数 CrudBizModel#save 对 ORM 实体对象做新增操作时, 若客户端没有为业务对象属性设置非空(不为 null 或空字符串)的值,并且该属性也未配置 <autoExpr />,则会以该配置项的值作为属性的缺省值复制到待新增的 ORM 实体对象上。

相关处理逻辑见 AutoExprRunner#runAutoExpr

propIdint属性的唯一编号

用于设置属性的唯一编号,例如:

    <prop name="name" propId="2" />

在与 gRPC 集成时,该编号对应于 Protocol Buffers 中的字段编号。 其映射逻辑见 ServiceSchemaManager#buildObjSchema

可通过应用配置项 nop.grpc.auto-init-prop-id 设置是否启用自动初始化属性编号,从而对未设置编号的属性添加递增的编号。缺省为 true

当业务对象属性与 ORM 实体的相映射时, 该属性编号与列的 propId 配置是一致的。NopORM 将会根据该编号对 ORM 实体对象的属性进行索引,用于属性赋值和取值等。

对 ORM 实体对象属性赋值的逻辑见 DynamicOrmEntity#orm_propValue(int, java.lang.Object)

属性编号的值必须大于 0,并且与 ORM 实体列相映射时,该值不能超过 OrmModelConstants#MAX_PROP_ID(固定为 2000),而与 gRPC 字段映射时,则没有为其值设定上限。

注意:在与 NopORM、gRPC 的列或字段映射以外的场景中,不需要设置该配置项。

mapToProp

prop-path

属性所映射的 ORM 实体对象属性名

[ORM Only] 用于为 ORM 实体对象的属性定义一个别名,例如:

    <prop name="parentName" mapToProp="parent.name" />

其表示为 ORM 实体对象的 parent.name 属性定义别名 parentName, 这样,在客户端便可使用 parentName 指代 parent.name

比如,通过别名 parentName 做过滤查询:

{
"query": "query ($query: QueryBeanInput) { ... }",
"variables": { "query": { "filter": {
"name": "parentName", "$type": "eq", "value": "人力资源部"
} } }
}

在过滤条件 filter 中的别名 parentName 会被函数 BizQueryHelper#transformMapToProp 替换为其所指代的 ORM 实体对象的属性 parent.name

还可以通过别名 parentName 对 ORM 实体对象的属性做新增和修改, 并通过别名做 GraphQL 字段选择,例如:

定义 GraphQL 变更文档
mutation ($data: Map) {
DepartmentEntity_save(data: $data) {
parentName
}
}
通过别名设置待变更属性的值
{
"query": "mutation ($data: Map) { ... }",
"variables": {
"data": {
"parentName": "信息技术部"
}
}
}

最终,为别名 parentName 设置的值将先被复制到 ORM 实体对象的 parent.name 属性上,再保存关联对象 parent。完成后,又可以通过 GraphQL 字段 parentName 得到保存后的 parent.name 的值。

在新增或修改 ORM 实体对象时,将调用 OrmEntityCopier#copyField 将别名的值复制到 parent.name 上。而在做 GraphQL 字段选择时,则会通过该别名字段的取值函数 OrmDependsPropFetcher 获取 parent.name 的值。注:别名字段的取值函数是在 OrmFetcherBuilder#initFetchers 中被绑定的。

注意:被映射的 ORM 实体对象属性并不要求在业务对象上也有相映射的属性, 例如上例中的 parent,只需确保在 ORM 实体对象上定义了该属性即可, 在业务对象上并不需要也定义 parent 属性。

depends

csv-set

属性所依赖的 ORM 实体对象属性

[ORM Only] 用于设置在对业务对象属性做 GraphQL 字段取值时,需要先获取 ORM 实体对象中的哪些关联属性的值。 也就是,配置对该属性的取值所要依赖的属性,例如:

    <!-- <prop name="userMappings" ...>
<schema>
<item bizObjName="NopAuthUserRole"/>
</schema>
</prop> -->
<prop name="relatedUserList"
depends="~userMappings" ...>
<schema>
<item bizObjName="NopAuthUser"/>
</schema>
<getter><![CDATA[
import io.nop.orm.support.OrmEntityHelper;

return OrmEntityHelper.getRefProps(
entity.getUserMappings(),
'user'
);
]]></getter>
</prop>

可以看到,属性 relatedUserList 依赖于 ORM 实体对象的属性 userMappings,因为,其对应的 GraphQL 字段取值函数为自定义的 <getter /> 函数,该函数需要通过 ORM 实体对象 entityuserMappings 属性的值才能得到该字段的值。

设置了 depends 的字段将绑定取值函数 OrmDependsPropFetcher。该绑定逻辑在 OrmFetcherBuilder#initFetchers 中实现。

该配置项的值是以 , 分隔的依赖属性名字符串,其中,依赖属性名采用 prop-path 形式,也就是,即可以为单一属性名,也可以为复合属性名。

所配置的依赖属性必须为 ORM 实体对象中的关联属性, 但其可以在当前的 XMeta 中有相应的映射属性,也可以没有。若未定义相应的 XMeta 属性,则需为该属性名添加前缀 ~,如 ~userMappings,用以表示其为内部属性(非强制性要求,有无前缀并不影响取值)。

对属性前缀的处理逻辑见 ObjPropMetaImpl#getDependOnProps

注意:若在 <getter /> 中引用的不是 ORM 实体对象的关联属性,或者引用的属性并未与 ORM 实体对象的属性相映射,则无需将其配置为依赖属性,直接通过 entity 引用即可。 因为,只有 ORM 实体对象的关联属性不是即时加载的,需要声明对其的依赖以便于提前批量加载其数据。

mandatoryboolean是否为必填属性

若设置其值为 true,则业务对象属性的值不能为空值(null 或空字符串)。缺省为 false

在对 ORM 实体对象做新增或修改时,必须为必填属性设置非空值, 否则,将抛出属性值为空的异常 nop.err.biz.mandatory-prop-is-empty

相关处理逻辑见 ObjMetaBasedValidator#_validate

internalboolean是否为内部属性

若设置其值为 true,则表示业务对象属性为内部属性。缺省为 false

在 Nop XView 中默认不会在生成的页面中为内部属性创建相应的组件。 但标记内部属性,并不影响其作为 GraphQL 的选择字段,也不影响对其所映射的 ORM 实体对象属性做新增或修改操作。

published 才会影响业务对象属性是否可作为 GraphQL 的选择字段。

deprecatedboolean是否为已废弃属性

若设置其值为 true,则表示业务对象属性已被废弃,已废弃的属性将不再被使用。缺省为 false

insertableboolean是否为可新增属性

若设置其值为 true,则表示在新增业务对象时,可以插入业务对象属性的值, 否则,其值将被忽略。缺省为 false

相关处理逻辑见 ObjMetaBasedValidator#validateForSave

注意:在调用变更函数 CrudBizModel#copyForNew 时,ORM 实体对象的可新增字段由 idcopyForNew<selection /> 指定,不受 insertable 的配置约束。

updatableboolean是否为可更新属性

若设置其值为 true,则表示在更新业务对象时,可以更新业务对象属性的值, 否则,其值将被忽略。缺省为 false

相关处理逻辑见 ObjMetaBasedValidator#validateForUpdate

virtualboolean是否为虚拟属性

若设置其值为 true,则在新增、修改 ORM 实体对象时,业务对象属性的值不会被复制到 ORM 实体对象上,其对于 ORM 实体对象而言是不可见的。缺省为 false

相关处理逻辑见 OrmEntityCopier#copyField

publishedboolean是否为开放属性

若设置为 false,则业务对象属性不能作为 GraphQL 选择字段出现在 GraphQL Document 中,否则将报字段未定义的异常 nop.err.graphql.undefined-field。缺省为 true

相关处理逻辑见 ObjMetaToGraphQLDefinition#toObjectDefinition

对于密码等敏感数据,需设置该配置项为 false,或者通过 ui:maskPattern 对其做脱敏处理。

exportableboolean是否为可导出属性

只有将其设置为 truepublished 也为 true,才可以导出当前属性的值。缺省为 true

相关处理逻辑见 BizExportHelper#isAllowExport

注意:该配置项目前还未真正使用。

sortableboolean是否为可排序属性

只有将其设置为 true,才能够在 GraphQL 服务调用时,在 $query 变量中指定根据业务对象属性做数据排序,否则,将抛出属性不可排序的异常 nop.err.biz.prop-not-sortable,例如:

声明属性可排序
    <prop name="status" sortable="true" />
指定属性的排序方向
{
"query": "query ($query: QueryBeanInput) { ... }",
"variables": { "query": {
"orderBy": [{
"name": "status", "desc" : false
}, { ... }, ...]
} }
}

其缺省值为 false

相关处理逻辑见 CrudBizModel#prepareFindPageQuery -> CrudBizModel#checkAllowQuery -> BizObjMetaHelper#checkPropSortable

queryableboolean是否为可查询属性

只有将其设置为 true,才能够在 GraphQL 服务调用时,在 $query 变量中使用业务对象属性构造过滤条件,否则,将抛出属性不可被用于查询条件的异常 nop.err.biz.prop-not-support-query,例如:

声明属性可查询
    <prop name="status" queryable="true" />
构造过滤条件
{
"query": "query ($query: QueryBeanInput) { ... }",
"variables": { "query": { "filter": {
"name": "status", "$type": "eq", "value": "2"
} } }
}

其缺省值为 false

相关处理逻辑见 ObjMetaBasedFilterValidator#validateVarFilter

该配置将与 allowFilterOp 配合使用。

注意:对于作为过滤器转换的业务对象属性, 则需要强制设置该配置项为 true

allowFilterOp

word-set

可在属性上应用的过滤运算符

用于设置可在业务对象属性上应用的过滤运算符,例如:

声明许可的过滤运算符
    <prop name="status"
queryable="true"
allowFilterOp="eq,gt,lt"
/>
应用许可范围内的过滤运算符
{
"query": "query ($query: QueryBeanInput) { ... }",
"variables": { "query": { "filter": {
"name": "status",
"$type": "gt",
"value": "2"
} } }
}

该配置项的值是以 , 分隔的运算符字符串,且运算符只能为 FilterBeanConstants 中以 FILTER_OP_ 开头的常量的值。若是对业务对象属性应用许可之外的运算符,则将抛出运算符不被支持的异常 nop.err.biz.prop-not-support-filter-op

缺省只支持 ObjMetaBasedFilterValidator#DEFAULT_ALLOW_FILTER_OP 列表中的运算符,即,该配置项的缺省值为 eq,in,dateBetween,dateTimeBetween

相关处理逻辑见 ObjMetaBasedFilterValidator#validateVarFilter

注意:只有 queryabletrue 时,该配置项才有意义。

lazyboolean是否为懒加载属性

用于标记业务对象属性是否为懒加载的。缺省为 false

若业务对象属性映射的是 ORM 实体对象的关联属性, 则该配置项应始终为 true

详细的配置逻辑可参考 CodeGen 模板 /nop/templates/meta/src/main/resources/_vfs/{moduleId}/model/{!entityModel.notGenCode}{entityModel.shortName}/{deltaDir}/_{entityModel.shortName}.xmeta.xgen

tagSet

tag-set

特性标记列表

[ORM Only] 该配置项的值与业务对象属性所映射的 ORM 实体对象的列属性关联属性别名属性组件属性上的 tagSet 一致

graphql:typestring属性的 GraphQL 类型

用于显式设置业务对象属性的 GraphQL 类型,NopGraphQL 将按此设置对业务对象属性所对应的 GraphQL 字段做类型转换,例如:

    <prop name="args"
mapToProp="funcMetaComponent.args"
graphql:type="[Map]"
/>
<prop name="funcMetaComponent"
tagSet="json" ext:kind="component"
>
<schema type="io.nop.orm.component.JsonOrmComponent"/>
</prop>

其中,funcMetaComponentORM JSON 组件, 这里以别名属性 args 映射该组件内部的数据 funcMetaComponent.args,这里假设其为 JSON 对象数组形式, 但 NopGraphQL 无法自动推导 JSON 组件内部数据的类型,因此,需要显式设置 graphql:type[Map],从而确保返回给客户端的 args 的字段值保持为 JSON 对象形式,而不是将其转换为字符串。

对 GraphQL 字段的取值结果做类型转换的逻辑见 GraphQLExecutor#normalizeValue

该配置项的可选值为枚举类 GraphQLScalarType 中的各个枚举项的名字(大小写敏感),如 StringMap 等。若 GraphQL 字段为列表类型,则需要将可选值包裹在 [ ] 内,如 [String](字符串列表)、[Map](Map 列表)。注意, 对于对象类型以外(不包括元素为对象类型的列表)的字段, 不能再对其做字段选择,否则将抛出异常 nop.err.graphql.not-obj-type

对于 GraphQLScalarType 的枚举项对应的 GraphQL 类型以外(包括列表类型)的字段将使用 IdentityTypeConverter 作为类型转换器,其直接返回数据本身, 所以,需确保字段取值结果与设定的类型是相同的。相关处理逻辑见 GraphQLFieldDefinition#getTypeConverter

默认通过 ` 来确定业务对象属性所对应的 GraphQL 字段的 GraphQL 类型,但对于 ORM JSON 组件的内部数据或其他未配置 <schema /> 的情况,则一般通过 graphql:type 指定其 GraphQL 类型。graphql:type 的优先级高于 <schema />,在二者均未配置的情况下,缺省将业务对象属性视为 String 类型。

通过 graphql:type 确定字段的 GraphQL 类型的逻辑见 ObjMetaToGraphQLDefinition#toGraphQLType -> GraphQLObjMetaHelper#getPropGraphQLType -> GraphQLDocumentParser#parseType -> GraphQLNamedType#setName -> GraphQLScalarType#fromText

此外,对于映射到类型为 Long主键属性,一般要求其对应的 GraphQL 字段返回 String 类型,因为 JS 无法处理超过一定大小数值的 Long 型数据,这时,便需要设置 graphql:type="String",从而忽略 <schema /> 设定的类型。

但在做 GraphQL 变更时, 客户端提供的输入数据则需由相应的业务操作函数进行转换处理, 如 CrudBizModel#doSave,NopGraphQL 不会主动根据业务对象属性的类型对输入数据做类型转换。

对 ORM 实体对象属性的输入数据的转换逻辑见 DynamicOrmEntity#orm_propValue(int, java.lang.Object)

注意:对于类型的转换,不是从任意类到任意类型均可支持的, 需要由转换的目标类型确定可被转换的源类型。对于 Nop 内置的标准类型可通过 StdDataType#getConverter 得到在 SysConverterRegistry 中注册的类型转换器,查看其对应的转换器便可确认可被转换的源类型。

graphql:mapperstring

预留配置,暂时未使用!

graphql:labelProp

prop-path

作为属性的显示文本的属性

[View Only] 当业务对象属性是对象类型(含对象列表)或数据字典时, 一般需要声明使用业务对象中的哪个属性的值作为其显示文本,例如:

    <prop name="relatedRoleList_ids"
graphql:labelProp="relatedRoleList_label"
>
<!-- ... -->
</prop>
<prop name="relatedRoleList_label">
<schema type="String"/>
<!-- ... -->
</prop>

其优先级高于 ext:joinRightDisplayProp

详细说明见《XMeta 属性显示文本》

注意:该配置项不是强制性的,其主要在 Nop XView 中用于指定从哪个属性中获取业务对象属性的显示文本信息。

graphql:dictNamestring属性所对应的数据字典

当业务对象属性的值对应的是数据字典的显示文本时, 需通过该配置项指定从哪个数据字典中获取该属性的值,例如:

    <prop name="status_label"
graphql:dictName="auth/user-status"
graphql:dictValueProp="status"
>
<schema type="String"/>
</prop>
<prop name="status">
<schema type="Integer" dict="auth/user-status"/>
</prop>

该配置项必须与 graphql:dictValueProp 配合使用才有效。后者用于指定字典值应该从业务对象中的哪个属性获取。

也就是,status_label 对应的字典显示文本,需要以 status 作为字典值,再从数据字典 auth/user-status 中获取该字典值对应的显示文本而得到。

该属性的 GraphQL 字段取值函数的构造逻辑见 DictLabelFetcherProvider#provideFetcher

详细说明见《XMeta 属性显示文本》

graphql:dictValueProp

prop-path

作为数据字典值的属性

该配置项必须与 graphql:dictName 配合使用才有效

graphql:joinLeftProps

word-set

预留配置,暂时未使用!

graphql:joinRightProps

word-set

预留配置,暂时未使用!

graphql:datePatternstring日期转换格式

用于设置对业务对象属性对应的 GraphQL 字段的取值结果做日期格式化,例如:

    <prop name="createTime" graphql:datePattern="ms"/>

若要支持对日期的格式化,需满足前提条件:应用配置项 nop.graphql.ignore-millis-in-timestamptrue(缺省值即为 true),并且业务对象属性的类型java.sql.Timestamp。在此条件下,若设置了该配置项,则按其指定的形式对 GraphQL 字段值做日期格式化,否则,缺省按 yyyy-MM-dd HH:mm:ss 进行格式化。

相应的 GraphQL 类型转换器的构造逻辑见 GraphQLFieldDefinition#getTypeConverter

该配置项支持的格式化形式如下:

  • ms: 得到日期的毫秒值字符串;
  • SimpleDateFormat 支持的形式:如 yyyy-MM-dd HH:mm:ss 等;
<graphql:transFilter />

xpl-fn: (filter, query, forEntity) => any

属性的过滤条件转换函数

若设置了该配置项,则在业务对象属性上构造的查询过滤条件, 将在做过滤查询前,被替换为该转换函数所构造的新的过滤条件,例如:

    <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>

对过滤条件的转换逻辑见 BizQueryHelper#transformFilter

其可以将复杂的过滤条件以属性的形式进行引用,从而简化对过滤条件的构造逻辑。

但是,此类属性只能用于构造过滤条件,而不能被用于 GraphQL 字段选择。 并且,对其的使用也必须符合 queryableallowFilterOp 的配置要求。

详细说明见《XMeta 过滤条件转换》

ui:maskPatternstring属性的掩码模式

用于设置对业务对象属性对应的 GraphQL 字段的取值结果做掩码处理,从而实现对敏感数据的脱敏,例如:

    <prop name="mobile" ui:maskPattern="4*"/>

该配置项指定的掩码由函数 StringHelper#maskPattern 解析,其必须为 3*43**4 形式,其中,数字代表字符串的开头或结尾所要保留的字符个数, 中间的字符则将被挨个替换为 *(替换前后的字符总数不变)。

只能针对字符串做掩码处理,对于非字符串类型,则将调用其 toString 方法得到其字符串后,再做掩码。

ui:maskPattern 的处理发生在解析 XMeta 的 x:post-extends 阶段, 其将由 Xpl 函数 meta-gen:GenMaskingExpr 为业务对象属性自动构造配置项 <transformOut />, 并在该配置项中指定相应的掩码转换逻辑。

meta-gen:GenMaskingExpr 函数定义在 /nop/core/xlib/meta-gen.xlib 中。

注意:若是要设置自定义的输出函数 <transformOut />,则不能再设置该配置项, 否则,自定义的逻辑将被覆盖。

biz:moduleIdstring属性对应的关联对象所属的模块

用于设置业务对象属性所关联的业务对象所属的 Nop 模块标识,例如:

    <prop name="product" biz:moduleId="demo/product">
<schema bizObjName="DemoProduct"/>
</prop>

在属性所关联的业务对象来自于外部模块时,需设置该配置项以标记其所属的模块。

注意:目前还未发现其实际用途,仅将其当作业务对象所属模块的识别标记即可。

biz:codeRulestring编码属性的生成规则

在业务对象属性是形式统一且具备唯一性的某种编码时,可以通过该配置项指定其生成规则, 可用于自动生成订单号、卡号等,例如:

    <prop name="orderNo"
biz:codeRule="D{@year}{@month}{@seq:5}"
insertable="false" updatable="false"
/>

该配置项指定的编码规则由接口 ICodeRuleGenerator 的实现进行解析并生成相应的编码值。

biz:codeRule 的处理发生在解析 XMeta 的 x:post-extends 阶段, 其将由 Xpl 函数 meta-gen:GenCodeRuleAutoExpr 为业务对象属性自动构造配置项 <autoExpr />,并在该配置项中指定相应的编码生成逻辑。 默认仅在新增 ORM 实体对象时才自动生成编码,更新操作不会重新生成,但是该编码依然可被客户端修改, 因此,还需要为业务对象属性设定 insertable="false"updatable="false", 以忽略客户端对其的修改。

meta-gen:GenCodeRuleAutoExpr 函数定义在 /nop/core/xlib/meta-gen.xlib 中。

在引入了 Nop 模块 nop-sys-dao 后,将由该模块提供 ICodeRuleGenerator 的缺省实现 SysCodeRuleGenerator,该编码器通过数据库记录各类编码信息,并确保生成编码的唯一性。 其编码生成规则详见文档《编码规则》

注意:若已设置了配置项 <autoExpr />,则对 biz:codeRule 的设置将被忽略。

ext:kindstring属性类别

用于设置业务对象属性映射的 ORM 实体对象属性所属的类别,例如:

    <prop name="groupMappings"
ext:kind="to-many">
<schema>
<item bizObjName="NopAuthGroupUser"/>
</schema>
</prop>
<prop name="propsConfigComponent"
ext:kind="component">
<schema type="io.nop.orm.component.JsonOrmComponent"/>
</prop>
<prop name="args"
ext:kind="alias"/>

该配置项可设置的值如下:

  • to-one: 表示 ORM 实体对象的一对一类别;
  • to-many: 表示 ORM 实体对象的一对多类别;
  • alias: 表示 ORM 实体对象的别名类别;
  • component: 表示 ORM 实体对象的组件类别;

通过对属性所属类别的标记,并配合 ext:joinLeftPropext:joinRightProp 等配置项,可以反向分析并自动维护对象之间的一对一、一对多的关联关系。

相关维护逻辑见 BizSchemaHelper#getCascadePropsCrudBizModel#requireManyToManyPropMeta

注意:对于与 ORM 文件组件相对应的业务对象属性,会通过 mapToProp 映射到文件组件的 fileStatusfileStatusList 属性上,因此,该业务对象属性本身并不属于 ORM 组件类别, 不能对其设置 ext:kind="component",例如:

    <prop name="logoComponentFileStatus"
mapToProp="logoComponent.fileStatus">
<schema type="io.nop.api.core.beans.file.FileStatusBean"/>
</prop>
<prop name="attachmentsComponentFileStatusList"
mapToProp="attachmentsComponent.fileStatusList">
<schema type="List&lt;io.nop.api.core.beans.file.FileStatusBean>"/>
</prop>
ext:relationstring关联映射到的属性

用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》

ext:joinLeftPropstring关联的源端对象的属性

用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》

ext:joinRightPropstring关联的目标端对象的属性

用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》

ext:joinRightDisplayPropstring关联的目标端对象的显示属性

用于设置业务对象间的一对一关联关系。详细说明见《XMeta 对象关联配置:一对一》

orm:manyToManyRefPropstring多对多关联的中间模型中对端的属性

用于设置业务对象间的多对多关联关系。详细说明见《XMeta 对象关联配置:多对多》

graphql:queryMethodGraphQLQueryMethod关联过滤查询所采用的方法名

用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》

相关处理逻辑见 OrmFetcherBuilder#getConnectionFetcher

graphql:connectionProp

prop-name

关联过滤查询所对应的关联属性

用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》

相关处理逻辑见 OrmFetcherBuilder#getConnectionFetcher

graphql:maxFetchSizeint关联过滤查询一次所能取得的最大数据量

用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》

相关处理逻辑见 OrmFetcherBuilder#buildConnectionFetcher

<graphql:orderBy />OrderFieldBean关联过滤查询的默认排序条件

用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》

相关处理逻辑见 OrmFetcherBuilder#buildConnectionFetcher

<graphql:filter />

filter-bean

关联过滤查询的默认过滤条件

用于设置对关联对象的过滤查询。详细说明见《XMeta 对象关联配置:关联过滤查询》

相关处理逻辑见 OrmFetcherBuilder#buildConnectionFetcher

注意:若在 XMeta 根节点上设置了配置项 biz:refsNeedToCheckWhenDelete, 并且业务对象属性在该列表内,则在删除业务对象时,CrudBizModel 会根据该配置项设定的过滤条件查询该属性所关联的业务对象,若存在关联结果,则删除将抛出异常 nop.err.biz.not-allow-delete-entity-when-ref-exists

相关处理逻辑见 CrudBizModel#doDelete -> CrudBizModel#checkEntityRefsNotExists -> CrudBizModel#findRefEntity

graphql:authObjNamestring关联过滤查询所应用的数据权限模型

用于设置在关联过滤查询时所应用的数据权限模型的名字, 用于控制关联对象的查询结果。缺省采用被过滤的业务对象的名字。

相关处理逻辑见 OrmFetcherBuilder#buildConnectionFetcher -> GraphQLObjMetaHelper#getPropAuthObjName

注意:该配置项仅在设置了 graphql:queryMethodgraphql:connectionProp 时才有效。

<graphql:inputType />

class-name

GraphQL 字段参数的类型

用于设置业务对象属性对应的 GraphQL 字段的参数类型, 从而便于对 GraphQL 字段参数进行数据校验和类型转换,例如:

    <prop name="customRoleList"
graphql:inputType="io.nop.api.core.beans.graphql.GraphQLConnectionInput"
queryable="true" insertable="false" updatable="false"
/>

可通过 @BizLoader 定义可接受参数的 GraphQL 字段。

对 GraphQL 字段参数的强类型定义,需满足以下条件:

  • 在类上标注 @GraphQLInput
  • 参数项均需提供 getter 方法,并在方法上标注 @PropMeta
  • 在注解 @PropMeta 上需按顺序递增设置 propId 的值;

该配置项与 <arg /> 的作用相同,只不过其是以强类型方式定义 GraphQL 字段参数的结构,比后者的灵活性差,但优先级高于后者。

对 GraphQL 字段参数的构造逻辑见 ObjMetaToGraphQLDefinition#toFieldDefinition -> ObjMetaToGraphQLDefinition#getArgsFromInputType

注意:在设置了 graphql:queryMethod 但未设置 <graphql:inputType /><arg /> 的情况下,NopGraphQL 缺省会使用 GraphQLConnectionInput 作为关联过滤查询字段的参数类型,不需要显式设置。

<arg />ObjPropArgModelGraphQL 字段的参数项

用于定义业务对象属性对应的 GraphQL 字段的参数项,例如:

    <prop name="customRoleList">
<arg name="limit">
<schema type="java.lang.Integer"/>
</arg>
<arg name="offset">
<schema type="java.lang.Integer"/>
</arg>
</prop>

该配置项与 <graphql:inputType /> 的作用相同,但后者的优先级更高。

详细说明见《GraphQL 字段参数项定义》

<transformOut />

xpl

属性的输出转换函数

用于定义对业务对象属性的输出转换函数,例如:

    <prop name="types">
<transformOut>
<c:script><![CDATA[
import java.lang.String;

return value ? String.join(',', value) : null;
]]]></c:script>
</transformOut>
</prop>

所配置的输出转换函数将被构造为业务对象属性对应的 GraphQL 字段的取值函数, 以便于向客户端返回其所接受的数据形式。比如上例中,将属性值 value 转换为以逗号分隔的字符串。

相关处理逻辑见 ObjectDefinitionExtProcessor#provideFetchers -> EvalActionTransformFetcher#transform

详细说明见《XMeta 输出转换》

注意ui:maskPattern 对属性值的掩码处理便是通过该配置项实现的。

<getter />

xpl

属性的 getter 函数

用于定义业务对象属性的 getter 函数,例如:

    <prop name="nameExt">
<getter>
<c:script><![CDATA[
return entity.getName() + '_Ext';
]]]></c:script>
</getter>
</prop>

若设置了该配置项,则其将作为业务对象属性对应的 GraphQL 字段的默认取值函数, 在该函数内可通过父字段的取值结果 entity 以及在该字段上指定的字段参数构造其字段值,例如:

客户端字段取值
query {
XxxEntity__get(id: '18273651') {
nameExt(suffix: '_Ext')
}
}
定义 getter 函数
    <prop name="nameExt">
<getter>
<c:script><![CDATA[
return entity.getName() + suffix;
]]]></c:script>
</getter>
</prop>

相关处理逻辑见 ObjMetaToGraphQLDefinition#toFieldDefinition -> PropGetterFetcher#get

该配置项与 <transformOut /> 的不同在于:前者主要用于「无中生有」以动态构造出属性,而后者主要是对已有属性的值做增强处理。

该 getter 函数的可用参数如下:

  • entity: 父字段的取值结果;
  • 其他:定义在 GraphQL 字段上的字段参数

注意:该配置项可以与 <transformOut /> 同时设定,并且该 getter 函数的执行结果将作为参数 value 传给 <transformOut /> 函数。

<transformIn />

xpl

属性输入转换函数

用于定义对业务对象属性的输入转换函数,例如:

    <prop name="types">
<transformIn>
<c:script><![CDATA[
return value?.split(',');
]]]></c:script>
</transformIn>
</prop>

通过输入转换函数,可以在新增(CrudBizModel#save)、更新(CrudBizModel#update)业务对象数据时, 将客户端提供的输入数据转换为服务端所接受的形式。比如上例中,将以逗号分隔的输入 value 转换为 String[] 类型。

相关处理逻辑见 ObjMetaBasedValidator#validateForSave / ObjMetaBasedValidator#validateForUpdate -> ObjMetaBasedValidator#validateAndConvert -> ObjMetaBasedValidator#transformIn

详细说明见《XMeta 输入转换》

注意:配置项 insertableupdatablefalse 时,相应地在新增或更新 ORM 实体对象时,该转换函数会被忽略,而配置项 virtual 不影响其调用,但其转换结果不会应用到 ORM 实体对象上。

<setter />

xpl

属性的 setter 函数

[ORM Only] 用于定义业务对象属性的 setter 函数,例如:

    <prop name="name">
<setter>
<c:script><![CDATA[
entity.setName(value + '_Ext');
]]]></c:script>
</setter>
</prop>

通过 setter 函数, 可以在新增(CrudBizModel#save)、更新(CrudBizModel#update)、复制新增(CrudBizModel#copyForNew)业务对象数据时, 向目标 ORM 实体对象赋予不同的值。

相关处理逻辑见 OrmEntityCopier#copyField

该配置项与 <transformIn /> 的不同在于:前者主要用于直接干预 ORM 实体对象的赋值逻辑,而后者主要是对客户端提供的输入数据做转换处理。

该 setter 函数的可用参数如下:

  • entity: 待新增或更新的 ORM 实体对象;
  • value: 客户端提交的输入数据。可能为 null,并且在设置了配置项 <transformIn /> 时,其值便为输入转换函数处理后的结果;
  • propMeta: IObjPropMeta 类型,其为当前业务对象属性的结构

注意:该配置项可以与 <transformIn /> 同时设定,并且 <transformIn /> 函数的执行结果将作为参数 value 传给该 setter 函数。

注意:在配置项 virtualtrue 时,该 setter 函数将被忽略,而配置项 insertableupdatable 不影响对该 setter 函数的调用,只是其参数 value 的值始终为 null

<autoExpr />ObjConditionExpr属性的缺省值计算函数

[ORM Only] 用于定义在新增、更新或复制新增时,业务对象属性映射的 ORM 实体对象属性的缺省值计算函数,例如:

    <prop name="code">
<autoExpr when="save">
<c:script><![CDATA[
const codeRuleGenerator = inject('nopCodeRuleGenerator');

return codeRuleGenerator.generate(propMeta['biz:codeRule'], $scope);
]]></c:script>
</autoExpr>
</prop>

以上表示根据当前属性的配置项 biz:codeRule 设置的规则生成编码。:该计算函数也是 biz:codeRule 的最终实现逻辑。

相关处理逻辑见 AutoExprRunner#runAutoExpr

该计算函数的可用参数如下:

  • propMeta: IObjPropMeta 类型,其为当前业务对象属性的结构

详细说明见《XMeta 属性自动计算》

注意:在配置项 virtualtrue 时,该计算函数将被忽略,而配置项 insertableupdatable 不影响对该函数的调用。

<auth />ObjPropAuthModel字段级别的访问控制

用于配置对业务对象属性的读写权限,从而实现对业务对象属性的访问权限控制。 详细说明见《XMeta 属性访问控制》

<graphql:selection />

field-selection

默认的 GraphQL 选择字段集

[View Only] 若业务对象属性映射的是一个对象,则可以配置该属性,用于指定默认的 GraphQL Field Selection:目前仅在 XView 中构造 GraphQL Field Selection 会时用到,具体处理逻辑见 XuiViewAnalyzer#addDispSelection

graphql:jsonComponentPropstring业务对象属性对应的 JSON 组件属性名

[View Only] 在业务对象属性为 JSON 文本时会自动构造一个 <schema />JsonOrmComponent 的对象属性,并赋值 graphql:jsonComponentProp 以指向该属性,从而建立起二者的关联。 :目前仅在 XView 中构造 GraphQL Field Selection 会时用到,具体处理逻辑见 XuiViewAnalyzer#addJsonComponent

XMeta 属性显示文本

在业务对象的属性对应的是数据字典,或者为关联对象的 ID 列表时, 可以在业务对象中定义一个作为其显示文本的属性,以便于在前端显示,再通过 graphql:labelProp 指向该显示文本属性,以建立二者之间的引用关系,比如:

NopAuthUser.xmeta
<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-statusvalue 值,其类型是 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
}
}

而对于一对多/多对多的对象关联场景中:

NopAuthUser.xmeta
<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&lt;java.lang.String&gt;"/>
</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对象属性即可,例如:

NopAuthSite.xmeta
<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

POST /r/NopAuthSite__findPage
{
"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 函数的返回值可以是 BooleanXNodenull 或者 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:outputModenode 的 Xpl 脚本输出一个 XNode 节点;

在前端仅需要构造一个如下的过滤器即可得到与前面例子相同的过滤条件:

POST /r/NopAuthSite__findPage
{
"query": {
"filter": {
"$type": "eq",
"name": "withOrderNoAndResourceStatus",
"orderNo": 100,
"resourceStatus": 1
}
}
}

注意,在该过滤器中不再设置 value 属性,而是分别指定了两个混合过滤器所需的参数 orderNoresourceStatus,并调用 TreeBean#getAttr 获得了过滤器的传入值。

可以发现,虽然上面的过滤器传入了两个附加参数,但依然采用的是 eq 运算符。 这是因为,在属性定义上,默认的 allowFilterOp(允许的过滤运算)仅有 eqin。 若是需要采用其他运算符,则需要显式设置 allowFilterOp,比如:

    <prop name="withOrderNoAndResourceStatus"
queryable="true"
allowFilterOp="with"
>
<graphql:transFilter>
<!-- ... -->
</graphql:transFilter>
</prop>

然后,构造过滤器为:

POST /r/NopAuthSite__findPage
{
"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:ifxpl:if 做分支处理,否则 graphql:transFilter 将返回最后一个 if 分支的结果,若该分支不满足判断条件,则实际将返回 null,而不是满足判断条件的分支结果。

如此,便可以按需使用 eqin 过滤器来进行过滤查询:

POST /r/NopAuthSite__findPage
{
"query": {
"filter": {
"$type": "eq", "name": "resourceStatus", "value": 1
}
}
}
// 或者
{
"query": {
"filter": {
"$type": "in", "name": "resourceStatus", "value": [1, 2, 3]
}
}
}

XMeta 对象关联配置

一对一

Leftid: intrightId: intright: RightRightid: intdisplayName: string11

根据以上图例所生成的 XMeta 为:

Left.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:kindext:joinLeftPropext:joinRightProp 等;
  • ext:kind 设置为 to-one(一对一)模式关联 Right
  • ext:joinLeftProp 表示在 Left(关联的源端)中用于与 Right(关联的目标端)建立关联的属性;
  • ext:joinRightProp 表示在 Left(关联的源端)中对应的 ext:joinLeftProp 所指向的 Right(关联的目标端)的属性;
  • ext:joinRightDisplayProp 表示关联目标端(Right)用于显示对象名称的属性(显示名),如,displayName
  • 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,lazy="true"

一对多

注意,一对多和一对一是互为反方向的关联配置,因此,二者是分别配置在关联的源端和目标端中的。

Leftid: intrightId: intdisplayName: stringRightid: intleftList: List<Left>0..*1

根据以上图例所生成的 XMeta 为:

Right.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 中是通过中间模型来建立多对多的关联,并通过中间模型将多对多分解为中间模型与关联双方的一对多关联:

详细的说明文件见文档《多对多关联》

Leftid: intrightMappings: List<Ref>RefleftIdrightIdRightid: intleftMappings: List<Ref>1111

根据以上图例所生成的 XMeta 分别为:

  • 配置 LeftRef 的一对多关联,也就是,通过 Ref 可以获取到关联上的多个 Right
Left.xmeta
<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&lt;java.lang.Integer&gt;"/>
</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(即,多对多的目标端模型)建立关联的属性;
  • relatedRightListrelatedRightIdList 为根据 orm:manyToManyRefPropLeft 模型上自动生成的属性,以便于直接获取多对多关联中的对端的对象和对象 id 列表;
  • ext:relation 参考一对一的说明;
  • graphql:labelProp 的配置说明见《XMeta 属性显示文本》
  • 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,lazy="true"
  • 配置 RightRef 的一对多关联,也就是,通过 Ref 可以获取到关联上的多个 Left
Right.xmeta
<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&lt;java.lang.Integer&gt;"/>
</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(即,多对多的目标端模型)建立关联的属性;
  • relatedLeftListrelatedLeftIdList 为根据 orm:manyToManyRefPropRight 模型上自动生成的属性,以便于直接获取多对多关联中的对端的对象和对象 id 列表;
  • ext:relation 参考一对一的说明;
  • graphql:labelProp 的配置说明见《XMeta 属性显示文本》
  • 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,lazy="true"
  • 配置 RefLeftRight 的一对一关联
Ref.xmeta
<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(一对一)模式关联 LeftRight
  • ext:joinLeftProp 表示在 Ref(关联的源端)中用于与关联目标端(LeftRight)建立关联的属性;
  • ext:joinRightProp 表示在 Ref(关联的源端)中对应的 ext:joinLeftProp 所指向的 关联目标端(LeftRight)的属性;
  • ext:relation 用在 Ref(关联的源端)直接与关联目标端(LeftRight)建立关联的属性上, 其指向在 Ref 中与映射到关联目标端对象的属性上,如,leftId -> leftrightId -> right
  • 非必要情况,对关联目标对象的加载方式默认均为懒加载,即,lazy="true"

关联过滤查询

详细的说明请参考:

提示

NopGraphQL 的 DataFetcher 机制会在获得主查询的结果后, 再逐条进行子查询,因此可能会出现明显的性能问题,在性能问题较明显时,需考虑在 @SqlLibMapper 中做自定义查询或者采用按需加载机制。

相关配置项如下:

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,将其定义为关联查询属性

NopAuthSite.xmeta
<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 升序排序;
  • 前端传入的 filterorderBy 参数不会覆盖对 graphql:filtergraphql: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 的配置,如:

NopAuthSite.xmeta
<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, 则表示在已推导得到的过滤条件的基础上再补充额外的过滤条件。

此外,定义的关联查询属性(前例中的 resourcesListresourcesConnection)是可以复用的,利用 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 /> 上设置的 domainstdDomain,从 /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} 形式):

/_vfs/_delta/default/nop/core/xlib/meta-prop.xlib
<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 属性自动计算

配置项配置项类型配置项名称是否必填
when

csv-set

执行计算的条件

其为调用 OrmEntityCopier#copyToEntity@BizAction 的名称列表,以 , 分隔。表示只有在执行该列表内名称的 BizModel Action 时才计算业务对象属性的缺省值。默认的可选值如下:

  • save: 保存时;
  • update: 更新时;
  • copyForNew: 复制新增时;
</>

xpl

计算函数

该函数的可用参数如下(详细执行逻辑见 AutoExprRunner#runAutoExpr):

  • entity: 其为业务对象自身;
  • data: 其为客户端提交的数据;
  • objMeta: IObjSchema 类型,其为业务对象的结构
  • propMeta: IObjPropMeta 类型,其为业务对象属性的结构

属性自动计算在如下情况将被忽略:

  • 客户端 已提交 业务对象属性的值;
  • 客户端的提交 已被禁用(即,insertableupdatablefalse);
  • 业务对象属性 已配置 缺省值
  • 业务对象属性 已被设置虚拟字段
提示

<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

xml-name

权限类型名

权限类型名的可选值如下:

  • read: 属性读权限;
  • write: 属性写权限;
  • all: 属性读和写权限;
roles

csv-set

拥有 for 权限的角色列表

, 分隔的角色列表。若操作者所拥有的角色存在于该列表内,则该操作者便拥有 for 对应的属性访问权限

permissions

multi-csv-set

拥有 for 权限的操作权限列表

,| 分隔的操作权限列表,, 之间为的关系,| 之间为的关系。 若操作者所拥有的操作权限满足该与/或关系(判断逻辑见 IActionAuthChecker#isPermissionSetSatisfied),则该操作者便拥有 for 对应的属性访问权限。

操作权限的格式为 {bizObjName}:{actionName},即,对应于 BizModel@BizAction 方法, 例如,NopAuthUser:queryNopAuthUser:mutationNopAuthUser:delete

publicAccessboolean是否可公开访问?

若该值为 true,则 rolespermissions 的设置均无效, 所有的操作者均可读写业务对象属性。缺省值为 false

对 XMeta 属性的访问由 ObjMetaBasedValidator#doCheckAuth 进行控制, 并且,roles 的设置优先于 permissions, 仅当 roles 未配置时,permissions 的配置才会起作用, 若二者均未配置,则视为无读写权限,将抛出无访问权限的异常。

permissions 的检查由 IActionAuthChecker 的实现类进行验证, 若是操作权限控制被禁用(即,配置 nop.auth.enable-action-authfalse), 或者在应用中未提供 IActionAuthChecker 的实现,则同样视为无读写权限。

IActionAuthChecker 的实现实例需绑定到 GraphQLEngine 的实例上。

此外,由于在 nop-file 模块中需要建立 IFileRecordBizObject 属性的关联, 因此,在上传或下载文件时,在 NopFileStoreBizModel 中也会通过 IBizAuthChecker 检查关联模型属性的访问权限,其检查逻辑与前面描述的一致, 具体见 GraphQLActionAuthChecker#checkAuth

GraphQL 字段参数项定义

配置项配置项类型配置项名称是否必填
name

var-name

参数名
mandatoryboolean参数是否必填?

指示当前参数是否为必填项。缺省为 false

displayNamestring参数显示名称
当前参数的显示名称,方便人阅读
<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),并以此进行相应的数据加载处理。