Orchid ORM 是一款专门为 Node.js 设计的数据库交互库,尤其适用于与 Postgres 数据库进行高效协作。其核心优势在于,它提供了高度灵活且类型安全的查询构建器,能够帮助开发者轻松定义和组合涉及表关系的复杂数据库查询。
Orchid ORM 围绕四大核心目标进行设计:强大的功能、简洁直观的操作、卓越的性能以及完全的类型安全。其查询构建器 pqb 虽借鉴了 knex 的设计理念,但却是完全基于 TypeScript 重新开发的产物。通过精确的列模式定义和类型推断机制,Orchid ORM 确保在所有的查询操作过程中,都能实现严格的类型安全。
与其他采用 OOP 风格、依赖 Active Record 模式的 ORM 不同,Orchid ORM 有着自己独特的实现方式 。它通过对列模式进行清晰定义,并在所有查询方法中运用推断类型,从而实现了类型安全这一重要特性。
// 在其他 ORM 中,post 是类 Post 的实例
const post = await Post.findBy({ id: 123 });await post.update({ title: 'new title' });
由于 Orchid ORM 的设计目标与其他 ORM 存在差异,因此它将查询结果以普通对象的形式返回。
// 在 Orchid ORM 中,post 是一个普通对象
const post = await db.post.findBy({ id: 123 });await db.post.update(post, { title: 'new title' });
这种方法支持以嵌套形式选择表关系,执行自定义子查询操作,并且能够全程保障所有操作及数据的类型安全性。
// post 类型完全推断
const post = await db.post
.find(123)
.select('title', 'body', {
likesCount: (q) => q.likes.count(),
comments: (q) =>
q.comments
.order({ createdAt: 'DESC' })
.limit(50)
.select('body', {
author: (q) => q.author.select('avatar', 'username'),
}),
})
// 可以根据选定的点赞数量进行过滤和排序
.where({ likesCount: { gt: 100 } })
.order({ likesCount: 'DESC' });
查询构建器具备极为灵活的功能,它允许用户借助关系和条件来链接查询。
举个例子,若要筛选出带有两个特定标签的帖子,该查询构建器就能发挥作用。
const posts = await db.post.where((q) =>
q.tags.whereIn('tagName', ['typescript', 'node.js']).count().gte(2),
);
在子查询中,关系是可以相互链接的。比如,我们可以通过这种链接方式,为每个帖子收集到所有评论者的名字所组成的数组。
const posts = await db.post.select({
// `pluck` 收集一个普通数组
commentedBy: (q) => q.comments.author.pluck('username'),
});
自定义 SQL 能够灵活地注入到查询的任意位置。同时,系统会对插入的值进行妥善处理,有效防范 SQL 注入风险,确保数据操作的安全性。
import { sql } from './baseTable';
const posts = await db.customer
.select({
upper: sql<string>`upper(title)`,
})
.whereSql`reverse(title) = ${reversedTitle}`
.orderSql`reverse(title)`
.havingSql`count("someColumn") > 300`;
在着手开发 Orchid ORM 之前,深入研究了市面上现有的 ORM 工具,并撰写了相关文章。最终发现,没有一款 ORM 能够完全契合 TypeScript 语言驱动的 Node.js 项目的典型需求。正是出于对更适配解决方案的迫切需求,Orchid ORM 应运而生。
当现有的 ORM 工具都让人感觉束缚重重、使用混乱时,开发者或许会考虑转向查询构建器或原始 SQL。然而,这两种替代方案也存在明显缺陷:
-
原始 SQL:编写动态查询的难度较大且容易出错。尤其当查询结构依赖用户输入参数时,SQL 语句的拼接极易产生混乱,增加代码维护成本与潜在风险。
-
查询构建器:缺乏对表间关系的内在理解。以查询包含评论的帖子为例,每次都需手动编写连接查询,若涉及多层关系(如 3 层、4 层甚至 10 层连接),开发过程将变得极为繁琐。并且,在创建或更新相关记录时,必须单独编写多个查询并手动包装在事务中,而 ORM 工具则能自动完成这些复杂操作 。
此外,其他 ORM 在模型定义方式上也存在各自的局限性:
- Prisma:使用专属语言定义数据模式,每次模式变更后,都需要重新编译为 TypeScript 代码,影响开发效率。
- Sequelize:最初为 JavaScript 设计,若要在 TypeScript 项目中使用,需编写大量样板代码,开发体验不够流畅。
- Objection:同样基于 JavaScript 构建,无法充分发挥 TypeScript 的优势,在编写查询时,无法通过自动完成功能检查关系名称或列名,容易引发类型相关错误。
- TypeORM 和 MikroORM:其模型依赖实验性的 TypeScript 装饰器,并且需要特定的 TypeScript 配置环境,增加了项目配置的复杂性与不稳定性。
- DeepKit:对 TypeScript 编译器进行了大幅修改,这种深度定制可能与现有项目生态难以兼容,且存在潜在的兼容性风险。
使用 Orchid ORM 进行表类编写时,具有简洁且类型安全的特点。以下是一个具体的 User 表类示例:
export type User = Selectable<UserTable>;
export class UserTable extends BaseTable {
readonly table = 'user';
columns = this.setColumns((t) => ({
id: t.identity().primaryKey(),
name: t.string(), // `string` 是 varchar,默认限制为 255
password: t.varchar(50), // 最大 50 个字符
// 添加 createdAt 和 updatedAt,带默认值:
...t.timestamps(),
}));
relations = {
// 用户有一个 Profile,user.id -> profile.userId
// 还有 belongsTo, hasMany, hasAndBelongsToMany
profile: this.hasOne(() => ProfileTable, {
required: true,
columns: ['id'],
references: ['userId'],
}),
};
}
在上述代码中,通过扩展 BaseTable 类来定义 UserTable。在 columns 部分,利用 setColumns 方法清晰地定义了表的各列属性,如主键 id、字符串类型的 name、特定长度限制的 password 以及自动生成的时间戳字段。而在 relations 部分,通过 hasOne 方法明确了与 ProfileTable 的一对一关系,并且可以方便地设置关系的相关参数。同时,Orchid ORM 不需要额外的语言、重新编译过程,也无需装饰器以及对 TS 编译器进行特殊调整,并且完全保证了类型安全性。当涉及到自定义查询时,不同的 ORM 暴露出各自的问题:
- Prisma:在 Prisma 中,只要 WHERE 语句的一小部分需要使用官方不支持的自定义 SQL 片段,就不得不将整个查询重写为原始 SQL。这无疑增加了开发的复杂性和工作量,并且可能破坏原有的查询逻辑。
- Sequelize:Sequelize 的结果类型始终返回完整记录,即使在只选择特定列的情况下,其类型系统也无法准确判断是否包含了关系。这可能导致在处理结果时出现类型不匹配的问题,增加了代码的维护难度。
- TypeORM 和 MikroORM:这两者为简单查询提供了非常有限的 ORM 接口(存在与 Sequelize 类似的问题),对于更复杂的查询则依赖查询构建器。然而,它们在类型安全性方面存在不足,无法像 Orchid ORM 那样提供可靠的类型检查。虽然 MikroORM 在较新版本中开始支持部分结果类型,但在访问嵌套记录时采用了类似 jQuery 的语法,这对于熟悉常规 TypeScript 语法的开发者来说可能不太友好。
- Objection:Objection 虽然在编写查询方面相对容易,但它缺乏类型安全性。这意味着在开发过程中,开发者需要花费更多的精力来确保查询的正确性,容易出现运行时错误。
综上所述,Orchid ORM 在表类编写和自定义查询的类型安全性方面具有明显优势,为 Node.js 项目中与 Postgres 数据库的交互提供了更可靠、高效的解决方案。
Orchid ORM 在数据库查询方面展现出显著优势,它有效地规避了其他 ORM 工具所面临的问题,尤其擅长构建复杂的关系查询,并且能够精准地跟踪所有类型信息。
例如,对于数据类型为 Array<{ id: number, name: string, authorName: string, commentsCount: number }> 的 posts 数据获取,Orchid ORM 的查询方式如下:
// posts 类型将是:Array<{ id: number, name: string, authorName: string, commentsCount: number }>
const posts = await db.post
// .join 允许仅指定在 Post 表中定义的关系名称
.join('author')
// .select 自动完成并检查 Post 列
.select('id', 'name', {
// 选择 "author.name" 作为 "authorName"
// 'author.name' 只有在连接 'author' 后才可选择,否则编译错误
authorName: 'author.name',
// 选择帖子评论的数量:
commentsCount: (q) => q.comments.count(),
});
在上述代码中,.join 方法允许开发者仅指定在 Post 表中已定义的关系名称,从而清晰地构建表之间的关联。而 .select 方法不仅能够自动完成操作,还能对 Post 表的列进行检查。例如,选择 author.name 作为 authorName 时,只有在正确连接 author 关系后才能进行选择,否则会产生编译错误,这大大增强了类型安全性。同时,通过 commentsCount: (q) => q.comments.count() 能够准确获取帖子的评论数量,体现了其在复杂查询方面的灵活性。
此外,Orchid ORM 还支持定义自定义可链接方法(通过 repository),以便编写简洁清晰的抽象查询。例如:
const posts = await postRepo
.selectForList()
.search('word')
.filterByTags(['tag 1', 'tag 2'])
.orderByPopularity()
.limit(20);