宣布新的打字稿ORM
#typescript #node #database #orm

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正在以不同的方式处理:

  • 仅公开一个有限的接口(PrismaSequelize),因此当您需要对查询的更多控制时,它们变得无法使用。

  • 在有限的接口和查询构建器之间切换(MikroORMTypeORM)。感觉就像使用两个不同的库,在两组不同的限制集之间切换。 Kysely是一个不错的查询构建器,具有良好的TS支持,但是Mikroorm改为使用Knex,因此您正在失去TS,而TypeORM的自定义查询构建器比Knex较不友好。

  • 建立在查询构建器顶部(ObjectionOrchidORM) - 这样,查询感觉很自然并且保持强大。

基于查询构建器的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,它允许在自定义方法下隐藏复杂或重复的零件。在下面的示例中,searchcustomOrder是在其他地方定义的自定义方法,您可以构建漂亮的易于阅读的查询:

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不友好,需要危险的类型铸件。

MikroORMTypeORM的有限接口不足以解决此查询,因此,如果使用它们,我们必须切换到查询构建器。

但是,查询构建器甚至怎么可能?使用MikroORMTypeORMKnexKysely进行此类查询,这将花费更多的时间和精力,从而产生真正可怕的东西。我们可能最终会得到一个单独的查询,然后在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的基准和其他基准测试。

编写模型

如果您曾经使用过ZodYupJoi等,则定义OrchidORM中的列看起来很熟悉。它比单独定义类型(Sequelize)或使用打字稿装饰器和!如TypeORMMikroORM中的符号,它不需要像Prisma中的每个模式更改上生成代码。

可以将表模式转换为Zod以稍后用于验证。

默认情况下需要所有列(不可撤销)。

text类型需要最小的和最大的参数,因此我们的表受到空白或十亿个字符的保护。所有列类型都可以在BaseTable中自定义,这可以用于将所有列的通用最小和最大设置在一起。

支持的关系类型为belongsTohasOnehasManyhasAndBelongsToMany

hasOnehasMany通过中间表的选项支持。

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代码。

如果您喜欢的话,请不要忘记饰演该项目,并分享反馈。

对您来说看起来很有趣吗?您是否同意现有工具不够好?