尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Feathers 数据库适配器通用 API 完全指南:初始化、分页、查询与扩展

Feathers 数据库适配器通用 API 完全指南:初始化、分页、查询与扩展 Feathers 数据库适配器通用 API 完全指南初始化、分页、查询与扩展【免费下载链接】feathersThe API and real-time application framework项目地址: https://gitcode.com/gh_mirrors/fe/feathers本文基于 Feathers 仓库 docs/api/databases/common.md 文档深入剖析 Feathers 数据库适配器Database Adapters共享的 Common API包括服务初始化与选项配置、分页机制、params.adapter/params.paginate的请求级动态覆盖、基于类的服务扩展方式以及find/get/create/update/patch/remove六个核心服务方法在各适配器中的具体行为。读完本文你将掌握如何在内存、MongoDB、KnexSQL等官方适配器之间以统一方式完成初始化、分页配置、批量操作管控与自定义扩展并能理解其底层实现原理。什么是 Feathers 数据库适配器Feathers 数据库适配器是提供了特定数据库标准 CRUD 功能的模块它们是 服务接口 的一种实现并共享一套通用初始化 API与通用查询语法。Feathers 官方核心适配器包括详见 adapters.mdMemorymemory内存数据存储MongoDBmongodbMongoDBSQLKnexknexMySQL、MariaDB、PostgreSQL、CockroachDB、SQLite、Amazon Redshift、OracleDB、MSSQL此外还有大量社区维护的适配器。需要强调的是每个数据库适配器都是 Feathers 服务接口 的一种实现。如果你的数据库没有现成适配器依然可以直接在自定义服务中使用它。在使用适配器之前建议先熟悉 Feathers 服务、服务事件和 hooks 以及数据库本身。初始化new NameService(options)每个适配器都会导出一个NameService类可以实例化后直接注册也可以被导出和继承import { NameService } from feathers-name app.use(/messages, new NameService()) app.use(/messages, new NameService({ id, events, paginate }))其中feathers-name对应具体的适配器包名如feathers-memory、feathers-mongodb、feathers-knex。通用选项以下选项对所有数据库适配器均适用id {string}可选— id 字段属性的名称通常默认值为id或_id。paginate {Object}可选— 一个分页对象包含default和max页面大小。multi {string[]|boolean}可选默认false— 允许create接收数组、patch和remove使用nullid 来修改多条记录。可以设为true以允许所有方法也可以传一个允许的方法名数组如[ remove, create ]。从源码看这些默认值集中在 packages/adapter-commons/src/service.ts 的AdapterBase构造函数中任何适配器在调用super(options)时都会以如下默认值合并用户传入的选项this.options { id: id, events: [], paginate: false, multi: false, filters: {}, operators: [], ...options }遗留选项已弃用应避免使用events {string[]}可选已弃用— 该服务发送的自定义服务事件列表。应改为在 app.use 注册服务时 通过events选项配置。operators {string[]}可选已弃用— 额外允许的非标准查询参数列表如[ $regex ]。更推荐使用查询 schema 来替代。filters {Object}可选已弃用— 额外的顶层查询过滤器对象例如{ $populate: true }也可以是转换函数如{ $ignoreCase: (value) value true ? true : false }。同样更推荐使用查询 schema。这些弃用选项依然保留在 packages/adapter-commons/src/declarations.ts 的AdapterServiceOptions接口中还包括同样被弃用的whitelist以便向后兼容但新代码不应再依赖它们。查询是如何被限制的适配器通过两种方式之一保护外部查询默认情况下二者是二选一的关系而非叠加的防护层路径适用时机定义允许的查询内置净化sanitization没有validateQueryhook或查询未被标记为已校验通用查询语法外加服务上的operators/filters查询 schemavalidateQuery以默认选项校验成功仅有你的查询 schema使用查询 schema 时适配器默认不会重新执行其$操作符白名单schema 本身即为完整白名单。请使用querySyntax/ query 辅助函数以及additionalProperties: false这样未知操作符无法通过校验。手写的 TypeBoxType.Object({ ... })schema 若缺少该选项在 Ajv 下是宽松的。若希望同时运行 schema 校验与内置白名单可使用validateQuery(schema, { skipSanitize: false })。从源码层面看这套机制的实现非常清晰packages/adapter-commons/src/service.ts 的sanitizeQuery方法会检查查询上是否带有VALIDATED符号标记export const VALIDATED Symbol.for(feathersjs/adapter/sanitized) async sanitizeQuery(params: ServiceParams {} as ServiceParams): PromiseQuery { // 查询已被 schema 校验过则跳过传统净化 if (params.query (params.query as any)[VALIDATED]) { return params.query || {} } // ...否则走 filterQuery 的内置过滤器/操作符白名单 }而validateQueryhook 在成功校验后会通过Object.defineProperty(query, VALIDATED, { value: true })给查询打上标记见 packages/schema/src/hooks/validate.ts。只有skipSanitize: false时才不打标记从而让两层防护同时生效。分页Pagination在初始化适配器时可以在paginate对象中设置以下分页选项default— 当$limit未设置时默认返回的记录条数。max— 每页允许的最大记录条数即使$limit查询参数设置得更高也会被限制。当设置了paginate.default时find将返回一个_分页对象_而非普通数组其形式如下{ total: total number of records, limit: max number of items per page, skip: number of skipped items (offset), data: [/* data */] }分页选项可以通过以下方式设置const service require(feathers-db-name) // 在初始化时设置 paginate 选项 app.use( /todos, service({ paginate: { default: 5, max: 25 } }) ) // 针对本次调用在 params.paginate 中覆盖分页设置 app.service(todos).find({ paginate: { default: 100, max: 200 } }) // 针对本次调用禁用分页 app.service(todos).find({ paginate: false })注意客户端无法禁用或更改默认分页。只有params.query会被传给服务器相关 workaround 可参考 Feathers issue #382 的讨论。分页的底层实现paginate.default/paginate.max的约束最终由 packages/adapter-commons/src/query.ts 的getLimit函数落地export const getLimit (_limit: any, paginate?: PaginationParams) { const limit parse(_limit) if (paginate (paginate.default || paginate.max)) { const base paginate.default || 0 const lower typeof limit number !isNaN(limit) limit 0 ? limit : base const upper typeof paginate.max number ? paginate.max : Number.MAX_VALUE return Math.min(lower, upper) } return limit }其逻辑为未显式传入$limit时取default作为下限基准max作为上限最终$limit被夹在[0, max]之间。而$limit过滤器是在 filterQuery 中通过FILTERS.$limit调用getLimit完成的。结合 packages/memory/src/index.ts 中_find的实现可以看到分页对象的确切构造方式先过滤出匹配项并统计total再按$skip切片、按$limit截断最终组装出{ total, limit, skip, data }。请求级动态选项params.adapter在服务方法params中设置adapter可以基于请求动态修改数据库适配器选项。例如可以临时允许批量创建/修改或临时调整分页设置const messages [ { text: message 1 }, { text: message 2 } ] // 仅针对本次请求启用批量插入 app.service(messages).create(messages, { adapter: { multi: true } })提示如果适配器有Model选项可以用params.adapter.Model根据请求指向不同的数据库从而实现多租户系统。这通常是在 hook 中通过设置context.params.adapter来完成的。从源码看params.adapter的合并逻辑在 packages/adapter-commons/src/service.ts 的getOptions中它先以this.options为基底再叠加params.paginate若显式提供和params.adapter因此params.adapter拥有最高优先级getOptions(params: ServiceParams): Options { const paginate params.paginate ! undefined ? params.paginate : this.options.paginate return { ...this.options, paginate, ...params.adapter } }这解释了为何params.adapter可以覆盖multi、Model乃至paginate等所有选项——每次服务方法调用都会经过getOptions重新计算生效配置。请求级分页覆盖params.paginate在服务方法params中设置paginate可以针对单次请求更改或禁用默认分页// 以数组形式获取全部消息 const allMessages await app.service(messages).find({ paginate: false })配合前面getOptions的代码可以看到params.paginate是独立于params.adapter的覆盖通道当它被显式设置包括设为false时会直接取代服务级的paginate配置。这也是params.adapter与params.paginate两个参数可以并存、且各自独立生效的原因。扩展适配器扩展已有数据库适配器有两种方式继承基类或通过 hooks 添加功能。基于类的扩展所有模块都会导出可被直接继承的 ES6 类。一个典型的扩展模式是自定义类继承MemoryService或任意NameService覆盖create等方法在调用super前后注入自定义逻辑同时可以复用基类全部能力分页、查询净化、multi管控等。packages/memory/src/index.ts 中的MemoryService本身就是这种模式的最佳示例——它继承自实现全部底层操作的MemoryAdapter然后在公开方法中统一注入sanitizeQuery查询净化与allowsMulti批量校验。服务方法的行为细节本节描述所有适配器如何实现服务方法。每个方法都同时给出服务端调用与对应 REST 请求的等价写法。constructor(options)初始化一个新的服务。重写构造函数时应调用super(options)。无 hooks 的方法Hook-less Methods数据库适配器支持通过给方法名加_前缀来不经过任何 hooks直接调用_find、_get、_create、_patch、_update和_remove。当需要服务的原始数据、且不想触发任何 hooks 时非常有用// 不运行任何 hooks 直接调用 get const message await app.service(/messages)._get(message id)注意这些方法仅在服务器端内部可用客户端不可用且仅适用于 Feathers 数据库适配器。它们不会发送任何事件。从源码看这些方法定义在 packages/adapter-commons/src/declarations.ts 的InternalServiceMethods接口中其文档注释明确标注了Does not sanitize the query and should only be used on the server不做查询净化、仅限服务端使用以及不触发事件。以 Memory 适配器为例MemoryService 的公开find/get/create等方法都会先经过sanitizeQuery和allowsMulti校验再委托给_find/_get等底层方法而_前缀方法直接落在 MemoryAdapter 上完全跳过这些防护。adapter.find(params)adapter.find(params) - Promise使用通用查询机制返回所有匹配params.query的记录列表。若启用了分页将返回分页对象否则返回结果数组。// 查找用户 id 为 1 的所有消息 const messages await app.service(messages).find({ query: { userId: 1 } }) console.log(messages) // 查找属于房间 1 或 3 的所有消息 const roomMessages await app.service(messages).find({ query: { roomId: { $in: [1, 3] } } }) console.log(roomMessages)对应的 REST 请求GET /messages?userId1 GET /messages?roomId[$in]1roomId[$in]3adapter.get(id, params)adapter.get(id, params) - Promise通过唯一标识即初始化时id选项指定的字段检索单条记录。const message await app.service(messages).get(1) console.log(message)对应 REST 请求GET /messages/1adapter.create(data, params)adapter.create(data, params) - Promise用data创建一条新记录data也可以是数组以批量创建需multi允许。const message await app.service(messages).create({ text: A test message }) console.log(message) const messages await app.service(messages).create([ { text: Hi }, { text: How are you } ]) console.log(messages)对应 REST 请求POST /messages { text: A test message }adapter.update(id, data, params)adapter.update(id, data, params) - Promise用data完全替换id标识的单条记录。不允许批量替换id不能为null且id本身不可被修改。const updatedMessage await app.service(messages).update(1, { text: Updates message }) console.log(updatedMessage)对应 REST 请求PUT /messages/1 { text: Updated message }adapter.patch(id, data, params)adapter.patch(id, data, params) - Promise将id标识的记录与data合并。id可以为null以批量合并匹配params.query的所有记录规则与.find相同。id本身不可被修改。const patchedMessage await app.service(messages).patch(1, { text: A patched message }) console.log(patchedMessage) const params { query: { read: false } } // 将所有未读消息标记为已读 const multiPatchedMessages await app.service(messages).patch( null, { read: true }, params )对应 REST 请求PATCH /messages/1 { text: A patched message }将所有未读消息标记为已读PATCH /messages?readfalse { read: true }adapter.remove(id, params)adapter.remove(id, params) - Promise删除id标识的记录。id可以为null以批量删除匹配params.query的所有记录规则与.find相同。const removedMessage await app.service(messages).remove(1) console.log(removedMessage) const params { query: { read: true } } // 删除所有已读消息 const removedMessages await app.service(messages).remove(null, params)对应 REST 请求DELETE /messages/1删除所有已读消息DELETE /messages?readtrue源码纵深multi批量操作如何被管控multi选项的判定逻辑集中在 packages/adapter-commons/src/service.ts 的allowsMulti方法。其中有一个固定规则表决定了某些方法的行为不受multi选项影响const alwaysMulti: { [key: string]: boolean } { find: true, get: false, update: false }即find永远允许多结果语义、get和update永远不允许而create、patch、remove的批量行为取决于multi配置multi true所有方法都允许批量multi为数组仅数组中列出的方法允许例如[ remove, create ]默认false均不允许。以 MemoryService.create 为例当传入数组且allowsMulti(create, params)返回 false 时会直接抛出MethodNotAllowedCan not create multiple entriespatch、remove对nullid 的处理同理见 MemoryAdapter._patch 与 _remove。这一点在 packages/adapter-commons/test/service.test.ts 中有完整的测试覆盖默认配置下patch(null)、remove(null)、create([])都会抛出MethodNotAllowed而将service.options.multi true后create([])即可正常通过。源码纵深查询净化与内置白名单适配器对外部查询的保护最终由 filterQuery 实现。它把查询拆分为过滤器和查询两部分并强制套用内置白名单内置过滤器FILTERS$skip、$sort、$limit、$select、$or、$and见 query.ts其中$limit会经由getLimit受分页配置约束$skip/$sort的取值会被解析为数字或规范化对象内置操作符OPERATORS$in、$nin、$lt、$lte、$gt、$gte、$ne、$or见 query.ts。任何以$开头、但既不在filters也不在operators白名单中的键都会触发BadRequestInvalid filter value ... / Invalid query parameter ...见 query.ts 与 query.ts。这正是内置净化路径下查询被限制的机制。不同适配器在此基础上还会叠加数据库特有的能力。例如 packages/knex/src/adapter.ts 为 SQL 适配器扩展了$like、$notlike、$ilike操作符并将通用操作符映射为 SQL 子句$ne→whereNot、$in→whereIn、$nin→whereNotIn、$lt→、$lte→、$gt→、$gte→这解释了为什么 Knex 适配器在 构造函数 中会把这些操作符合并进operators列表。总结Feathers 数据库适配器的 Common API 由四个核心支柱构成统一初始化所有适配器都通过new NameService(options)创建共享id、paginate、multi三个核心选项请求级覆盖params.adapter与params.paginate让单次请求可以动态修改数据库连接多租户、批量权限与分页策略统一查询与分页内置白名单 filterQuery净化 getLimit分页约束保证跨数据库一致的查询行为并可通过validateQuery与查询 schema 无缝替换为 schema 驱动的严格校验统一方法与扩展点六个服务方法语义一致_前缀方法提供无 hooks 的服务器内部通道ES6 类继承与 hooks 两条路径支撑自定义扩展。掌握这套 Common API你就能以完全一致的心智模型操作 Feathers 官方及社区的任意数据库适配器并随时深入 adapter-commons 源码理解其底层行为。【免费下载链接】feathersThe API and real-time application framework项目地址: https://gitcode.com/gh_mirrors/fe/feathers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表