ARTICLE DETAIL

资讯详情

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

objection.js 与 Koa + TypeScript 实战:从模型定义到 REST API 的完整示例解析

objection.js 与 Koa + TypeScript 实战:从模型定义到 REST API 的完整示例解析 数据库后端【免费下载链接】objection.jsAn SQL-friendly ORM for Node.js项目地址https://gitcode.com/gh_mirrors/ob/objection.js点击查看免费下载导读examples/koa-ts 是 objection.js 官方仓库中面向Node.js 8.0.0 及以上环境的 TypeScript 示例项目一个基于 Koa 的 REST API 服务展示了 objection.js 的核心能力——模型定义、查询、关系映射、eager loading贪婪加载与 graph inserts图插入。本文以该示例为骨架逐层拆解其工程结构、模型设计、API 路由与调用链并结合仓库源码验证每一个关键机制帮助你快速在真实的 TypeScript Koa 项目中落地 objection.js。注意正如示例自身强调的这不是一篇“如何搭建 Web 服务器”的教程而是一篇“如何在 Web 服务器中使用 objection.js”的教程其余部分被刻意保持得最简单。一、项目概览与运行方式1.1 工程定位该示例的完整目录结构如下均在 examples/koa-ts 下examples/koa-ts/ ├── migrations/20150613161239_initial_schema.js # Knex 数据库迁移脚本 ├── models/ │ ├── Animal.ts # 动物模型pets / owner │ ├── Movie.ts # 电影模型actors │ └── Person.ts # 人物模型pets / movies / children / parent ├── api.ts # REST 路由定义唯一业务层 ├── app.ts # Koa 应用入口、Knex 初始化、错误处理 ├── client.js # 基于 axios 的 API 演练脚本 ├── knexfile.js # Knex 数据库配置sqlite3 开发 / postgresql 生产 ├── tsconfig.json # TypeScript 编译配置 └── package.json # 依赖与脚本从结构上可以清晰看到 objection.js 的推荐分层模型层Model声明 schema 与关系 → 路由层Router组合查询 → 入口层App绑定 Knex 并启动服务。1.2 安装与运行按照 examples/koa-ts/README.md 给出的步骤可以直接从仓库运行git clone gitgithub.com:Vincit/objection.js.git objection cd objection/examples/koa-ts npm install npm start node client.js其中npm start实际执行的是见 package.json 的scripts字段migrate: knex migrate:latest, start: npm run migrate rm -rf dist tsc node dist/app.js即先跑数据库迁移 → 清空并重新编译 TypeScript → 运行编译产物dist/app.js。服务默认监听8641端口见 app.ts。node client.js则是一段用 axios 编写的演示脚本会依次调用全部 REST 端点打印每一步的 JSON 结果。1.3 依赖与运行环境依赖清单来自 package.json明确了本示例的技术栈依赖用途objection ^3.0.0-rc.4SQL 友好 ORM 核心knex ^0.95.13查询构建器 / 数据库驱动层koa ^2.11.0Web 服务器koa-bodyparser ^4.2.1解析请求体koa-router ^7.4.0路由注册axios ^0.19.0client.js 中的 HTTP 请求sqlite3 ^5.0.2开发环境数据库typescript 4.4.4dev编译types/koa等devTypeScript 类型定义engines.node声明为8.0.0这也是 README 所说“targets node 8.0.0 and up”的依据。注意示例中对objection的依赖为^3.0.0-rc.4属 3.x 候选版本若在真实项目中使用应结合当前 objection.js 发布版本调整。二、入口层 app.tsKnex 绑定、中间件与错误处理app.ts 是整个服务的入口只有约 60 行却完整演示了 objection.js 的三个关键接入步骤。2.1 初始化 Knex 并绑定模型// Initialize knex. const knex Knex(knexConfig.development) // Bind all Models to a knex instance. If you only have one database in // your server this is all you have to do. For multi database systems, see // the Model.bindKnex() method. Model.knex(knex)这里展示了 objection.js 最核心的全局约定通过静态方法Model.knex(knex)将当前进程内的所有模型绑定到同一个 Knex 实例。单数据库场景下一行代码即可完成全部模型的连接注入多数据库场景则需改用每个模型实例的bindKnex()方法该机制在 lib/model/modelBindKnex.js 中有独立实现。从源码结构看Model.knex()是 objection.js 提供的一种便捷全局绑定方式它让后续Person.query()、Movie.query()等调用无需再显式传入连接。2.2 中间件编排const router new KoaRouter() const app new Koa() // Register our REST API. registerApi(router) app.use(errorHandler) app.use(bodyParser()) app.use(router.routes()) app.use(router.allowedMethods())顺序为全局错误处理 → body 解析 → 路由分发。registerApi(router)来自 api.ts通过export default (router: KoaRouter) {...}的形式把全部路由挂到同一个 router 上。2.3 错误处理objection.js 异常类型的实际应用async function errorHandler(ctx: Context, next: () Promiseany) { try { await next() } catch (err: any) { if (err instanceof ValidationError) { ctx.status 400 ctx.body { error: ValidationError, errors: err.data } } else if (err instanceof ForeignKeyViolationError) { ctx.status 409 ctx.body { error: ForeignKeyViolationError } } else { ctx.status 500 ctx.body { error: InternalServerError, message: err.message || {} } } } }这是一个简单但非常典型的 objection.js 错误处理范式从objection包导入ValidationError与ForeignKeyViolationError分别映射为 HTTP 400请求体未通过模型 jsonSchema 校验与 HTTP 409外键冲突。其中ValidationError的data属性携带具体校验错误详情ForeignKeyViolationError的产生与数据库外键约束有关——注意 knexfile.js 中PRAGMA foreign_keys ON的开启正是为了让 SQLite 真正强制执行外键约束从而让这类错误能够被触发。仓库的 lib/model/ValidationError.js 与 lib/model/NotFoundError.js 等文件共同构成了 objection.js 的异常体系代码注释也建议读者参考仓库文档中的 错误处理手册 获取更完善的方案。三、数据库层迁移脚本与 Knex 配置3.1 迁移脚本定义的表结构20150613161239_initial_schema.js 创建了 4 张表正好支撑起示例的全部关系表关键列说明personsid(PK)、parentId(FK→persons.id,SET NULL)、firstName、lastName、age、address(json)自引用实现父子关系moviesid(PK)、name电影animalsid(PK)、ownerId(FK→persons.id,SET NULL)、name、species宠物属于某个人persons_moviespersonId(FK→persons.id,CASCADE)、movieId(FK→movies.id,CASCADE)多对多连接表两个细节值得注意persons.parentId与animals.ownerId都使用.onDelete(SET NULL)删掉父记录时子记录的外键会被置空而非级联删除而persons_movies使用.onDelete(CASCADE)删除人或电影时连接记录随之删除。down()方法按逆序dropTableIfExists依次清理保证可回滚。3.2 Knex 双环境配置knexfile.js 提供开发与生产两套配置module.exports { development: { client: sqlite3, useNullAsDefault: true, connection: { filename: ./example.db }, pool: { afterCreate: (conn, cb) { conn.run(PRAGMA foreign_keys ON, cb) }, }, }, production: { client: postgresql, connection: { database: example }, pool: { min: 2, max: 10 }, }, }开发环境使用无服务器依赖的 SQLite 文件./example.db并通过afterCreate钩子开启外键约束SQLite 默认关闭生产环境示例切换到 PostgreSQL并配置了连接池大小min: 2, max: 10。这意味着本示例可零成本本地运行同时保留了生产切换路径。四、模型层jsonSchema、Modifiers 与 relationMappings三个模型文件集中体现了 objection.js 模型定义的完整形态。4.1 Person 模型最完整的示例Person.ts 定义了 4 类关系覆盖了 objection.js 最常用的三种关系类型static relationMappings () ({ pets: { relation: Model.HasManyRelation, // 一对多一个人多只宠物 modelClass: Animal, join: { from: persons.id, to: animals.ownerId }, }, movies: { relation: Model.ManyToManyRelation, // 多对多演员 ↔ 电影 modelClass: Movie, join: { from: persons.id, through: { from: persons_movies.personId, to: persons_movies.movieId }, to: movies.id, }, }, children: { relation: Model.HasManyRelation, // 一对多 自引用 modelClass: Person, join: { from: persons.id, to: persons.parentId }, }, parent: { relation: Model.BelongsToOneRelation, // 反向自引用 modelClass: Person, join: { from: persons.parentId, to: persons.id }, }, })要点解读**relationMappings写成 thunk箭头函数**是为了避免模型间循环依赖——Person引用Movie而Movie又引用Person直接以对象字面量定义会因模块加载顺序而报错。多对多关系必须通过through对象描述连接表from/to分别指向连接表中两侧的外键。jsonSchema只是校验用途不是数据库 schema——代码注释明确强调“Nothing is generated based on this”。它规定了required: [firstName, lastName]及各字段类型是 objection.js 默认基于 JSON Schema 的校验机制见 lib/model/AjvValidator.js的数据来源。Person 还定义了可复用查询片段Modifierstatic modifiers: Modifiers { searchByName(query, name) { query.where((query) { for (const namePart of name.trim().split(/\s/)) { for (const column of [firstName, lastName]) { query.orWhereRaw(lower(??) like ?, [column, namePart.toLowerCase() %]) } } }) }, }这是一个“半智能”的模糊姓名搜索把输入按空白切分后对每个片段同时尝试firstName与lastName的前缀匹配并利用嵌套where生成括号以隔离多个or条件。Modifier 的底层机制可参考 lib/utils/createModifier.js在路由层通过query.modify(searchByName, name)按名调用。4.2 Movie 模型与 Animal 模型Movie.ts 定义了反向的多对多actors与 Person 的movies共用同一张persons_movies连接表from/to互换Animal.ts 则定义BelongsToOneRelation的owner。两者都带有各自的 jsonSchema 校验规则required: [name]。三个模型共同构成一个互相引用、可完整走通插入与查询的关系图。五、路由层 api.ts八组 REST 端点的 objection.js 用法api.ts 是业务核心几乎每一条路由都对应一个 objection.js 的典型查询场景。5.1 图插入POST /personsrouter.post(/persons, async (ctx) { const insertedGraph await Person.transaction(async (trx) { const insertedGraph await Person.query(trx) // For security reasons, limit the relations that can be inserted. .allowGraph([pets, children.[pets, movies], movies, parent]) .insertGraph(ctx.request.body) return insertedGraph }) ctx.body insertedGraph })insertGraph允许一次请求插入“人 其宠物 其子女 其参演电影 其父”这样的完整关系树所有行按依赖顺序落库。allowGraph是安全关键它限定可插入的关系白名单防止客户端通过请求体注入未预期的关系。代码注释特别说明若只需插入单个 person可把insertGraph/allowGraph替换为insert(ctx.request.body)。由于insertGraph可能执行多条 SQL示例将其包在Person.transaction中保证原子性graph 插入的底层实现在 lib/queryBuilder/graph/insert/GraphInsert.js 与 GraphInsertAction.js。对应的客户端演示见 client.js会提交一个包含parent、pets数组、movies数组、children数组的嵌套对象这正是 graph insert 的典型载荷。5.2 条件化查询构建GET /personsGET /persons 展示了 objection.js 查询构建器的“可组合”风格——按查询参数动态拼接const query Person.query() if (ctx.query.select) query.select(ctx.query.select) if (ctx.query.name) query.modify(searchByName, ctx.query.name) if (ctx.query.hasPets) query.whereExists(Person.relatedQuery(pets)) if (ctx.query.isActor) query.whereExists(Person.relatedQuery(movies)) if (ctx.query.withGraph) { query .allowGraph([pets, parent, children.[pets, movies.actors], movies.actors.pets]) .withGraphFetched(ctx.query.withGraph) } if (ctx.query.orderBy) query.orderBy(takeFirst(ctx.query.orderBy)) if (ctx.query.withPetCount) query.select(Person.relatedQuery(pets).count().as(petCount)) if (ctx.query.withMovieCount) query.select(Person.relatedQuery(movies).count().as(movieCount)) ctx.body await query要点relatedQuery返回一个“关联子查询”可被whereExists用于存在性过滤也可直接.count().as(petCount)作为标量子查询注入select实现计数列。withGraphFetchedallowGraph是安全地执行 eager loading 的标准组合withGraphFetched指定要贪婪加载的关系表达式allowGraph限制其白名单。这里的白名单表达式[pets, parent, children.[pets, movies.actors], movies.actors.pets]支持嵌套与多级展开。takeFirst工具函数处理ctx.query中参数可能是数组的情况?selectaselectb时 Koa 会给出数组。注释还提示可打开query.debug()查看实际执行的 SQL这是定位查询问题的实用手段。eager loading 的相关实现可查阅 lib/queryBuilder/operations/eager/EagerOperation.js 及其子类JoinEagerOperation、WhereInEagerOperation、NaiveEagerOperation。5.3 更新与删除PATCH / DELETE /persons/:idrouter.patch(/persons/:id, async (ctx) { const numUpdated await Person.query().findById(ctx.params.id).patch(ctx.request.body) ctx.body { success: numUpdated 1 } }) router.delete(/persons/:id, async (ctx) { const numDeleted await Person.query().findById(ctx.params.id).delete() ctx.body { success: numDeleted 1 } })findById(...).patch(...)与findById(...).delete()是 objection.js 提供的最常用便捷链返回受影响行数numUpdated/numDeleted据此判断操作是否命中目标行。5.4 关联查询与关联插入children / pets 端点子资源端点统一使用Person.relatedQuery(xxx).for(id)模式它把后续查询自动限定在该父记录的关系范围内router.post(/persons/:id/children, async (ctx) { const personId parseInt(ctx.params.id) const child await Person.relatedQuery(children).for(personId).insert(ctx.request.body) ctx.body child }) router.get(/persons/:id/children, async (ctx) { const query Person.relatedQuery(children).for(ctx.params.id) if (ctx.query.select) query.select(ctx.query.select) if (ctx.query.name) query.modify(searchByName, ctx.query.name) if (ctx.query.actorInMovie) { const movieSubquery Person.relatedQuery(movies).where(name, ctx.query.actorInMovie) query.whereExists(movieSubquery) } ctx.body await query })其中actorInMovie过滤是子查询优于 join 的典型场景通过Person.relatedQuery(movies)构造“该人出演过的电影”子查询再用whereExists筛选出“出演过指定电影的子女”。代码注释指出子查询不会像 join 那样干扰查询的其他部分是更易维护的选择。宠物端点的GET /persons/:id/pets则直接以where(name, like, ...)与where(species, ...)组合过滤。5.5 多对多的连接与断开relate / unrelaterouter.post(/movies/:movieId/actors/:personId, async (ctx) { const numRelated await Movie.relatedQuery(actors) .for(ctx.params.movieId) .relate(ctx.params.personId) ctx.body { success: numRelated 1 } }) router.delete(/movies/:movieId/actors/:personId, async (ctx) { const numUnrelated await Movie.relatedQuery(actors) .for(ctx.params.movieId) .unrelate() .where(persons.id, ctx.params.personId) ctx.body { success: numUnrelated 1 } })relate(personId)只在persons_movies连接表中插入一行把已存在的演员关联到电影而不创建或修改两侧记录。unrelate()相反删除连接行这里通过.where(persons.id, ctx.params.personId)精确限定要断开的是哪一位演员——注意过滤条件需写成连接表关联的目标模型列persons.id。多对多关系的底层操作实现在 lib/relations/manyToMany 目录下ManyToManyRelateOperation.js、ManyToManyUnrelateOperation.js等。5.6 完整端点清单方法路径objection.js 核心用法POST/personstransactionallowGraphinsertGraphGET/persons条件化select/modify/whereExists/withGraphFetched/ 计数子查询PATCH/persons/:idfindById().patch()DELETE/persons/:idfindById().delete()POST/persons/:id/childrenrelatedQuery(children).for(id).insert()GET/persons/:id/childrenrelatedQuery 子查询whereExistsPOST/persons/:id/petsrelatedQuery(pets).for(id).insert()GET/persons/:id/petsrelatedQuerywhere过滤POST/moviesMovie.query().insert()POST/movies/:movieId/actors/:personIdrelatedQuery(actors).for(id).relate()DELETE/movies/:movieId/actors/:personIdrelatedQuery(actors).for(id).unrelate().where(...)GET/movies/:id/actorsrelatedQuery(actors).for(id)六、client.js一键验证全部端点client.js 是一个可直接运行的 axios 演练脚本按顺序演示了完整业务流程插入带关系的 Matt含父、两只宠物、两部电影、一个子女→ 带过滤器查询所有人select、模糊姓名damo、withMovieCount、withGraph: [pets, children]→ 更新年龄 → 删除子女 → 为 Matt 及其父分别插入子女 → 查询出演过《Good Will Hunting》的子女 → 为子女插入仓鼠宠物 → 按物种过滤查询 → 插入电影 → 关联/断开演员。每一步都通过console.dir(data, { depth: null })打印完整结果跑完一遍即可直观确认模型的全部关系与查询链路正常。其请求均指向http://localhost:8641/与服务端口一致。七、从示例到实战的延伸建议模型定义三件套tableName必需、jsonSchema校验可替换为 doc/recipes/custom-validation.md 描述的自定义校验器、relationMappings用 thunk 防循环依赖。安全边界所有接受客户端输入的关系表达式insertGraph、withGraphFetched都应配合allowGraph白名单这也是官方文档反复强调的实践。事务使用涉及多条写入尤其 graph insert时优先使用Model.transaction()。错误映射基于ValidationError400、ForeignKeyViolationError409等 objection.js 内建异常做统一响应更多场景可参考 错误处理手册。可观测性临时打开query.debug()可打印生成的 SQL便于排查复杂查询生产环境应改用日志集成。八、进一步阅读模型与关系完整 APIModel 静态方法、关系文档查询构建器查询示例、eager 加载 API图操作插入图 与 graph 相关 API同构的 JavaScript 版示例examples/koa与本示例结构一致便于对比 TS 与 JS 的差异更简化的入门示例examples/minimal赞分享数据库后端【免费下载链接】objection.jsAn SQL-friendly ORM for Node.js项目地址https://gitcode.com/gh_mirrors/ob/objection.js点击查看免费下载相关推荐Moment Timezone 在 TypeScript 中的完整应用类型定义与实战示例Moment Timezone 在 TypeScript 中的完整应用类型定义与实战示例 Moment Timezone 作为 Moment.js 的重要插件后端终极Kiln API接口使用手册完整REST API参考与实战示例终极Kiln API接口使用手册完整REST API参考与实战示例 Kiln是一个功能强大的AI系统构建平台提供了全面的REST API接口让开发者能够轻AI 技能科研AI 评测人工智能Objection.js 模型系统从基础定义到高级特性Objection.js 模型系统从基础定义到高级特性 本文深入探讨了Objection.js ORM框架的模型系统从基础定义到高级特性全面解析。文章首先介数据库后端上一篇gh_mirrors/vag/vagas职位筛选技巧快速找到符合期望的后端工作下一篇scrcpy 快捷键完全指南窗口操作、屏幕控制与剪贴板同步的底层实现解析创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表