值得一试的 TypeORM

TypeScript 是一门静态类型语言, 这是我认为的其最大的特点. 在使用 TypeScript 编写的项目中, 应该尽可能使用由 TypeScript 编写的类库或框架.

在传统的使用 JavaScript 编写的 Node.js 后端项目中,比较有名的两个 ORM/ODM 是 Sequelize 和 Mongoose, 但他们对 TypeScript 的支持是很差的, 并不说运行不了, 但是无论是写法还是静态检查上面都很弱. Sequelize/Mongoose 大量使用了对象和函数, 而不是类. 举个例子, 在 model 的定义上, 需要单独为每个 model 在写一次 interface, 十分不方便.

TypeScript 的一个特点之一是类可以当做接口使用, 因此如果可以用类的方式描述 model, 结合装饰器, 通过反射得到数据类型, 可以极大程度方便 model 的书写.

所以我很快就改用了 TypeORM. 到现在也用了很久了, 也踩了不少坑, 我也参与了部分讨论, 贡献了部分代码. 直到上个月, 它才发布了 0.1.0 版本, 截止目前它在 GitHub 上有 3704 个 star.

事实上 TypeORM 的文档已经非常齐全非常友好, 传送门: http://typeorm.io/#/ . 这篇文章是想说一说 TypeORM 那些我觉得很不错的功能, 并结合之前我所使用过的 Sequelize, 谈谈他们的一些区别, 以及我使用 TypeORM 的一些感受.

TypeORM 简介

TypeORM 是一个 Node.js ORM 框架, 采用 TypeScript 编写, 可以运行在 NodeJS, Browser, Cordova, PhoneGap 和 Ionic 等平台上. TypeORM 同时支持 Active Record 和 Data Mapper 两种模式, 可以轻松创建出松耦合, 可伸缩, 可维护的应用. TypeORM 深受 Hibernate, Doctrine 和 Entity Framework 等 ORM 的影响.

官方 README.md 指出了很多 feature, 可以简单瞧一下:

  • supports both DataMapper and ActiveRecord (your choice)
  • entities and columns
  • database-specific column types
  • entity manager
  • repositories and custom repositories
  • clean object relational model
  • associations (relations)
  • eager and lazy relations
  • uni-directional, bi-directional and self-referenced relations
  • supports multiple inheritance patterns
  • cascades
  • indices
  • transactions
  • migrations and automatic migrations generation
  • connection pooling
  • replication
  • using multiple database connections
  • working with multiple databases types
  • cross-database and cross-schema queries
  • elegant-syntax, flexible and powerful QueryBuilder
  • left and inner joins
  • proper pagination for queries using joins
  • query caching
  • streaming raw results
  • logging
  • listeners and subscribers (hooks)
  • supports closure table pattern
  • schema declaration in models or separate configuration files
  • connection configuration in json / xml / yml / env formats
  • supports MySQL / MariaDB / Postgres / SQLite / Microsoft SQL Server / Oracle / WebSQL / sql.js
  • supports MongoDB NoSQL database
  • works in NodeJS / Browser / Ionic / Cordova / Electron platforms
  • TypeScript and JavaScript support
  • produced code is performant, flexible, clean and maintainable
  • follows all possible best practices
  • CLI

TypeORM 几个很棒的功能

说几点我觉得 TypeORM 很棒的功能:

  1. 支持 MongoDB

    虽然 MongoDB 部分使用的函数是和关系型数据库不一样的, 但总的而言, 统一在一个 ORM 里面来操作, 总是比用两个好.

  2. 支持 Migration

    Migration 是操作数据库表的利器, 然而似乎 Node.js 并没有这个传统. Sequelize 虽然也支持 Migration, 但是写法就很奇怪... 这可能是函数和对象用的太多的原因... 也可能 Sequelize 的设计原则之一就是避免手写 SQL 吧...

    并且 Sequelize 的 Migraiton 要手动书写. 而 TypeORM 可以做到自动生成. 通过 TypeORM CLI 可以检测当前 model 文件和数据库差异, 自动生成 migration 文件.

    对比下 Sequelize 和 TypeORM 的 migration 文件:

    // Sequelize migration
    module.exports = {
      up: (queryInterface, Sequelize) => {
        return queryInterface.createTable('Person', {
            name: Sequelize.STRING,
            isBetaMember: {
              type: Sequelize.BOOLEAN,
              defaultValue: false,
              allowNull: false
            }
          });
      },
      down: (queryInterface, Sequelize) => {
        return queryInterface.dropTable('Person');
      }
    }
    
    // TypeORM
    import { MigrationInterface, QueryRunner } from 'typeorm'
    
    export class UpdateGroupId1510118119686 implements MigrationInterface {
      async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query('ALTER TABLE `user` CHANGE `groupid` `groupid` int(11) NOT NULL')
      }
    
      async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query('ALTER TABLE `user` CHANGE `groupid` `groupid` smallint(6) NOT NULL')
      }
    }
    
    

    emmmm.. 这个哪个看起来比较好应该不用多说吧. TypeORM 的这个 migration 文件是自己生成的, 不需要自己写.

  3. Lazy relation

    这个功能不知道 Sequelize 有没有, TypeORM 是近期才加上的. 我觉得这个功能挺新鲜的.

    const question = await connection.getRepository(Question).findOneById(1);
    const answers = await question.answers;
    // you'll have all question's answers inside "answers" variable now
    

    作用应该就是避免不必要的查询, 需要的时候自己再去 await 这样. Java 和 PHP 都有类似的实现. 我还没有去使用过, 因为上次看文档还没有见过这个0.0

  4. 日志

    TypeORM 的日志功能是非常完善的. Sequelize 我不知道能不能做到类似的, 挺久没用的了, 而且 Sequelize 的文档我还没看完... 说回来 TypeORM.

    TypeORM 支持打印多种 Level 的日志, 就是说你可以指定打印 warn 或者 error 级别的错误, 也可以把 migration 的日志也打印, 取决于你传的数组里面有哪些值.

    另外, 它还支持打印超过 xxx ms 的执行语句. 通过配置 maxQueryExecutionTime 可以实现.

    同时, 它还支持自定义 Logger. 支持 advanced-console (chalk), simple-console, file 三种.

  5. cache 功能

    只要在 TypeORM 配置里面加入 cache: true , 就可以缓存 getMany, getOne, getRawMany, getRawOne, getCount 等请求.

    接着还要在查询的时候加入一行 cache(true)

    const users = await connection
        .createQueryBuilder(User, "user")
        .where("user.isAdmin = :isAdmin", { isAdmin: true })
        .cache(true)
        .getMany();
    

    也可以自己指定时间:

    const users = await connection
        .getRepository(User)
        .find({
            where: { isAdmin: true },
            cache: 60000
        });
    

除了以上五点, 还有像:

  1. 事务可以使用一条 Promise 链来写, 也可以使用装饰器直接包装一个函数
  2. Hooks 支持完善
  3. 多数据库连接
  4. ....

而且源码也写的很不错, 注释齐全. 作者连 migration from Sequelize to TypeORM 都写好了, 你真的不瞧一瞧吗... 当然, JavaScript 的话有 babel 加持会好一点. 最好的还是搭配 TypeScript ~

这里说的都只是 TypeORM 的一小部分, 更多的就去看下文档或者自己真正去尝试下吧 ~

TypeORM 的使用感受

在之前用 JavaScript 写 Node.js 后端的时候, 大多都是使用 Sequelize. Sequelize 是一个非常好用的框架, 这点毋庸置疑. 其易用程度, 可以让很多人可以完全不懂 SQL 就可以进行花式查询. 我使用 TypeORM 后第一个不适点就是 TypeORM 只提供了简单的基本查询, 很多时候还是需要自己去通过 querybuilder 的形式去写 SQL 的关键部分.

比如, Sequelize 可以很方便的进行 increase 和 decrease 操作, 但是在 TypeORM 是没有哪个函数可以让你方便做到这一点的, 我当初还提了个 issue 去询问如何操作. 最后得到的解决方案是自己手写 SQL 语句, 虽然还有其他方案, 但是所谓的其他方案并没有手写 SQL 方便...

await entityManager.query(`UPDATE \`oppty\` SET count=count+${change} WHERE activityId=${activityId} AND userId=${userId}`)

emmmm.... 所以刚使用 TypeORM 的时候, 由于自己的 SQL 知识基本都还给老师了, 比较少用到(因为 Sequelize 实在太方便了...), 所以写的时候遇到很多困难, 但一定程度促使我去加深了很多数据库知识, 包括一些常用 SQL 语句和关键字, 索引等.

TypeORM 的索引, 关联定义很方便, 不像 Sequelize. 如果你用过 Sequelize, 你一定会喜欢 TypeORM 的写法:

@Entity()
@Index(['category'])
export class Article {
  @PrimaryGeneratedColumn() id: number

  /** 标题 */
  @Column() title: string

  /** 内容 */
  @Column('text') content: string

  /** 概要 */
  @Column() summary: string

  /** 缩略图 */
  @Column() thumbnail: string

  /** 创建时间 */
  @CreateDateColumn() createdAt: string

  /** 更新时间 */
  @UpdateDateColumn() updatedAt: string

  /** 原文链接 */
  @Column() sourceUrl: string

  /** 推送到头条的时间 */
  @Column('datetime', { nullable: true })
  headlineTime?: Date

  /** 用户 ID */
  @Column({ nullable: true })
  uid?: number

  /** 分类 ID */
  @Column() categoryId: number

  @OneToOne(type => User)
  @JoinColumn({ name: 'uid' })
  user?: User

  @OneToOne(type => Category)
  @JoinColumn({ name: 'categoryId' })
  category: Category

  @OneToMany(type => Comment, comment => comment.article)
  comments: Comment[]

  @ManyToMany(type => Game, game => game.articles)
  @JoinTable()
  games: Game[]

  @OneToMany(type => ArticleReadersUser, item => item.article)
  readers?: ArticleReadersUser[]

  @RelationCount((article: Article) => article.readers, 'readers', qb =>
    qb.andWhere('readers.like=:like', { like: true })
  )
  likeCount: number

  @RelationCount((artile: Article) => artile.comments)
  commentCount: number


  @BeforeInsert()
  insertSummaryAndThumbnail(): void {
    this.summary = this.summary || this.content.replace(/<[^>]+>/g, '').slice(0, 120)
  }

  @BeforeUpdate()
  updateSummaryAndThumbnail(): void {
    this.summary = this.summary || this.content.replace(/<[^>]+>/g, '').slice(0, 120)
  }
}

需要说明下的是, 并不是所有 Entity 上面定义的东西都会转换为数据库真实的表上面, 只有 @Column() 或者 @XXXColumn() 之类的才会有这个作用. 这里有几个比较特殊的装饰器:

  • @Index

    定义索引, 很方便有木有

  • @CreateDateColumn@UpdateDateColumn

    Sequelize 会默认在每个表加上创建时间和更新时间, 在 TypeORM 需要自己通过这两个装饰器来手动创建

  • @OneToOne, @OneToMany, @ManyToMany

    都是定义关联用的, @JoinColumn 表示外键放在这里, 如果不自己手动指定的话, TypeORM 会自己创建 xxxId 出来, 但为了保持接口和表一致, 建议把外键写进去, 然后手动指定下(如果也叫 xxxId 的话似乎可以省略). @JoinTable 稍微有点区别, 不过这里不多介绍了.

  • @RelationCount

    这个是 TypeORM 中我也比较喜欢的一个功能, 作用应该不需要解释吧. 不过这个功能我用的时候有 Bug, 我修复了这个 BUG 提交了 PR, 不过作者跟我说这个功能实现起来比较复杂, 他打算废弃掉了... 最后我跟他说那行, 不过好歹也是 Fixed 合并先吧 0.0, 然后就合并过去了.

  • @BeforeInsert@BeforeUpdate()

    这个不用解释吧 0.0

Sequelize 需要将关联和表定义分散开来, 而 TypeORM 则直接通过类和装饰器的方式组织在一起了.

数据库是后端的一个很重要的部分, 后端开发人员应该对数据库有一定的认识, 不能太过依赖 ORM 来完成实现. 在使用 TypeORM 的过程中, 我也经常自己手写 SQL 语句, 目的是为了利用索引来加快一些操作, 比如典型的覆盖索引, 如果依赖 ORM 的话, 事实上我并不知道 ORM 会生成怎样的 SQL 语句出来(TypeORM 你可以使用一个函数得到它转换的 SQL 语句), 加之要写的 SQL 语句比较简单, 所以一般我会选择手写.当然手写 SQL 还有一个问题就是要避免 SQL 注入, 所以一般我会从参数校验以及使用 Parameter 的方式.

TypeORM 目前还是 0.1.0 版本, 但我一直在线上环境使用, 之前 0.0.x 的时候遇到过好几个 BUG, 目前暂时没有遇到了. TypeORM 的代码写的很好, 注释齐全, 如果想学习下 ORM 怎么实现, 学习一下 TypeScript 之类的, 从 TypeORM 入手或许也是一个不错的选择~