TypeORM 第一弹

TypeORM是Node平台一个TypeScript优先的成熟ORM框架,借鉴了Hibernate,Doctrine等ORM框架,我觉得我们可以拥抱这项技术,让我们的代码更加规范和优雅。

和很多的ORM框架一样,TypeORM也有Entity(实体),Relations(实体关系),EntityManager/Repository(实体管理和数据仓库),QueryBuilder等概念。我们逐个分析一下:

Entity

在TypeORM中实体是程序对数据库表的描述,我们可以通过实体在数据库中生成对应的数据表。除了基本的用法之外,TypeORM关于实体的几个特性十分有用:

1. 嵌入式实体(Embedded Entity)

这个特性可以帮助我们复用字段,比如如果我们在User表,Student表及Employee表中有大量相同的字段,这时我们可以将这些相同的字段抽取出来形成嵌入式实体。

2. 实体继承(Entity Inheritance)

这时除了嵌入式实体之外,另一种复用手段,这种方式有两种继承方法,一种是利用语言特点将可复用的字段抽取为抽象类,然后继承。另一种是使用框架本身的能力,只在数据库中建立一张表,所有实体均在存储在该表中,并通过一个字段对实体类型加以区别。下面就是一个例子:

import { Entity, PrimaryGeneratedColumn, Column, TableInheritance } from 'typeorm';

@Entity('T_CONTENT')
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export class Content {
 @PrimaryGeneratedColumn({ type: 'bigint' })
 id: number;
 @Column({
   length: 100
 })
 name: string;

 @Column({
   type: 'text'
 })
 description: string;
} 
import { Column, ChildEntity } from 'typeorm';
import { Content } from './Content';

@ChildEntity()
export class Photo extends Content {
 @Column()
 filename: string;

 @Column('double')
 views: number;

 @Column()
 isPublished: boolean;
} 
import { Content } from './Content';
import { ChildEntity, Column } from 'typeorm';

@ChildEntity()
class Post extends Content {
 @Column('int')
 viewCount: number;
} 

而这三个实体在数据库中生成的表格也只有一个 T_CONTENT

+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name        | varchar(100) | NO   |     | NULL    |                |
| description | text         | NO   |     | NULL    |                |
| filename    | varchar(255) | YES  |     | NULL    |                |
| views       | double       | YES  |     | NULL    |                |
| isPublished | tinyint(4)   | YES  |     | NULL    |                |
| type        | varchar(255) | NO   | MUL | NULL    |                |
| viewCount   | int(11)      | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+ 

所有实体的字段都会在同一张表上,这样的设计模式也不鲜见。

3. 树形实体

我们有时会存储树形数据,例如多级分类,部门组织架构等。常用的方法有邻接表,嵌套集,物化路径,闭合表。TypeORM对于这四种方法都做了支持。

领接表我们暂且不提,通过关联关系可以轻松做到,但其缺点也很明显,它无法查询层级太深的树(外接有限制)。

物化路径是个简单强大的存储方法,它将树形关系通过一个path字段存储。

import { Entity, PrimaryGeneratedColumn, Column, TreeChildren, Tree, TreeParent } from 'typeorm';


@Entity('T_CATEGORY')
@Tree('materialized-path')
export class Category {
 @PrimaryGeneratedColumn({ type: 'bigint' })
 id: number;
 @Column()
 name: string;
 @TreeChildren()
 children: Category[];
 @TreeParent()
 parent: Category;
} 

实际的表结构也很简单

+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name     | varchar(255) | NO   |     | NULL    |                |
| mpath    | varchar(255) | YES  |     |         |                |
| parentId | bigint(20)   | YES  | MUL | NULL    |                |
+----------+--------------+------+-----+---------+----------------+ 

当我们存储树的时候

const croot = new Category();
croot.name = 'root';
await connection.manager.save(croot);

const csub = new Category();
csub.name = 'csub';
csub.parent = croot;
await connection.manager.save(csub); 

实际存储数据如下:

+----+------+-------+----------+
| id | name | mpath | parentId |
+----+------+-------+----------+
|  2 | root | 2.    |     NULL |
|  3 | csub | 2.3.  |        2 |
+----+------+-------+----------+ 

通过这样的存储方式,我们读取树的效率会很高

select * from T_CATEGORY where mpath LIKE '2.%'

这个sql会查询root分类中的所有分类。

其他方式我们可以通过改变树的类别来实现

例如嵌套集

@Tree('nested-set') 

其表结构为

+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name     | varchar(255) | NO   |     | NULL    |                |
| nsleft   | int(11)      | NO   |     | 1       |                |
| nsright  | int(11)      | NO   |     | 2       |                |
| parentId | bigint(20)   | YES  | MUL | NULL    |                |
+----------+--------------+------+-----+---------+----------------+ 

其主要思想就是构建一个二叉树,通过nsleft和nsright指定Category的区间。比较复杂,难以修改。

另外还有闭合表

@Tree('closure-table') 

这种方法将树节点之间的关系单独成表。

mysql> describe T_CATEGORY;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name     | varchar(255) | NO   |     | NULL    |                |
| parentId | bigint(20)   | YES  | MUL | NULL    |                |
+----------+--------------+------+-----+---------+----------------+

mysql> describe T_CATEGORY_closure;
+---------------+------------+------+-----+---------+-------+
| Field         | Type       | Null | Key | Default | Extra |
+---------------+------------+------+-----+---------+-------+
| id_ancestor   | bigint(20) | NO   | PRI | NULL    |       |
| id_descendant | bigint(20) | NO   | PRI | NULL    |       |
+---------------+------------+------+-----+---------+-------+ 

这种方法各方面都很好,只不过会多出一张表。

各个方法有各个方法的好处及缺点,下面这张表总结得很棒:

TypeORM还给我们提供了各种好用的查询方法,让我们免去繁复的树查询

const treeCategories = await repository.findTrees();
// returns root categories with sub categories inside
const rootCategories = await repository.findRoots();
// returns root categories without sub categories inside
const childrens = await repository.findDescendants(parentCategory);
// returns all direct subcategories (without its nested categories) of a parentCategory
const childrensTree = await repository.findDescendantsTree(parentCategory);
// returns all direct subcategories (with its nested categories) of a parentCategory 

Relations

对应一对一,一对多/多对一,多对多三种关系,TypeORM给出了 @OneToOne @OneToMany @ManyToOne @ManyToMany 四种装饰器来完成实体关系的建模。

一对一关系相对简单,User和Profile就是简单的一对一关系

@Entity('T_USER')
export class User {

   @PrimaryGeneratedColumn({ type: 'bigint' })
   id: number;

   @Column()
   firstName: string;

   @Column()
   lastName: string;

   @Column()
   age: number;

   @OneToOne(type => Profile)
   @JoinColumn()
   profile: Profile;
} 

一对多的关系需要我们在两个实体间分别使用 @OneToMany @ManyToOne  。以User和Photo为例,一个User可能有多个Photo,其声明如下:

@Entity('T_USER')
export class User {

   @PrimaryGeneratedColumn({ type: 'bigint' })
   id: number;

   @Column()
   firstName: string;

   @Column()
   lastName: string;

   @Column()
   age: number;

   @OneToOne(type => Profile)
   @JoinColumn()
   profile: Profile;

   @OneToMany(type => Photo, (photo: Photo) => photo.user)
   photos: Photo[];
} 

而多个Photo可能属于同一User,故而

@ChildEntity()
export class Photo extends Content {
 @Column()
 filename: string;

 @Column('double')
 views: number;

 @Column()
 isPublished: boolean;

 @ManyToOne(type => User, (user: User) => user.photos)
 user: User;
} 

多对多的关系也相对简单,但需要我们指定多对多之间的关联表

@Entity('T_CONTENT')
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export class Content {
 @PrimaryGeneratedColumn({ type: 'bigint' })
 id: number;
 @Column({
   length: 100
 })
 name: string;

 @Column({
   type: 'text'
 })
 description: string;
 @ManyToMany(type => Category)
 @JoinTable({
   name: 'T_CONTENT_CATEGORIES'
 })
 categories: Category[];
} 

这里Content可能有多个分类,而Category也可能有多个Content。

TypeORM也支持Eager关联和Lazy关联,Eager关联只要我们在 @ManyToMany  修饰器中添加eager参数即可,以Content为例

@ManyToMany(type => Category, category => category.contents, { eager: true })
@JoinTable({
   name: 'T_CONTENT_CATEGORIES'
})
categories: Category[]; 

这里当我们通过 contentRepository 查询时,连带其categories也全部被查询出来。而Lazy关联,则正好相反,关联字段被设定为Promise,如下代码:

@ManyToMany(type => Category)
@JoinTable({
   name: 'T_CONTENT_CATEGORIES'
})
categories: Promise<category[]>; 

</category[]>

我们通过以下代码获取categories

const photo = await connection.getRepository(Photo).findOne(1);
const categories = await photo.categories; 

photo.categories其实是一个Promise,而其查询语句就封装在这个Promise中,我们只要get它就会执行Promise任务。

Entity Manager/Repository

我们可以通过Entity Manager和Repository两种模式进行CRUD。两者其实很相像,但是EntityManager可以操作任意的实体,而Repository则是针对单独的实体进行操作。例如我们要查询ID为1的User:

const entityManager = getManager();
const user = await entityManager.findOne(User, 1);

const userRepository = getRepository(User);
const user = await userRepository.findOne(1); 

这就是两者的区别,实际使用时你可以根据自己的需求任意使用其中一种模式。

上述findOne方法你可以转换为

const user = await userRepository.find({ where: { id: 1 } }); 

findOne只是find方法的快捷方法,find还有很多其他参数,例如你可以选择查询出来的参数:

userRepository.find({ select: ["firstName", "lastName"] }); 

你也可以排序

const users = await userRepository.find({ order: { id: 'DESC' } }); 

分页

const users = await userRepository.find({ skip: 5, take: 10 }); 

关联

const users = await userRepository.find({ relations: [ 'profile' ], order: { id: 'DESC' } }); 

TypeORM也对操作符进行了相应的封装

例如 !=

const users = await userRepository.find({ id: Not(1) }); 

其他操作符的映射如下:

  • <  LessThan

  • >  MoreThan

  • = Equal

  • LIKE Like

  • BETWEEN Between

  • IN In

  • Any Any

  • IS NULL IsNull

  • Raw

QueryBuilder

QueryBuilder允许我们使用编程语法替代不友好的SQL语句。我们由简入难来讲讲QueryBuilder如何替代SQL语句:

select * from T_USER; 

这是最为简单的查询语句,我们当然可以利用UserRepository的find方法轻松实现,QueryBuilder也不难

await userRepository
   .createQueryBuilder('user')
   .getMany(); 

那如果我们想要对应字段的实体呢?

await userRepository
   .createQueryBuilder('user')
   .select([
       'user.id',
       'user.firstName'
   ])
   .getMany(); 

再加点条件

await userRepository
   .createQueryBuilder('user')
   .select([
       'user.id',
       'user.firstName'
   ])
   .where('user.id = :id', { id: 1 })
   .getOne() 

条件还可以再复杂些

await userRepository
   .createQueryBuilder('user')
   .select([
       'user.id',
       'user.firstName'
   ])
   .where('user.id = :id', { id: 1 })
   .andWhere('user.firstName LIKE :name', { name: '%Tim%' })
   .getOne() 

甚至可以在where和from中做子查询

const profileQb = await connection.getRepository(Profile)
   .createQueryBuilder('profile')
   .select('profile.gender');
await userRepository
   .createQueryBuilder('user')
   .where(`user.firstName IN (${profileQb.getQuery()})`)
   .setParameters(profileQb.getParameters())
   .getMany(); 

你还可以分页并添加排序

await userRepository
   .createQueryBuilder('user')
   .select([
       'user.id',
       'user.firstName'
   ])
   .skip(5)
   .take(10)
   .orderBy()
   .orderBy({
       'user.id': 'DESC'
   })
   .getMany() 

我们也不仅限于单表查询可以做关联查询

await userRepository
   .createQueryBuilder('user')
   .select([
       'user.id',
       'user.firstName'
   ])
   .leftJoinAndSelect('user.profile', 'profile') // innerJoinAndSelect
   .skip(5)
   .take(10)
   .orderBy({
       'user.id': 'DESC'
   })
   .getMany() 

这都是查询语法,我们还可以增删改

await getConnection()
   .createQueryBuilder()
   .insert()
   .into(User)
   .values([
       { firstName: "Timber", lastName: "Saw" }, 
       { firstName: "Phantom", lastName: "Lancer" }
    ])
   .execute(); 
await getConnection()
   .createQueryBuilder()
   .update(User)
   .set({ firstName: "Timber", lastName: "Saw" })
   .where("id = :id", { id: 1 })
   .execute(); 
await getConnection()
   .createQueryBuilder()
   .delete()
   .from(User)
   .where("id = :id", { id: 1 })
   .execute(); 

Transactions

事务是非常重要的功能,我们在执行跨表操作时是要经常使用。TypeORM对事务功能有着强大的支持,既通过装饰器对事务提供了自动化管理也允许我们自行管理事务,还允许我们自定义事务隔离级别。

下面的例子我们使用了装饰器进行了事务操作:

@Transaction()
save(@TransactionManager() manager: EntityManager, user: User) {
   return manager.save(user);
} 

这里注入了EntityManager,其实我们也可以注入Repository。

@Transaction()
save(user: User, @TransactionRepository(User) userRepository: Repository<user>) {
   return userRepository.save(user);    
} 

如果你需要自定义事务隔离级别,也可以像下面这样做:

@Transaction({ isolation: "SERIALIZABLE" })
save(@TransactionManager() manager: EntityManager, user: User) {
   return manager.save(user);
} 

TypeORM的queryRunner可以进行手动事务管理:

import {getConnection} from "typeorm";

// get a connection and create a new query runner
const connection = getConnection();
const queryRunner = connection.createQueryRunner();

// establish real database connection using our new query runner
await queryRunner.connect();

// now we can execute any queries on a query runner, for example:
await queryRunner.query("SELECT * FROM users");

// we can also access entity manager that works with connection created by a query runner:
const users = await queryRunner.manager.find(User);

// lets now open a new transaction:
await queryRunner.startTransaction();

try {

   // execute some operations on this transaction:
   await queryRunner.manager.save(user1);
   await queryRunner.manager.save(user2);
   await queryRunner.manager.save(photos);

   // commit transaction now:
   await queryRunner.commitTransaction();

} catch (err) {

   // since we have errors lets rollback changes we made
   await queryRunner.rollbackTransaction();

} finally {

   // you need to release query runner which is manually created:
   await queryRunner.release();
} 

Migrations

Migration(数据库迁移)其实是十分有用的功能,使用这个功能你可以进行数据库的持续集成。TypeORM同样提供这个功能。其实现原理也很简单,即在运行迁移文件时,在数据库中新建一张migration表,记录迁移记录,方便迁移的回滚。我们以 T_CONTENT 为例,我们想将该表上的 views 字段从 double 改为 int ,怎么做的?

首先我们需要生成一个Migration文件:

npx typeorm migration:create -n ContentRefactoring 

运行上述命令我们会生成一个命名带有时间戳的迁移文件:

src/migration/
└── 1539132837841-ContentRefactoring.ts 

我们编写我们想要的迁移过程,up方法里面编写迁移逻辑,而down则是编写回滚逻辑。

import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";

export class ContentRefactoring1539132837841 implements MigrationInterface {

   public async up(queryRunner: QueryRunner): Promise<any> {
       await queryRunner.changeColumn('T_CONTENT', new TableColumn({
           name: 'views',
           type: 'double'
       }), new TableColumn({
           name: 'views',
           type: 'int'
       }));
   }

   public async down(queryRunner: QueryRunner): Promise<any> {
       await queryRunner.changeColumn('T_CONTENT', new TableColumn({
           name: 'views',
           type: 'int'
       }), new TableColumn({
           name: 'views',
           type: 'double'
       }));
   }

} 

这时我们运行 npx ts-node ./node_modules/.bin/typeorm migration:run 命令即可进行迁移,并且会在数据库中生成迁移记录:

这样迁移算是完成了。那现在我们发现我们的更改是有问题的,需要回滚怎么办呢?十分简单:

npx ts-node ./node_modules/.bin/typeorm migration:revert 

这是我们其实运行了down方法中的逻辑,而且删除了数据库中的迁移记录。