目录

TypeORM 的基本使用

本文主要讲述如何用 typeorm 建表,建立一对一,一对多,多对多的关系,建立表的外连接。
以及在 typeorm 做查询操作的两种常用方式:Find 选项 和 QueryBuilder。

建表

typeorm 建表时,将 @Entity() 装饰的 class 映射为数据表,entity 中 @PrimaryColumn() 装饰的属性作为表的主键, @PrimaryGeneratedColumn() 表示自动生成主键, @Column() 装饰属性作为表的属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Entity()
export class Photo {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 100 })
  name: string;

  @Column("text")
  description: string;

  @Column()
  views: number;

  @Column()
  isPublished: boolean;
}

数据库中的列类型是根据你使用的属性类型推断的,例如: number 将被转换为 integer,string 将转换为 varchar,boolean 转换为 bool 等。下面我们从实际的例子出发探索如何用 typeorm 建一对一、一对多、多对多的关系。

一对一

用户 user 和用户档案 profile 是一对一关系,一个用户只有一份档案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Entity("users")
export class UserEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @OneToOne((type) => ProfileEntity, (profile) => profile.user)
  @JoinColumn()
  profile: ProfileEntity;
}
注意
profile 是 ProfileEntity 类型的,在数据库中存储的类型却是 profile.id 的类型。

@OneToOne 中需要指明对方 entity 的类型,指明对方 entity 的外键。@JoinColumn 必须在且只在关系的一侧的外键上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Entity("profiles")
export class ProfileEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  gender: string;

  @Column()
  photo: string;
  @OneToOne((type) => UserEntity, (user) => user.profile)
  user: UserEntity;
}

这将生成以下数据表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
+-------------+--------------+----------------------------+
|                          users                          |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| username    | varchar(255) |                            |
| profileId   | int(11)      | FOREIGN KEY                |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                        profiles                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| gender      | varchar(255) |                            |
| photo       | varchar(255) |                            |
+-------------+--------------+----------------------------+

一对多

用户 user 与用户发布的文章 article 是一对多关系,一个用户可发布多篇文章。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Entity("users")
export class UserEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @OneToMany((type) => ArticleEntity, (article) => article.author)
  articles: ArticleEntity[];
}

@OneToMany,@ManyToOne 中需要指明对方的 entity 类型,指明对方 entity 的外键。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Entity("articles")
export class ArticleEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToOne((type) => UserEntity, (user) => user.articles)
  author: UserEntity;
}

typeorm 在处理 “一对多”关系时将“一”的主键作为“多”的外键 (即 @ManyToOne 装饰的属性),建表时有最少的数据表操作代价,避免数据冗余,提高效率。这会生成以下表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
+-------------+--------------+----------------------------+
|                        articles                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| title       | varchar(255) |                            |
| authorId    | int(11)      |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                          users                          |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| username    | varchar(255) |                            |
+-------------+--------------+----------------------------+

多对多

用户 user 对文章 article 的喜欢 favorite 是多对多关系。一个用户可对多篇文章标记喜欢,一篇文章可被多个用户标记喜欢。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Entity("users")
export class UserEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @ManyToMany((type) => ArticleEntity, (article) => article.favoritedBy)
  favorites: ArticleEntity[];
}

@OneToMany 中需要指明对方的 entity 类型,指明对方 entity 的外键。@JoinTable 必须在且只在关系的一侧的外键上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Entity("articles")
export class ArticleEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToMany((type) => UserEntity, (user) => user.favorites)
  @JoinTable()
  favoritedBy: UserEntity[];
}

typeorm 的处理方式是将多对多关系转化为两个一对多关系:

  • 用户 user 与 喜欢 favorites 一对多。
  • 文章 article 与被喜欢 favoritedBy 一对多。

多对多关系需要采用中间表的方式处理,这是为了避免笛卡尔积的出现。这会生成以下表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
+-------------+--------------+----------------------------+
|                          users                          |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| username    | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|                        articles                         |
+-------------+--------------+----------------------------+
| id          | int(11)      | PRIMARY KEY AUTO_INCREMENT |
| title       | varchar(255) |                            |
+-------------+--------------+----------------------------+

+-------------+--------------+----------------------------+
|              articles_favorited_by_users                |
+-------------+--------------+----------------------------+
| articlesId  | int(11)      | PRIMARY KEY FOREIGN KEY    |
| usersId     | int(11)      | PRIMARY KEY FOREIGN KEY    |
+-------------+--------------+----------------------------+

增删改查

创建好一对一,一对多,多对多的实体 entity 后,我们如何做增删改查呢?单个实体的 crud 可参考我的这一篇文章 。而关联后的实体对象会作为该实体对象的一个属性, 直接对属性进行操作即可。如下是文章被用户喜欢的实现:

1
2
3
4
5
6
async favoriteArticle(slug: string, user: UserEntity) {
    const article = await this.articleRepo.findOne({ where: { slug }});
    article.favoritedBy.push(user);
    await article.save();
    return article;
}

crud 操作中查询操作是我们最常遇到的,下面讲如何查询,typeorm 支持两种查询方式:Find 选项 和 QueryBuilder。

Find 选项

在 Nest.JS 中,对具体实体的管理(insert, update, delete, load 等)我们使用的是 Repository。对应的查找方法是:Repository.find(FindOptions)。 使用 find 查询只能获得一种类型的结果:entities。

find 选项的完整例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
userRepository.find({
    select: ["username"],
    relations: ["profile", "article""article.favoritedBy"],
    where: {
        username: "Timber",
    },
    order: {
        id: "DESC"
    },
    skip: 5,
    take: 10,
    cache: true
});

直接使用 find 是不会查出关联的对象的,要查询的关联对象需要添加到 relations 数组中。
除了 relations 以外,其他选项等同于原生 sql 操作, order 等同于 order by, skip 等同于 offset, take 等同于 limit, cache 是查询缓存。细节请参考 Find 选项

这种查询有个局限就是只能查询到关联对象的整个实体或主键。而不能 select 关联实体的其他属性。因此更复杂的查询我们需要使用 QueryBuilder。

QueryBuilder

使用 QueryBuilder 查询可以获得两种类型的结果:entities 或原始数据。 要获取 entities,请使用 getOne 和 getMany。 要获取原始数据,请使用 getRawOne 和 getRawMany。 它能够很方便的帮我们构造出 sql 语句,addSelect() 可以获取关联对象上的其他属性。

1
2
3
4
5
6
7
8
9
if (query.author) {
  const article = await getRepository(ArticleEntity)
    .createQueryBuilder("article")
    .select("article.id", "id")
    .addSelect("favoritedBy.username", "name")
    .leftJoin("article.favoritedBy", "favoritedBy")
    .where("favoritedBy.username = :name", { name: query.author })
    .getRawMany();
}

获取生成的 sql 语句可以在 getRawMany() 前获取 getSql() 或打印 printSql() 生成的 sql 语句。细节请参考 Query Builder

参阅资料