ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PostGraphile V4 到 V5:makeExtendSchemaPlugin 迁移完全指南

PostGraphile V4 到 V5:makeExtendSchemaPlugin 迁移完全指南 后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载本篇迁移指南以 PostGraphile 官方makeExtendSchemaPlugin迁移文档为主体系统讲解从 V4 的 lookahead预读取与 resolver 体系迁移到 V5 Grafast规划plan与执行引擎的完整路径。读者将掌握requires、pgField、pgQuery、pgSubscription、selectGraphQLResultFromTable、embed、Savepoints、context.pgClient.query、QueryBuilder、getTypeAndIdentifiersFromNodeId等十余项 V4 特性的 V5 等价方案并能够用extendSchema源码位于 graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts写出更简洁、更高效的定制插件。为什么 V4 的许多技巧在 V5 中不再需要PostGraphile V4 依赖一套被称为 lookahead预读取的机制resolver 需要主动向前看GraphQL 查询中请求了哪些字段据此拼装 SQL这套系统被官方文档形容为hacky and chaotichack 且混乱。从 V4 迁移到 V5 后这套系统被全新的 Grafast规划与执行引擎取代因此一大批曾经的 workaround 都不再需要了具体包括指令Directivesrequires、pgField、pgQuery辅助函数selectGraphQLResultFromTable、embedSavepoints保存点context.pgClient.queryQueryBuilder 的 named children命名子节点QueryBuilder 本身build.getTypeAndIdentifiersFromNodeId各种先确保类型加载完成再按名字引用的 hack根本原因是V5 不再使用 resolver而是使用 plan计划。从源码看extendSchema 的实现接收一个返回ExtensionDefinition的回调该定义中的plans、objects、interfaces、unions、scalars、enums、inputObjects等键会被转换为 Grafastschema 配置旧的resolvers键在源码中被标记为deprecated Use objects/scalars/etc instead见 makeExtendSchemaPlugin.ts#L96-L107。严格来说处理外部系统时你仍然可以继续使用 resolver但凡是原来依赖上述指令行为的场景都必须改用 plan所以不如直接全面拥抱 plan。官方文档在pgQuery一节还留了一个 TODO为scope指令寻找替代方案说明迁移过程仍在持续演进。extendSchema 与 makeExtendSchemaPlugin 的关系V5 中迁移的主入口是extendSchema。二者的关系如下维度V4V5入口makeExtendSchemaPlugin(build, options)extendSchema(build)options 即build.options字段实现resolversplans推荐或objects更类型安全的后续模式底层手工写 GraphQL resolver转换为 Graphile Build 的 schema hooks在源码中extendSchema与makeExtendSchemaPlugin实际上是同一个函数的两个导出名见 graphile-build/graphile-utils/src/index.ts#L37-L41。调用extendSchema时如果回调签名仍保留两个参数(build, options)源码会打印弃用提示you should just use thebuildargument sinceoptionsis justbuild.options见 makeExtendSchemaPlugin.ts#L205-L209。新的调用方式从postgraphile/utils导入const { extendSchema, gql } require(postgraphile/utils);requires用 .get() 从父 plan 取列V4 中requires(columns: [...])的作用是保证传入 resolver 的父对象带有指定列但列名可能被转换成驼峰大小写官方文档对此用了:grimacing:表情吐槽。在 V5 的 plan 中直接对父 plan 逐个调用.get(...)即可父对象上的列本来就是 plan 可以随时按需取用的。下面是从 V4 文档转换而来的例子通过lambdastep 调用convertUsdToAud函数把price_in_us_cents换算成澳元价格。-const { makeExtendSchemaPlugin, gql } require(graphile-utils); const { extendSchema, gql } require(postgraphile/utils); const { convertUsdToAud } require(ficticious-npm-library); const { lambda } require(postgraphile/grafast); -const MyForeignExchangePlugin makeExtendSchemaPlugin((build, options) { const MyForeignExchangePlugin extendSchema((build) { const { options } build; return { typeDefs: gql extend type Product { - priceInAuCents: Int! requires(columns: [price_in_us_cents]) priceInAuCents: Int! } , - resolvers: { plans: { Product: { - priceInAuCents: async (product) { - // Note that the columns are converted to fields, so the case changes - // from price_in_us_cents to priceInUsCents - const { priceInUsCents } product; - return await convertUsdToAud(priceInUsCents); - }, priceInAuCents($product) { const $cents $product.get(price_in_us_cents); return lambda($cents, cents convertUsdToAud(cents)); }, }, }, }; });:::tip 批量场景请用 loadOne 上面用lambda是因为convertUsdToAud每次只转换一个值如果手里有能一次性批量换算多个货币值的函数更高效的做法是通过loadOne只调用一次而不是像lambda那样每个值调用一次。 :::lambda与loadOne都是 Grafast的标准 step前者用于把单个或少数plan 值交给普通 JS 函数处理后者用于以批处理方式加载数据并自动避免 N1 问题。pgField给正确的字段挂上正确的 planpgField指令从一开始就是 workaround权宜之计在 V5 中它不再有意义——只需确保给正确的字段添加正确的 plans一切就会按你期望的方式工作而且比 V4 的许多模式尤其是 mutation payload 相关模式更高效、更直接。:::tip 不要在单个字段里做所有事 不要总是试图在一个字段里做完所有逻辑。更好的做法是把 plans 分配给各个子字段这样相关逻辑只在该字段真正被请求时才执行代码也会更简单。 :::这一节的核心思想是 V5 的懒执行特性plan 只有在字段被客户端请求时才会被实例化并进入执行计划因此把逻辑拆分到多个子字段能天然获得按需计算的性能收益。pgQuery用 SQL 表达式或 lambda 代替内联 SQLV4 中pgQuery用于把 SQL 内联进 GraphQL 操作通常是作为绕过PostgreSQL 没有内联 computed column 函数之类问题的性能优化手段。V5 中这件事交给 plan 处理。根据目标不同你有多种 plan 可选。场景一叶子字段需要在数据库里计算对于叶子字段如果你希望在数据库里而非 JS 中完成计算可以使用 SQL 表达式-module.exports makeExtendSchemaPlugin(build { module.exports extendSchema(build { const { pgSql: sql } build; return { typeDefs: gql extend type User { - nameWithSuffix(suffix: String!): String! pgQuery( - fragment: ${embed( - (queryBuilder, args) - sql.fragment(${queryBuilder.getTableAlias()}.name || || ${sql.value( - args.suffix - )}::text) - )} - ) nameWithSuffix(suffix: String!): String! } , objects: { User: { plans: { nameWithSuffix($user, { $suffix }) { return $user.select( sql${$user.getClassStep().alias}.name || || ${$user.placeholder($suffix, TYPES.text)}, TYPES.text, ); } } } } }; });:::note 这不是 SQL 注入 上面的代码并不是SQL 注入攻击的示例。它使用来自pg-sql2模块的sql标签模板函数tagged template literal来确保所有参数都被正确、安全地处理。placeholder会把 plan 值作为参数绑定而不是拼进 SQL 文本。 :::场景二更简单高效的 JS 方案官方文档指出这个例子更高效也更简单的解法其实是在 JS 中完成 plans: { User: { nameWithSuffix($user, { $suffix }) { return lambda( [$user.get(name), $suffix], ([name, suffix]) ${name} ${suffix}, ); }, }, },lambda接收一个 plan 数组作为第一个参数回调收到的则是这些 plan 对应的运行时值数组——这里把name列和suffix参数都取出来拼成字符串。dataplan/pg的相关细节请参考其文档仓库内对应源码位于 grafast/dataplan-pg/src。pgSubscription用 subscribePlan listen() 替代V4 中pgSubscription来自graphile/pg-pubsub允许你在 SDL 中内嵌一个 topic 生成器。V5 中应该移除该指令把逻辑放进 Grafast的subscribePlan并使用listen(...)step其实现位于 grafast/grafast/src/steps/listen.ts#L113。V4 写法import { makeExtendSchemaPlugin, gql, embed } from graphile-utils; const currentUserTopicFromContext async (_args, context) { if (!context.jwtClaims?.user_id) throw new Error(Youre not logged in); return graphql:user:${context.jwtClaims.user_id}; }; export default makeExtendSchemaPlugin(() ({ typeDefs: gql extend type Subscription { currentUserUpdated: UserSubscriptionPayload pgSubscription(topic: ${embed(currentUserTopicFromContext)}) } type UserSubscriptionPayload { user: User event: String } , resolvers: { UserSubscriptionPayload: { user(event) { /* ... */ }, }, }, }));V5 写法import { extendSchema } from postgraphile/utils; export default extendSchema((build) { const { grafast: { context, get, listen, lambda }, dataplanJson: { jsonParse }, pgResources: { users }, } build; return { typeDefs: /* GraphQL */ extend type Subscription { currentUserUpdated: UserSubscriptionPayload } type UserSubscriptionPayload { user: User event: String } , objects: { Subscription: { plans: { currentUserUpdated: { subscribePlan(_$root, _args) { const $pgSubscriber context().get(pgSubscriber); const $userId get(context().get(jwtClaims), user_id); const $topic lambda($id, (id) graphql:user:${id}); return listen($pgSubscriber, $topic, jsonParse); }, plan($event) { return $event; }, }, }, }, UserSubscriptionPayload: { plans: { user($payload) { const $id get($payload, subject); return users.get({ id: $id }); }, }, }, }, }; });迁移要点关键迁移点topic 的选择现在是subscribePlan里的普通代码而不是指令元数据。listen($pgSubscriber, $topic, jsonParse)监听由$topicstep 计算出的 topic 名称并把收到的消息通过jsonParse解析成 payload。如果你的 V4 topic 来自字段参数那么在subscribePlan中用fieldArgs.getRaw(...)取出const $forumId fieldArgs.getRaw(forumId);然后把这个 step 用于构造$topic。更完整的订阅迁移指导可参考仓库中的 Realtime 与 Subscriptions 文档。selectGraphQLResultFromTable成为历史N1 自动解决V4 中这个方法用于从 GraphQL resolver 中发起一次look-ahead增强数据读取但它始终存在引入 N1 问题的风险。很多用户觉得它难以理解甚至会拿它来为自己在 resolver 里取数据——这完全背离了它的设计意图。V5 中不再需要这个辅助函数每一个 plan step 都无需任何仪式就自动纳入了规划系统N1 问题由 Grafast自动解决。用于在 plan 中取数据的方法与用于填充数据的方法现在是同一个所以两者之间的混淆也不复存在。下面把 V4 文档里的例子移植到 V5。首先找到代表match_user函数的pgResource然后为Query.matchingUser字段添加 plan执行该函数并透传searchText参数。-module.exports makeExtendSchemaPlugin((build) { module.exports extendSchema((build) { const matchUser build.pgResources.match_user; return { typeDefs: /* GraphQL */ type Query { matchingUser(searchText: String!): User } , - resolvers: { plans: { Query: { - matchingUser: async (parent, args, context, resolveInfo) { - const [row] await resolveInfo.graphile.selectGraphQLResultFromTable( - sql.fragment(select * from match_user(${sql.value( - args.searchText, - )})), - () {}, // no-op - ); - return row; - }, matchingUser($parent, { $searchText }) { return matchUser.execute({ step: $searchText }); }, }, }, }; });:::tip[这个模式已被弃用] 上面的typeDefs/plans模式是为了展示最省事的迁移路径但它已经被弃用——因为它难以做到类型安全。新的推荐模式是typeDefs/objects针对使用 GraphQLtype关键字的对象类型把 plans 嵌套在对应对象内部-module.exports makeExtendSchemaPlugin((build) { module.exports extendSchema((build) { const matchUser build.pgResources.match_user; return { typeDefs: /* GraphQL */ type Query { matchingUser(searchText: String!): User } , - resolvers: { objects: { Query: { - matchingUser: async (parent, args, context, resolveInfo) { - const [row] await resolveInfo.graphile.selectGraphQLResultFromTable( - sql.fragment(select * from match_user(${sql.value( - args.searchText, - )})), - () {}, // no-op - ); - return row; - }, plans: { matchingUser($parent, { $searchText }) { return matchUser.execute({ step: $searchText }); }, }, }, }, }; });你可以选择一次性完成迁移也可以分两个阶段进行。 :::这一模式与extendSchema源码中的objects处理逻辑一致源码会对objects里每个类型做位置校验assertLocation并检查字段确实在typeDefs中定义过见 makeExtendSchemaPlugin.ts#L495-L513从而尽早暴露拼写错误。embed暂无替代但也不再需要目前embed没有替代品你也不应该再需要它。官方文档的建议是如果你觉得自己确实需要它请到 Graphile 的 Discord 社区提问。Savepoints按需事务彻底告别V4 中每个 GraphQL 请求都被包裹在一个事务里。为了满足 GraphQL 规范每个 mutation 都需要包在SAVEPOINT中确保单个 mutation 失败时其它 mutation 不会被回滚即所谓的部分成功。V5 中事务是按需创建的因此不再需要 savepoints。这消除了一个已知的性能隐患——PostgreSQL 子事务subtransactions是有性能开销的。context.pgClient.query换成按需的 PG client 步骤V4 在每次 GraphQL 请求开始时就预备好一个 Postgres client 并放进事务里——即使根本用不上它。这个 client 以pgClient身份放进 GraphQL context可以在 mutation 中直接使用。V5 中 Postgres client 是按需供给的自定义读取使用loadOneWithPgClient()/loadManyWithPgClient()这样你仍然能享受 Grafast的批处理batching能力mutation 写入按是否显式需要事务选择sideEffectWithPgClient()或sideEffectWithPgClientTransaction()。值得注意的是这里的 pgClient 是一个通用适配器generic adaptor如果你想用自己偏好的 Postgres 客户端pg、postgres、pg-promise等处理完全没问题。下面是一个完整的自定义 mutation 示例来自官方文档演示withPgClientTransaction的用法import { object } from postgraphile/grafast; // highlight-next-line import { withPgClientTransaction } from postgraphile/dataplan/pg; import { extendSchema } from postgraphile/utils; export default extendSchema((build) { const { sql } build; /** * The executor tells us which database were talking to. This is the * default executor. * * If youre talking to multiple databases, you can get this from the * registry, the default executor name is main but you can override this and * add extra sources/executors via the pgServices configuration option: * * * const executor build.input.pgRegistry.pgExecutors.main; * */ const executor build.pgExecutor; return { typeDefs: /* GraphQL */ input MyCustomMutationInput { count: Int } type MyCustomMutationPayload { numbers: [Int!] } extend type Mutation { An example mutation that doesnt really do anything; uses Postgres generate_series() to return a list of numbers. myCustomMutation(input: MyCustomMutationInput!): MyCustomMutationPayload } , objects: { Mutation: { plans: { myCustomMutation(_$root, { $input: { $count } }) { /** * This step dictates the data that will be passed as the second argument * to the withPgClientTransaction callback. This is typically * information about the field arguments, details from the GraphQL * context, or data from previously executed steps. */ const $data object({ count: $count, }); // Callback will be called with a client thats in a transaction, // whatever it returns (plain data) will be the result of the // withPgClientTransaction step; if it throws an error then the // transaction will roll back and the error will be the result of the // step. // highlight-start const $transactionResult withPgClientTransaction( executor, $data, async (client, data) { // The data from the $data step above const { count } data; // Run some SQL const { rows } await client.query( sql.compile( sqlselect i from generate_series(1, ${sql.value( count ?? 1, )}) as i;, ), ); // Do some asynchronous work (e.g. talk to Stripe or whatever) await sleep(2); // Maybe run some more SQL as part of the transaction await client.query(sql.compile(sqlselect 1;)); // Return whatever data youll need later return rows.map((row) row.i); }, ); // highlight-end return $transactionResult; }, }, }, MyCustomMutationPayload: { plans: { numbers($transactionResult) { return $transactionResult; }, }, }, }, }; });要点拆解executor 从哪里来build.pgExecutor指向默认的数据库执行器。多数据库场景下可以从 registry 中取build.input.pgRegistry.pgExecutors.main通过pgServices配置可以覆盖默认 executor 名并新增额外的 source/executor。$data step用object({ count: $count })组装传给回调的第二个参数其内容可以是字段参数、GraphQL context 信息或之前步骤产生的数据。回调语义withPgClientTransaction(executor, $data, callback)会用一个已开启事务的 client 调用回调回调返回的普通数据即该 step 的结果若回调抛错事务回滚错误成为该 step 的结果。SQL 组装sql标签模板 sql.value()绑定参数再通过sql.compile()得到可执行的 SQL 文本——同样的防注入机制。QueryBuilder named children概念作废named children 这个概念在 V5 中不再有用、也不再需要它可以被移植成更直接的 Grafaststeps。如果移植遇到困难官方建议到 Discord 寻求帮助。QueryBuilder 本身被 pgSelect 等 step 取代QueryBuilder 在 V5 中不再存在。取而代之你大多会使用pgSelect见 dataplan/pg 的 step 库以及同类 step 上的辅助方法。这些 step 提供了按需执行、自动批处理等能力写法上更接近声明式描述而不是命令式拼 SQL。build.getTypeAndIdentifiersFromNodeId改用 specFromNodeId这个辅助函数已被specFromNodeId取代实现位于 grafast/grafast/src/steps/node.ts#L19。工作方式如下每个实现了 Node 接口的 GraphQL 类型都会注册一个 node ID handler节点 ID 处理器如果你知道预期的typeName可以通过build.getNodeIdHandler(typeName)拿到这个 handler从 handler 可以确定编码 NodeID 所用的 codec编解码器把 handler、codec 与 node ID 一起传给specFromNodeId它会返回该节点的一个规范specification典型形态是{id: $id}其中$id是一个可执行的 step但具体形状会因节点类型而异。示例const typeName User; const handler build.getNodeIdHandler(typeName); const objects { Mutation: { plans: { updateUser(parent, fieldArgs) { const spec specFromNodeId(handler, fieldArgs.$id); const plan object({ result: pgUpdateSingle(userSource, spec) }); fieldArgs.apply(plan); return plan; }, }, }, };这里specFromNodeId把不透明的全局节点 ID 解码为可执行的spec如{ id: $id }随后交给pgUpdateSingle(userSource, spec)这类dataplan/pg的步骤直接使用。迁移清单总结V4 特性V5 替代方案说明makeExtendSchemaPlugin(build, options)extendSchema(build)options即build.options从postgraphile/utils导入resolversplans/objectsobjects是更类型安全的推荐模式requires(columns)$parent.get(column)直接取列无大小写转换问题pgField给正确字段加 plans指令整体作废pgQuerySQL 表达式 plan /lambda数据库计算用$user.select(...)JS 计算用lambdapgSubscriptionsubscribePlanlisten()topic 变成普通代码字段参数用fieldArgs.getRaw()selectGraphQLResultFromTablepgResource.execute({ step })N1 由 Grafast自动解决embed无不再需要Savepoints按需事务消除子事务性能开销context.pgClient.queryloadOneWithPgClient()/sideEffectWithPgClient()/withPgClientTransaction()client 是通用适配器可自选驱动QueryBuilder / named childrenpgSelect等 stepQueryBuilder 整体移除getTypeAndIdentifiersFromNodeIdspecFromNodeIdgetNodeIdHandler(typeName)返回可执行 spec更多参考extendSchema 完整文档extendSchema的完整签名、回调返回值约定与更多示例仓库内对应章节还提到SDL 只作为便捷语法底层会转换成 Graphile Build 的 schema hooks并不会被严格校验。自定义查询与自定义 mutationextendSchema的实际应用场景。自定义概览extendSchema被定位为添加字段与类型的首选工厂函数。Realtime 与 Subscriptions订阅相关迁移。PostGraphile 仓库 README内置一个完整的购物车摘要业务逻辑示例展示extendSchema结合pgResources、loadOne等能力实现任意 Node.js 业务逻辑。源码实现graphile-build/graphile-utils/src/makeExtendSchemaPlugin.ts、grafast/grafast/src/steps/node.ts、grafast/grafast/src/steps/listen.ts。赞分享后端API网关【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址https://gitcode.com/gh_mirrors/cry/crystal点击查看免费下载相关推荐PostGraphile V5 迁移指南用 Grafast 计划Plans重写 makeExtendSchemaPluginPostGraphile V5 迁移指南用 Grafast 计划Plans重写 makeExtendSchemaPlugin 导读 PostGraphi后端API网关PostGraphile V4 到 V5 迁移指南makeAddPgTableOrderByPlugin 到 addPgTableOrderBy 的完整改造PostGraphile V4 到 V5 迁移指南makeAddPgTableOrderByPlugin 到 addPgTableOrderBy 的完整改造后端API网关PostGraphile V5 迁移指南从 makeAddPgTableOrderByPlugin 到 addPgTableOrderByPostGraphile V5 迁移指南从 makeAddPgTableOrderByPlugin 到 addPgTableOrderBy PostGraph后端API网关创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表