Sails Add Blueprint 深入解析:用 PUT 请求为集合关联添加成员记录

发布时间:2026/9/20 20:37:15
Sails Add Blueprint 深入解析:用 PUT 请求为集合关联添加成员记录 Sails Add Blueprint 深入解析用 PUT 请求为集合关联添加成员记录【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails本文以 SailsRealtime MVC Framework for Node.js内置的Add添加到集合blueprint为核心讲解PUT /:model/:id/:association/:fk这条 RESTful 路由如何把一条子记录挂到另一条父记录的集合属性上涵盖参数语义、边界行为、底层源码实现、Socket 实时通知以及与其他 blueprintpopulate / remove / replace / update的取舍。读完本文你将能够熟练使用 Add blueprint 组装一对多与多对多关联理解其 404 与幂等性细节并能基于本仓库源码lib/hooks/blueprints/自行定位问题或扩展行为。Add blueprint 是什么给集合属性追加成员在 Sails 的 ORMWaterline中模型属性除了string、number等字面类型还可以表示指向其他记录的关联association分为两类单数model关联指向一条记录例如Purchase模型上的cashier: { model: Employee }复数collection关联指向一组记录例如Employee模型上的involvedInPurchases: { collection: Purchase, via: cashier }。Add blueprint 专门用于处理复数collection关联——它把一条外部子记录的引用追加到当前主父记录的某个集合属性上。官方用法如下见 Add.mdPUT /:model/:id/:association/:fk例如PUT /employee/7/involvedInPurchases/47的含义是把采购单 #47 加入员工 Dolly#7的involvedInPurchases集合。如果你的目标是设置或清空单数model属性请改用 update blueprint把外键设置为新记录的 id或null以解除关联如果想要整体替换集合中的成员则应使用 replace blueprint。Add 在 blueprint 家族中的位置从本仓库源码看Add 是 Sails 为每个模型自动注册的九个核心 blueprint 动作之一。在 lib/hooks/blueprints/index.js 中blueprint 动作被集中声明var BlueprintController { create: require(./actions/create), find: require(./actions/find), findone: require(./actions/findOne), update: require(./actions/update), destroy: require(./actions/destroy), populate: require(./actions/populate), add: require(./actions/add), remove: require(./actions/remove), replace: require(./actions/replace), };随后在registerActions中每个模型都会获得一份独立的动作注册modelIdentity /add等这样不同模型可以挂载各自不同的 policy 中间件。也就是说/employee/add、/purchase/add在底层是同一份动作函数但通过路由选项区分作用于哪个模型。路由形态与参数语义Add blueprint 对应的路由有两种形态详见 Blueprint Routes.md路由类型路由模板示例RESTfulrest blueprints默认开启PUT /:model/:id/:association/:fkPUT /employee/7/involvedInPurchases/47Shortcut开发模式专用GET /:model/:id/:association/add/:fkGET /employee/7/involvedInPurchases/add/47在 lib/hooks/blueprints/index.js 中可以看到 RESTful 关联路由的绑定逻辑——对每个模型的每个collection类型关联绑定三条关联路由_.each(_.where(Model.associations, {type: collection}), function (association) { var alias association.alias; _bindAssocRoute(put %s/:parentid/%s/:childid, add, alias); // PUT - add _bindAssocRoute(put %s/:parentid/%s, replace, alias); // PUT - replace _bindAssocRoute(delete %s/:parentid/%s/:childid, remove, alias); // DELETE- remove });而 shortcut 形态默认shortcuts: true仅开发环境建议开启绑定的是get %s/:parentid/%s/add/:childid见同一文件第 319-324 行。所有关联路由在绑定时都会把模型的associations做一次_.cloneDeep后放入路由选项避免 blueprint 动作意外改动模型定义。四个核心参数参数类型说明modelstring父记录所属模型的 identity例如employee对应/employee/7/involvedInPurchases/47中的employeeidstring父记录的主键值例如7associationstring集合属性的名字例如involvedInPurchasesfkstring要加入集合的子记录主键值通常是 id例如47在参数解析层parse-blueprint-options.jsadd分支把路由段映射为三个标准化的 blueprint 选项case add: if (!req.options.alias) { throw new Error(Missing required route option, req.options.alias.); } queryOptions.alias req.options.alias; queryOptions.targetRecordId req.param(parentid); queryOptions.associatedIds [req.param(childid)]; break;注意这里associatedIds被包装成单元素数组Add 每次只关联一条记录而 replace 接收的是数组。alias则来自路由绑定时的req.options.alias缺少时会直接抛出错误。行为规则404、幂等性与反向更新根据 Add.md 与 add.js 的实现Add blueprint 遵循以下行为父记录不存在如果:id对应的父记录在数据库中不存在响应res.notFound()404。源码中Model.findOne(parentPk)返回空值时即return res.notFound();add.js 第 52-56 行。子记录不存在如果:fk对应的子记录不存在同样响应res.notFound()。源码通过ChildModel.findOne(childPk)校验add.js 第 59-63 行。已关联时的幂等语义如果父记录已经关联了该子记录Add 不会修改任何记录。但有一个已知限制——对多对多关联而言当前版本仍然会写入重复的 junction联结记录官方建议在数据库层添加多列索引来规避Sails 团队正在为 MongoDB、sails-disk 等 NoSQL 数据库寻求更友好的默认方案。2-way 关联的反向更新如果该关联是双向的即带有via那么子记录上通过via指向父记录的属性或集合也会被同步更新。例如下面的示例中Purchase.cashier这个外键会被自动设置为7。源码对子记录已存在这一幂等场景的处理值得注意add.js 第 68-86 行调用 Waterline 的Model.addToCollection(parentPk, relation, childPk)后如果返回的是AdapterError且err.code E_UNIQUE唯一约束冲突即子记录已在集合中则不视为错误继续走发布与响应流程最终仍返回 200只有UsageError会转为 400其余异常统一返回 500Model.addToCollection(parentPk, relation, childPk).exec( function(err) { if (err) { switch (err.name) { case UsageError: return res.badRequest(formatUsageError(err, req)); case AdapterError: switch (err.code) { // 子记录已经是集合成员时视为正常继续发布与响应 case E_UNIQUE: break; default: return res.serverError(err); } break; default: return res.serverError(err); } } // ... 发布 Socket 通知然后重新查询父记录并 populate 集合后返回 });最后一步blueprint 会重新查询父记录带默认的 populates默认DEFAULT_POPULATE_LIMIT 30见 parse-blueprint-options.js 第 47-48 行把集合填充好之后以res.ok(matchingRecord)返回——这就是为什么响应中会包含最新的集合内容。完整示例Dolly 与她的采购记录假设项目包含以下两个模型官方示例的配套定义见 Add.md 的 Notes// api/models/Employee.js module.exports { attributes: { name: { type: string }, involvedInPurchases: { collection: purchase, via: cashier } } }; // api/models/Purchase.js module.exports { attributes: { amount: { type: number }, cashier: { model: employee } } };把采购单 #47 加入员工 Dolly#7的采购列表PUT /employee/7/involvedInPurchases/47返回父记录 Dolly此时她的involvedInPurchases中已经包含采购单 #47{ id: 7, name: Dolly, createdAt: 1485462079725, updatedAt: 1485476060873, involvedInPurchases: [ { amount: 10000, createdAt: 1485476060873, updatedAt: 1485476060873, id: 47, cashier: 7 } ] }注意响应中的cashier: 7——这正是2-way 关联反向更新的体现由于involvedInPurchases带有via: cashier子记录Purchase.cashier也被自动设置为了7。各种客户端的调用方式jQuery$.put(/employee/7/involvedInPurchases/47, function (purchases) { console.log(purchases); });Angular$http.put(/employee/7/involvedInPurchases/47) .then(function (purchases) { console.log(purchases); });sails.io.jsSocket 客户端io.socket.put(/employee/7/involvedInPurchases/47, function (purchases) { console.log(purchases); });cURLcurl http://localhost:1337/employee/7/involvedInPurchases/47 -X PUT快速搭建演示项目官方 Notes 提供了从零开始搭建上述示例的步骤需要restblueprints 开启默认即开启$ sails new foo $ cd foo $ sails generate model purchase $ sails generate model employee然后编辑api/models/Purchase.js与api/models/Employee.js加入上面的关联定义即可。blueprint 默认配置rest: true、shortcuts: true、actions: false、autoWatch: true等在 lib/hooks/blueprints/index.js 的defaults中可以看到如果你生成的模板是 Web apprest可能默认关闭需要到 sails.config.blueprints 相关配置中显式开启。Socket 通知Add 触发的实时消息如果应用开启了 WebSocketssockets与ormhook 启用时 pubsub hook 才生效见 lib/hooks/pubsub/index.jsAdd blueprint 会向订阅者广播三类消息。1. 父记录订阅者收到addedTo所有订阅了父记录的客户端除发起请求的客户端外都会收到一条消息事件名是父模型的 identity例如employee消息格式为id: 父记录主键值, verb: addedTo, attribute: 父记录集合属性名, addedIds: 本次新增的子记录主键值承接上面的例子所有订阅了 Dolly#7的客户端会收到{ id: 7, verb: addedTo, attribute: involvedInPurchases, addedIds: [ 47 ] }底层实现在 pubsub hook 的_publishAdd中lib/hooks/pubsub/index.js它会构造{ id, verb: addedTo, attribute, addedId }事件并通过sails.sockets.broadcast广播到由_room(id)计算出的实例房间房间名形如sails_model_employee_7:employee。在 add.js 中如果请求来自 socket还会先执行Model.subscribe(req, [parentPk])把发起请求的 socket 也订阅到父记录上。2. 子记录订阅者收到updated或addedTo如果关联带有via订阅了子记录 #47 的客户端还会收到第二条通知具体类型取决于via指向的属性若via指向的关联也是复数多对多例如cashiers子记录订阅者会再收到一条addedTo通知若via指向的是单数属性一对多例如cashier子记录订阅者会收到updated通知。对应源码中_publishAdd的noReverse分支pubsub/index.js 第 802-830 行它会查找反向关联如果是collection类型则调用reverseModel._publishAdd(...)否则调用reverseModel._publishUpdate(...)把反向外键设置为父记录 id。_publishUpdate构造的消息同样包含verb: updated、id与previous字段第 598-599 行。3. 被抢走记录的通知removedFrom如果这次 Add 操作使采购单 #47 从另一位员工的involvedInPurchases集合中被抢走那么订阅了那位被抢员工的客户端例如 Motoki#12会收到removedFrom通知详见 Remove blueprint 的 socket 通知一节。源码逻辑在 add.js 第 98-100 行当反向关联是单数ChildModel.attributes[associationAttr.via].model且子记录原来的外键值不为null时调用Model._publishRemove(childRecord[associationAttr.via], relation, childPk, ...)向前父记录的订阅者广播移除事件。关于订阅/发布机制的进一步说明可参考 resourceful-pubsub关于verb语义与addedTo/removedFrom事件在客户端如何消费可查看集成测试 hook.pubsub.modelEvents.subscribers.test.js 与 hook.blueprints.shortcut.routes.test.js后者覆盖了 shortcut 形态下/add/路由的绑定与响应。与 remove / replace / update 的配合与取舍场景推荐 blueprint说明向集合追加一个成员addPUT /:model/:id/:association/:fk本文主题从集合移除一个成员removeDELETE /:model/:id/:association/:fk见 Remove.md底层调用Model.removeFromCollectionremove.js整体替换集合成员replacePUT /:model/:id/:association请求体提供 id 数组设置/清空单数关联外键updatePATCH /:model/:id外键设为新 id 或null读取集合成员populateGET /:model/:id/:association见 Populate.md从源码对比看add 与 remove 的实现高度对称都先校验父/子记录存在再调用 Waterline 的集合操作方法最后重新查询并 populate 返回区别仅在于操作方向与 socket 事件addedTovsremovedFrom。注意事项与最佳实践多对多重复 junction 记录如前所述当前版本在子记录已属于集合时仍可能写入重复的联结记录。如果使用关系型数据库且对数据整洁度有要求建议在数据库层为 junction 表添加多列唯一索引NoSQLMongoDB、sails-disk用户需等待更友好的默认方案。单数 vs 复数Add 只处理复数collection属性单数model属性的赋值/清空请走 update blueprint。权限与 CSRFPUT属于写操作若开启 CSRF 防护需要携带或禁用 CSRF token否则会得到 403生产环境务必用 policies 保护这些自动暴露的 blueprint 路由Web app 模板默认关闭rest即为安全考虑。覆盖 blueprint 动作如果默认的 add 行为不满足需求推荐在对应控制器文件或 standalone action中编写同名自定义 action 覆盖它或在config/routes.js中显式绑定自定义路由详见 blueprint-api.md 的 Overriding blueprints 一节。也可以利用sails.config.blueprints.parseBlueprintOptions完全自定义参数解析lib/hooks/blueprints/index.js 中提供了默认实现Sails 1.0 起defaultLimit等旧配置已迁移到该函数。事务性add.js 中有一段 FUTURE 注释第 42-50 行提到未来在参与模型使用同一 datastore 且该 datastore 支持事务时会将校验父/子记录 执行 addToCollection包裹进数据库事务以避免中间态不一致——当前实现是分步执行的理解这一点有助于排查并发场景下的数据问题。测试佐证路由绑定与E_UNIQUE幂等行为在集成测试中均有覆盖可参考 test/integration/hook.blueprints.shortcut.routes.test.js 和 test/integration/hook.blueprints.action.routes.test.js 自行验证。一句话总结Add blueprint 是 Sails 自动为每个集合关联生成的关联追加端点它替你完成了父/子记录存在性校验、addToCollection调用、反向外键同步、Socket 实时广播和 populate 后返回——开发者只需约定好PUT /:model/:id/:association/:fk即可零代码获得一对多/多对多关联的加入集合能力。【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询