Strapi 如何通过 REST API 调整关联项顺序(connect position 与 disconnect)?
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
当你的内容类型里存在一对多、多对多这类"列表型"关联字段时,关联项的排列顺序本身是有意义的(比如导航栏、轮播位、排序列表)。Strapi 的数据库层对这类关联提供了可排序能力:你可以把某一项移动到指定位置之前、之后、列表开头或结尾,也可以在同一次请求里移除不需要的项。文档说明这一重排(reordering)功能同时可用在 Content Manager 和 API 中(见 reordering.md)。本文只走 REST(Content API)这条路:说明请求体怎么写、哪些关联支持排序、顺序如何存储、结果怎么验证,以及测试中覆盖到的报错边界。
适用前提(均来自仓库文档与 API 测试):
- 关联字段必须是 many 型关系(oneToMany / manyToMany / 单向的 has-many)。one-to-one 关系每对只有一条关联,没有顺序可言;polymorphic(多态)关系明确不存储 order 值(文档原话:too complicated to implement)。
- 请求走 Content API:
/api前缀下的资源路由,并携带Authorization: Bearer <token>头。仓库 API 测试框架就是按这个方式访问的(CONTENT_API_URL_PREFIX = '/api',见 request.js)。
顺序在数据库里怎么存
先理解存储方式,才能理解position参数为什么是"相对位置"而不是序号:
- 关联的顺序值存在
order字段中;双向关系还会把另一侧的顺序存在inverse_order字段中(如 m2m 的category_order/address_order成对出现)。 - 单向关系只有一侧有 order 字段(如 restaurant has many categories,只有
category_order,没有restaurant_order)。 - 排序算法采用 fractional indexing(分数索引),用小数作为顺序值。文档给出了一组示例:同一顺序值发生碰撞时会被重算去重,例如
[ {id: 5, order: 1.5}, {id: 3, order: 1.5} ]→[ {id: 5, order: 1.33}, {id: 3, order: 1.66} ](文档示例,非固定预期值)。
算法要点(摘自 reordering.md 的 "Algorithm steps"):
connect数组被顺序处理,每一项按before/after引用的现有关联计算新的 order 值;放到开头时 order 记为 0.5。- 如果
before/after引用的 id不在当前关联列表中,直接抛错。 - 同一个 id 在数组里重复出现时,先前的那条会被移除(即"重新 connect 已存在的关联"等价于把它移动到新位置)。
disconnect数组处理的是删除:从数据库删掉这些关联,然后基于剩余项的位置(ROW_NUMBER())重排顺序。
顺序计算的具体实现在 relations-orderer.ts,其类型定义为position?: { before?: ID; after?: ID; start?: true; end?: true }。
请求怎么写:connect + position 与 disconnect
更新走标准的内容 API 路由:PUT /api/<pluralName>/<documentId>(仓库测试 relations.test.api.ts 中的updateEntry即以PUT /shops/${documentId}方式调用)。请求体中,关联字段不再直接传 id 数组,而是分成connect与disconnect两个数组——这与 Content Manager 文档对 API 契约的描述一致(04-relations.mdx:The API to update the entity expects relations to be categorised into two groups, aconnectarray anddisconnectarray)。
字段项支持两种 id 写法(测试中两套模式都跑):
{ "id": 3 }:数据库自增 id;{ "documentId": "67a9f5f614f101002287e7d5" }:文档 id 字符串。
注意:纯字符串数组(如["docId1", "docId2"])不能携带 position——测试文件中的注释明确写着 "reordering only works with objects"。要重排,就必须用对象形式。
基础示例:把一项移动到另一项之前
假设 shop 的products_mw字段当前按[A, B, C]有序(id 分别对应docid1、docid2、docid3,为仓库测试 fixture 用的值,实际使用请替换为你自己内容类型的字段名和文档 id)。把 A 移动到 C 之前:
curl -X PUT "<strapi-host>/api/shops/<shopDocumentId>?populate=products_mw" \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "data": { "products_mw": { "connect": [ { "documentId": "<docid-A>", "position": { "before": "<docid-C>" } } ] } } }'其中<strapi-host>是你部署 Strapi 的地址,<token>是访问内容 API 的 Bearer token,<shopDocumentId>是被更新实体的 documentId,<docid-A>/<docid-C>是关联项的 documentId。<shopDocumentId>与关联项 id 都可以从带?populate=products_mw的查询响应中取得(见下文验证部分)。
四种位置参数的语义(均出自 reordering.md 的负载示例与算法说明):
| position 写法 | 含义 |
|---|---|
{ "before": "<id>" } | 放到 id 指定的关联之前 |
{ "after": "<id>" } | 放到 id 指定的关联之后 |
{ "start": true } | 放到列表开头(order 记为 0.5) |
{ "end": true } | 放到列表结尾 |
移动"已经在列表里"的项,就是重新 connect 它并给出新位置,不需要先 disconnect。
一次移动多项
connect数组可以包含多个项,按数组顺序依次计算。仓库 API 测试里的一个用例:初始[docid1, docid2, docid3],请求
{ "data": { "products_mw": { "connect": [ { "documentId": "<docid-1>", "position": { "end": true } }, { "documentId": "<docid-3>", "position": { "start": true } }, { "documentId": "<docid-2>", "position": { "after": "<docid-1>" } } ] } } }测试断言最终顺序为[docid3, docid1, docid2](见 content-manager 侧测试 "Reorder multiple relations")。可见数组内元素的先后引用关系会被框架自动整理:当某项的位置依赖数组里另一项的新位置时,relations-orderer.ts 的sortConnectArray会先把 connect 数组重排成可计算的顺序(其头注释举的例子:{ id: 5, position: { before: 1 } }必须先等id 1定位完成),你不必手工排好依赖顺序。
同一次请求里 connect 和 disconnect 一起用
两个数组可以放在同一个字段对象里。测试 "Reorder a relation before one that is disconnected in the same call" 的场景:初始[id1, id2, id3],请求
{ "data": { "products_mw": { "connect": [ { "id": 2, "position": { "before": 1 } } ], "disconnect": [ { "id": 1 } ] } } }即把 id2 移到 id1 之前的同时删掉 id1。测试断言最终为[id2, id3],且不会因 id1 既是 disconnect 目标又是位置参照而崩溃。
另有一个值得知道的边界(测试 "Does not delete a relation that is reconnected with position metadata"):如果同一请求里某项既出现在disconnect又带 position 出现在connect,它不会被删除,而是被移到 position 指定的位置(该用例断言最终顺序为[id1, id3, id2])。
验证结果
仓库 API 测试的验证方式可以直接照搬到手工操作:更新请求带上?populate=<字段名>,响应data中的关联数组就是数据库里存储的顺序,测试正是对这些数组的顺序做断言。例如 "Reorder single relation" 用例:初始[id1, id2, id3],请求connect: [{ id: id1, position: { before: id3 } }]后,测试断言顺序变为[id2, id1, id3](测试代码)。新建实体同理:POST /api/shops时带上?populate=products_mw,响应里即可看到按 position 计算出的顺序(如两个{ "start": true }项会形成后进在前的[docid2, docid1],两个{ "end": true }项则保持[docid1, docid2],均为测试断言值,可作示例参考而非固定预期)。
报错与边界
以下现象均能在仓库源码或测试中找到依据,遇到时按对应条件排查:
before/after引用了不在当前关联列表中的 id:默认情况下请求失败,测试断言返回 400("Rejects strict reorder before a non-related id...")。如果确实要容忍非法引用,在该字段对象上传options: { strict: false },同一请求即可成功返回(测试 "Invalid reorder with non-strict mode should not give an error")。注意文档对strict的默认取值没有展开说明,这里只按测试行为描述。- 同一个 id 在一次 connect 里重复连接:抛
InvalidRelationError,消息为The relation with id <id> is already connected. You cannot connect the same relation twice.(relations-orderer.ts);测试 "Update relations using the same id multiple times" 断言该场景返回 400ValidationError。 - connect 数组内出现循环引用(A 在 B 之前、B 又在 A 之前):抛
InvalidRelationError,消息为A circular reference was found in the connect array. ...。 - one-to-one 与 polymorphic 关系:不存储 order 值,position 排序对它们不适用(reordering.md)。polymorphic 测试中出现的
position: 'end'/__type组合属于多态关联的 connect 用法,与本文的顺序调整不是一回事。 - 纯 id 数组不能重排:
["<docId1>", "<docId2>"]这种写法只用于普通的 connect/disconnect,要重排必须升级为带position的对象。
更多细节可继续读:重排算法图解(reordering-algo-2.png 见原文档)、connect 数组排序逻辑的单元测试 sort-connect-array.test.ts,以及 Content Manager 前端如何 diff 出 connect/disconnect 的说明(04-relations.mdx "Cleaning data to be posted to the API" 一节)。
【免费下载链接】strapi🚀 Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.项目地址: https://gitcode.com/GitHub_Trending/st/strapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考