node.js已经有很多orms。
-
类型安全
-
灵活性
-
更复杂的情况的使用
-
性能
这就是创建Orchid ORM的原因 - 回答所有这些挑战!
在创建一个新的ORM之前,我检查了5个顶级流行node.js orms和2个查询构建者,以找出是否有“ go to”解决方案,我可以安全地选择任何项目,而无需担心我正在寻找的这样的工具。
在这篇文章中,我将介绍不同的ORM和查询构建者如何回应期望,以及Orchid
可以提供的。
RAW SQL与查询构建器与ORMS
但是,为什么我们根本需要ORMS,不是RAW SQL或查询构建器我们需要的一切?
它们在不同级别的抽象上运作:
-
RAW SQL意味着手动编写SQL,根本没有抽象。这使得很难使用动态查询。想象一下,您正在为获取帖子列表构建端点,并且客户端可以发送各种参数:如何订购,过滤,分页等。 RAW SQL都是可能的,但是您最终会得到大量的代码,除非非常仔细地进行,否则看起来很混乱,并且很容易受到SQL注射的影响。
-
查询构建器解决了动态查询的问题,但并没有抽象关系。当您在两个表上进行连接时,如果左表中的一个记录在右侧有许多记录,这将导致左表数据的重复。通过使用组,或使用子查询或通过在JS侧删除数据,这是可以解决的,但这很麻烦。此外,如果关系很复杂(涉及多个表),则可能需要在各个地方使用它,它会导致复制相当复杂的代码零件。
-
ORMS解决了上述问题,但通常会因生成效率低下的代码和不灵活而受到批评,而RAW SQL和查询构建者则更好。兰花Orm的目标是证明Orm可以将查询构建器和ORMS的所有好处组合在一起。
类型安全
我相信Typescript是JavaScript发生的最好的事情。如果在更改某些东西时要非常小心之前,我们必须将所有内容保持在我们的脑海中,并搜索某些对象相应地更新代码的所有地方,现在我们有了TS的奢侈指向我们需要的地方更新代码。
理想情况下,ORM必须能够:
-
选择特定列时保留正确的类型
-
知道包含哪些关系
-
不同类型的不同操作可用于不同类型:“低于”和“大于“可用于数字类型”,“包含”和“开头”的“可用于文本类型”等。
。 -
将原始语句混合到查询中,并能够指定其类型
大多数ORM始终将完整记录作为返回类型,而忽略选择列的子集。这是通过在OOP启发的ORM中进行设计完成的,其中您具有“实体”,并且必须始终充满负载。因此,对于某些人来说,这一点可能不是问题,而是一种有效且首选的方法。但是对我来说,理想的ORM允许您仅选择所需的内容并保留正确的类型。在Node.js中的所有流行ORM中,只有Prisma都能很好地实现此目的。
知道包括哪些关系:慢慢但稳定的事情正在改善,有些ORMS正在获得这种能力。 MikroORM
去年获得了,Sequelize
具有积极开发的Alpha版本,它可能可以做到这一点。
将原始语句混合到具有指定其类型的查询中:据我所知,没有一个ORM可以做到这一点。
让我分享一个用例,以演示兰花Orm如何从上方解决这些点:
想象两个桌子,帖子和喜欢。用户可以喜欢帖子。
我们想加载具有特定字段的多个帖子(点#1)。
如果这是授权请求,我们要加载布尔值,无论当前用户是否喜欢该帖子(包括相关表的数据,点#2)。
如果未授权此请求,我们没有currentUserId
,因此我们选择一个常数false
而不是相关表(点#4)。这里的t.boolean()
在这里指示SQL表达式的返回类型。
通过标题包含一个单词显示#3的过滤帖子。
const result = await db.post.select(
'title',
'body',
{
liked: currentUserId
? (q) => q.likes.where({ userId: currentUserId }).exists()
: db.article.raw((t) => t.boolean(), 'false'),
},
).where({
title: {
contains: 'word',
},
});
最终的类型是完全推断的,并且等于:
type Result = {
title: string,
body: string,
liked: boolean
}[]
灵活性
除了他们没有帮助,查询构建者的灵活性非常出色,他们被允许使用嵌套的子查询来构建复杂的查询。
orms正在以不同的方式处理:
-
仅公开一个有限的接口(
Prisma
,Sequelize
),因此当您需要对查询的更多控制时,它们变得无法使用。 -
在有限的接口和查询构建器之间切换(
MikroORM
,TypeORM
)。感觉就像使用两个不同的库,在两组不同的限制集之间切换。 Kysely是一个不错的查询构建器,具有良好的TS支持,但是Mikroorm改为使用Knex,因此您正在失去TS,而TypeORM
的自定义查询构建器比Knex
较不友好。 -
建立在查询构建器顶部(
Objection
,OrchidORM
) - 这样,查询感觉很自然并且保持强大。
基于查询构建器的ORM允许逐步构建查询,并根据条件添加零件。 Orchid ORM
示例代码:
let q = db.post.select('id', 'title')
if (params.search) {
q = q.or([
{ title: { contains: params.search } },
{ body: { contains: params.search } },
]);
}
if (params.order === 'newer') {
q = q.order({ createdAt: 'DESC' })
} else if (params.order === 'older') {
q = q.order({ createdAt: 'ASC' })
}
const posts = await q
Orchid ORM
具有一个自定义查询构建器,专门设计为对打字稿尽可能友好。查询构建器这里的灵感来自Knex
,并支持所有相同的查询方法(以及更多)。
为了使事情变得更加干净,Orchid ORM
具有一个repository feature,它允许在自定义方法下隐藏复杂或重复的零件。在下面的示例中,search
和customOrder
是在其他地方定义的自定义方法,您可以构建漂亮的易于阅读的查询:
const posts = await postRepo
.search(params.search)
.customOrder(params.order)
对更复杂的情况的使用易用性
假设我们要加载帖子记录,包括帖子作者,标签和最后一些评论,评论应包括作者。这是Orchid
的外观:
await db.post
.select('id', 'title', 'description', {
author: (q) => q.author.select('id', 'firstName', 'lastName'),
tags: (q) => q.postTags.pluck('tagName'),
lastComments: (q) =>
q.comments
.select('id', 'text', {
author: (q) =>
q.author.select('id', 'firstName', 'lastName'),
})
.order({ createdAt: 'DESC' })
.limit(commentsPerPost),
})
.order({ createdAt: 'DESC' });
14行代码。
Prisma
也是如此,我花了48行代码(source),而且看起来很干净,但是需要绘制prisma结果,因为Prisma不支持您在查询中所需的命名字段,因此我们可以't告诉Prisma将评论加载为“ lastcments”,但这可能是我们的API规格所要求的。
Sequelize
代码的同一查询(source)与Prisma
的代码相似,只是Sequelize
不友好,需要危险的类型铸件。
MikroORM
和TypeORM
的有限接口不足以解决此查询,因此,如果使用它们,我们必须切换到查询构建器。
但是,查询构建器甚至怎么可能?使用MikroORM
,TypeORM
,Knex
,Kysely
进行此类查询,这将花费更多的时间和精力,从而产生真正可怕的东西。我们可能最终会得到一个单独的查询,然后在JS侧结合记录。并且需要仔细地执行此操作,以免引入N + 1
问题。让我知道我是否错了,我很想在这个问题上错过,看看如何正确地使用查询构建器。
表现
过早的优化是邪恶的,因此性能不如其他特征重要,但在某些情况下,它仍然至关重要。
构建了OrchidORM
的初始版本是为了测试这个想法:如果Postgres可以通过子查询在其末端包含嵌套资源,并将其返回为JSON列,为什么不使用单个ORM使用此词?以这种方式效果会有效吗?
sql示例要演示这种方法:加载帖子,带有json的评论阵列:
SELECT
*,
(
SELECT json_agg(t.*)
FROM (
SELECT * FROM comments WHERE postId = posts.id
) AS t
) AS comments,
FROM posts
这就是OrchidORM
处理引擎盖下的关系的方式,因此所有嵌套的选择查询都变成一个SQL查询。
Prisma
将每个新关系加载为新查询,然后在JS侧结合结果。
Sequelize
与加入一起产生巨大的查询。
OrchidORM
是与其他ORM甚至查询构建者相比最快的,请参见here的基准和其他基准测试。
编写模型
如果您曾经使用过Zod
,Yup
,Joi
等,则定义OrchidORM
中的列看起来很熟悉。它比单独定义类型(Sequelize
)或使用打字稿装饰器和!如TypeORM
和MikroORM
中的符号,它不需要像Prisma
中的每个模式更改上生成代码。
可以将表模式转换为Zod
以稍后用于验证。
默认情况下需要所有列(不可撤销)。
text
类型需要最小的和最大的参数,因此我们的表受到空白或十亿个字符的保护。所有列类型都可以在BaseTable
中自定义,这可以用于将所有列的通用最小和最大设置在一起。
支持的关系类型为belongsTo
,hasOne
,hasMany
,hasAndBelongsToMany
。
hasOne
和hasMany
通过中间表的选项支持。
export class ArticleTable extends BaseTable {
table = 'article';
columns = this.setColumns((t) => ({
id: t.serial().primaryKey(),
userId: t.integer().foreignKey('user', 'id').index(),
title: t.text(10, 200),
body: t.text(100, 100000),
favoritesCount: t.integer(),
...t.timestamps(),
}));
relations = {
author: this.belongsTo(() => UserTable, {
primaryKey: 'id',
foreignKey: 'userId',
}),
};
}
// convert columns schema to Zod, use it later for validations
export const ArticleSchema = tableToZod(ArticleTable);
什么是捕获?
-
此时仅支持Postgres
-
不可能在不同数据库中的表之间建立关系(将来要完成)
-
尽管它在演示项目上有效,但对于生产而言,它太绿色了
-
尽管它包含了knex的所有方法,并以prisma风格插入/更新,但要完成许多功能
-
它可以从现有数据库中生成迁移,它可以从运行迁移中生成表文件。但是,从表模式(如Prisma中)生成迁移或即时更新数据库模式(如许多ORMS中)。
兰花的摘要
-
类型安全性是最高优先级
-
查询构建器接口启用编写复杂的自定义查询
-
创建和更新嵌套的记录与Prisma
中一样强大
-
类似ZOD的定义列比其他列更简单
-
关心性能
试试看!
我希望您能欣赏OrchidORM
并在非关键个人项目上尝试并分享反馈,以便ORM可以成长和繁荣。
为了加快设置,我添加了一个脚本以自动进行所有例程准备,只需在新目录中运行此脚本,然后查看quickstart:
npx orchid-orm
或者您可以克隆this examples repo并使用博客API代码。
如果您喜欢的话,请不要忘记饰演该项目,并分享反馈。
对您来说看起来很有趣吗?您是否同意现有工具不够好?