
Sails Blueprint API 之 UpdatePATCH 路由下的记录更新、关联通知与源码级解析【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails导读Update是 Sails 内置 Blueprint蓝图API 提供的通用更新端点用于按主键更新数据库中的既有记录并向已订阅该记录的 WebSocket 客户端推送变更通知。本文基于当前仓库 sailsRealtime MVC Framework for Node.js的 Update 官方文档 展开并结合 lib/hooks/blueprints/actions/update.js、lib/hooks/blueprints/index.js、lib/hooks/blueprints/parse-blueprint-options.js 等源码深入讲解其路由绑定、参数解析、底层执行链路与 Socket 通知机制。读完本文你将能熟练使用PATCH /:model/:id完成记录更新、理解其错误语义与关联广播行为并掌握通过parseBlueprintOptions自定义该端点的进阶方法。Blueprint Update 是什么Blueprint Update 是 Sails 为每个模型自动生成的影子路由shadow route动作之一。当 Blueprint 功能开启时只要你的应用中存在 Waterline 模型例如UserSails 便会自动注册对应模型身份identity的update动作无需手写任何控制器代码即可获得一个完整的 RESTful 更新端点PATCH /:model/:id该端点完成三件事按id定位要更新的记录应用请求体中携带的属性变更表单编码或 JSON 均可向已订阅该记录的 Socket 客户端广播变更通知需开启 pubsub/WebSocket 能力。如果校验失败返回携带无效属性信息且状态码为400的 JSON 响应如果不存在匹配id的记录则返回404。端点如何被注册路由绑定与开关从源码结构看update 动作的注册逻辑位于 lib/hooks/blueprints/index.js 中。动作注册Blueprints 钩子初始化时会通过registerActions为每个模型注册一套同名动作lib/hooks/blueprints/index.js其中更新动作对应sails.registerAction(BlueprintController.update, modelIdentity /update);每个模型都会获得独立的一套动作如user/update、pet/update这样可以为不同模型配置不同的策略policies与中间件。路由绑定路由的绑定由bindShadowRoutes完成分为三种形式RESTful 路由默认开启当sails.config.blueprints.rest true时绑定patch %s/:id指向 update 动作lib/hooks/blueprints/index.js。同时 Sails 仍会绑定put %s/:id并打印一条调试日志Using PUT to update a record is deprecated in Sails 1.0. Use PATCH instead!快捷路由shortcut默认开启当sails.config.blueprints.shortcuts true时绑定get %s/update/:idlib/hooks/blueprints/index.js允许用 GET 请求加查询字符串参数触发更新。显式动作路由当sails.config.blueprints.actions true时也可通过{action: user/update}之类的路由目标显式引用该动作。相关开关与prefix、restPrefix、pluralize等修饰项的说明可参考 docs/reference/sails.config/sails.config.blueprints.md。请求参数详解更新端点接受两类输入路由参数定位记录与请求体/查询参数要变更的属性值。下表来自 Update 文档参数类型说明modelstring目标模型的 identity如PATCH /product/5中的productidstring要更新记录的主键值如PATCH /product/5中的5*json对于PATCHRESTful请求在请求体中传递与模型属性同名的参数以设置这些值对于GETshortcut请求则将这些参数放入查询字符串参数的底层解析逻辑从 lib/hooks/blueprints/parse-blueprint-options.js 可以看到update分支的解析细节criteria.where[Model.primaryKey]取req.param(id)即按主键精确定位单条记录valuesToSet取req.allParams()并_.omit掉id主键保护无论请求中是否携带主键参数代码都会强制把主键值恢复为路由中id指定的值若检测到试图通过请求体修改主键会打印告警Cannot change primary key via update blueprint; ignoring value sent for ...并忽略该值meta被设置为{}源码注释明确指出此处特意不设置fetch: true因为某些 Waterline 版本会在与.updateOne()搭配时对fetch提出异议。完整示例修改一条记录的属性沿用文档示例将用户 #47Applejack的爱好修改为 kickin。发送请求PATCH /user/47请求体JSON 或表单编码均可{ hobby: kickin }预期响应{ hobby: kickin, id: 47, name: Applejack, createdAt: 1485462079725, updatedAt: 1485476060873 }返回的 JSON 字典是更新后且已填充关联的完整记录其中id、name、createdAt、updatedAt为模型既有字段hobby已被更新为新值updatedAt也相应刷新。快捷路由shortcut方式若shortcuts开启同样效果可通过 GET 请求达成GET /user/update/47?hobbykickin底层执行链路update 动作源码逐段解析整个端点的核心实现在 lib/hooks/blueprints/actions/update.js其执行流程是一个典型的三段式异步链路第一步解析蓝图选项并定位模型var parseBlueprintOptions req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions; req.options.blueprintAction update; var queryOptions parseBlueprintOptions(req); var Model req._sails.models[queryOptions.using];parseBlueprintOptions可从req.options或全局配置覆盖这为自定义解析逻辑留出了扩展点见后文进阶自定义一节。第二步先 findOne 定位原始记录Model.findOne(_.cloneDeep(criteria), _.cloneDeep(queryOptions.populates)) .exec(function (err, matchingRecord) { ... });源码注释明确解释了这一设计意图本可以用一条查询完成更新但先执行一次findOne是为了给前端集成者提供更好的体验——既能区分记录不存在返回404也能拿到更新前的原始记录用于后续 Socket 通知。若findOne抛出的错误名为UsageError典型如非法查询条件则通过res.badRequest(formatUsageError(err, req))返回400其他错误返回500。第三步updateOne 实际更新Model.updateOne(_.cloneDeep(criteria)) .set(queryOptions.valuesToSet) .meta(queryOptions.meta) .exec(function (err, updatedRecord) { ... });这里使用 Waterline 的updateOne并配合.set()写入变更值。错误处理进一步细分AdapterError且code E_UNIQUE唯一性约束冲突→ 返回400UsageErrorWaterline 校验错误→ 返回400并携带校验信息其余错误 → 返回500若updateOne之后仍找不到记录竞态场景→ 返回404。第四步发布更新通知pubsubif (req._sails.hooks.pubsub) { if (req.isSocket) { Model.subscribe(req, [updatedRecord[Model.primaryKey]]); } var pk updatedRecord[Model.primaryKey]; Model._publishUpdate(pk, _.cloneDeep(queryOptions.valuesToSet), !req.options.mirror req, { previous: _.cloneDeep(matchingRecord) }); }要点如果请求本身来自 Socket会先把发起请求的 socket 订阅到该记录_publishUpdate广播的是valuesToSet实际变更的键值对而非整条记录并附上previous更新前的原始记录源码注释中的_.cloneDeep()是为了确保广播出去的是纯字典而非被篡改的对象引用若请求来自普通 HTTP 而非 Socket!req.options.mirror req该请求也会被广播出去。第五步重新查询以填充关联并响应Model.findOne(_.cloneDeep(criteria), _.cloneDeep(queryOptions.populates)) .exec(function foundAgain(err, populatedRecord) { if (err) { return res.serverError(err); } if (!populatedRecord) { return res.serverError(Could not find record after updating!); } res.ok(populatedRecord); });与第二步类似源码注释承认这条额外查询本可以省去但保留它以便返回给前端的记录已经填充好所有关联populates提升集成体验。此时若再次查询失败则视为服务端异常返回500。Socket 通知实时推送更新事件当应用启用了 WebSocketpubsub能力时每个订阅了该记录的客户端都会收到一条事件事件名即模型身份如userpayload 结构如下verb: updated, id: the record primary key, data: a dictionary of changes made to the record, previous: the record prior to the update继续上面的例子所有订阅了User#47 的客户端发起请求的客户端除外都会收到{ id: 47, verb: updated, data: { id: 47, hobby: kickin updatedAt: 1485476060873 }, previous: { hobby: pickin, id: 47, name: Applejack, createdAt: 1485462079725, updatedAt: 1485462079725 } }关联变更引发的额外通知如果本次更新修改了与其他记录的链接则可能产生额外的通知。以把用户 #47 改派给商店 #25为例——store代表一对多关联中的一侧PATCH /user/47{ store: 25 }订阅了**新商店25**的客户端会收到addedTo通知订阅了旧商店的客户端会收到removedFrom通知。这两类通知的详细格式分别见 Add blueprint 参考 与 Remove blueprint 参考。通知的源码实现印证上述行为在 lib/hooks/pubsub/index.js 的_publishUpdate中有完整对应实现构造基础广播数据{ model, verb: update, data: changes, id }若提供了options.previous且未禁用反向通知会逐键比对changes与previous找出发生了变化的关联属性对于model型关联如上面的store若旧值存在则对旧关联执行_publishRemove/_publishUpdate(null)若新值存在则对新关联执行_publishAdd/_publishUpdate(link)——这正是文档所述addedTo与removedFrom通知的来源注意广播事件中verb的取值_publishUpdate内部使用verb: update而客户端收到的资源型 pubsub 事件描述为verb: updated对应 docs/reference/websockets/resourceful-pubsub/resourceful-pubsub.md 中记录的updated语义两者在事件语义上是同一回事。错误处理速查场景状态码说明校验失败validation error400响应体携带无效属性信息来源UsageError或AdapterError唯一性约束冲突400AdapterError且code E_UNIQUE指定id无对应记录404findOne/updateOne均查不到目标记录其他底层错误500数据库驱动异常、更新后复查失败等注意事项与最佳实践本动作可用于替换整个集合关联例如替换用户的完整好友列表效果与 replace blueprint 动作 相同若要逐个增删集合项应改用 add 或 remove 动作。在早期 Sails 版本中该动作绑定在PUT /:model/:id路由上Sails 1.0 起推荐使用PATCHPUT仍可用但会输出弃用提示日志见 lib/hooks/blueprints/index.js。主键不可通过本端点修改请求体中即使携带了与路由id不同的主键值也会被忽略并告警如需改主键应删除重建记录。更新动作对关联变更采用先查后更再查的三次查询策略findOne → updateOne → findOne这是刻意为之——既为了区分 404 语义也为了让响应包含已填充关联的最新记录牺牲少量性能换取更好的前端集成体验源码注释已明示见 update.js。仓库源码还在更新动作处预留了数据库事务的注释update.js标明 FUTURE 可在 datastore 支持事务时将其包裹进Model.getDatastore().transaction(...)可作为深度定制时的参考方向。进阶自定义改写 parseBlueprintOptions从 parse-blueprint-options.js 的注释可知该函数是just the default implementation -- it can be overridden默认实现可被覆盖。如果你希望改变 update 端点的默认行为例如限制可被更新的属性白名单、对valuesToSet做预处理可以在config/blueprints.js中提供自定义的parseBlueprintOptions函数修改返回的valuesToSet、criteria或meta而不必改动 Sails 核心代码。详细的配置位置与用法可参考 docs/reference/sails.config/sails.config.blueprints.md。小结Blueprint Update 是 Sails 零控制器 CRUD 能力中承上启下的关键一环它由 Blueprints 钩子自动为每个模型注册PATCH /:model/:id及兼容的 shortcut/显式路由内部通过findOne → updateOne → 广播 → findOne的链路保证 404 语义、校验反馈与关联填充的完整性同时借力 pubsub 钩子将updated及关联变更的addedTo/removedFrom事件实时推送给订阅客户端。理解其路由开关、参数解析与错误语义是构建 Sails 实时应用Realtime MVC与自定义 REST 行为的基础。【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考