TypeORM
1.基本介绍
不同于现有的所有其他 JavaScript ORM 框架,TypeORM 支持 Active Record 和 Data Mapper 模式,这意味着你可以以最高效的方式编写高质量的、松耦合的、可扩展的、可维护的应用程序。
TypeORM 参考了很多其他优秀 ORM 的实现, 比如 Hibernate, Doctrine 和 Entity Framework。
TypeORM 可以应用于很多的平台,这里只介绍它在 nestjs 里面的应用
2.基本使用
1.安装
nest.js
对他做了很好的集成, 虽然它的官网写的挺全的但是实际开发起来还是不太够, 并且里面有大坑
我会把我知道的都列出来, 这篇也会把一些常见的解决方案写出来。
yarn add @nestjs/typeorm typeorm mysql2 -S
2.连接数据库
/share/src/app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
port: 3306,
type: 'mysql',
username: 'root',
host: 'localhost',
charset: 'utf8mb4',
password: '19910909',
database: 'learn_nest',
synchronize: true,
autoLoadEntities: true, //免注册实体,否则每个实体都必须要注册
}),],
// ...
- 上面演示的是链接我本地的
mysql
,database
是库名。 - 可以在
imports
里面定义多个TypeOrmModule.forRoot
可以操作多个库, 多个时还需要填写不同的name
属性。 synchronize
自动载入的模型将同步。autoLoadModels
模型将自动载入。
3.实体
1.建立实体
实体是一个映射到数据库表(或使用 MongoDB 时的集合)的类。 你可以通过定义一个新类来创建一个实体,并用@Entity()
来标记:
实体其实就是对应了一张表, 这个实体的 class 名字必须与表名对应, 新建 entity 文件夹 /share/src/modules/goods/entity/goods.entity.ts
:
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Goods {
@PrimaryColumn()
id: number;
@PrimaryGeneratedColumn()
id: number;
@PrimaryGeneratedColumn("uuid")
id: number;
@Column()
name: string;
@CreateDateColumn()
date: Date;
@UpdateDateColumn()
date: Date;
}
@PrimaryColumn()装饰了 id 为主键, 类型为数字。
@PrimaryGeneratedColumn()装饰了 id 为主键, 类型为数字。
@PrimaryGeneratedColumn("uuid")创建一个主列,该值将使用
uuid
自动生成。 Uuid 是一个独特的字符串 id。 你不必在保存之前手动分配其值,该值将自动生成。@Column()装饰普通行, 类型为字符串。
@CreateDateColumn()是一个特殊列,自动为实体插入日期。无需设置此列,该值将自动设置。
@UpdateDateColumn()以后每次更新数据都会自动的更新这个时间值
属性命名
注意:数据库用_命名,实体类用驼峰即可!!!
列类型
TypeORM 支持所有最常用的数据库支持的列类型。 列类型是特定于数据库类型的 - 这为数据库架构提供了更大的灵活性。
写法:
@Column("int")
@Column({ type: "int" })
@Column("varchar", { length: 200 }) //注意:当长度不是 255 的时候一定要这样写,否则会误删表!
@Column({ type: "int", length: 200 })
注意:mysql
/mariadb
的列类型
int`, `tinyint`, `smallint`, `mediumint`, `bigint`, `float`, `double`, `dec`, `decimal`, `numeric`, `date`, `datetime`, `timestamp`, `time`, `year`, `char`, `varchar`, `nvarchar`, `text`, `tinytext`, `mediumtext`, `blob`, `longtext`, `tinyblob`, `mediumblob`, `longblob`, `enum`, `json`, `binary`, `geometry`, `point`, `linestring`, `polygon`, `multipoint`, `multilinestring`, `multipolygon`, `geometrycollection
注意事项:
默认情况下,列名称是从属性的名称生成的。 你也可以通过指定自己的名称来更改它。
length: number
- 列类型的长度。 例如,如果要创建varchar(150)
类型,请指定列类型和长度选项。(如果不指定会出现错误,默认是 255)
2.实体继承
这些实体都有共同的列:id
,title
,description
。 为了减少重复并产生更好的抽象,我们可以为它们创建一个名为Content
的基类:
export abstract class Content {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
description: string;
}
@Entity()
export class Photo extends Content {
@Column()
size: string;
}
@Entity()
export class Question extends Content {
@Column()
answersCount: number;
}
@Entity()
export class Post extends Content {
@Column()
viewCount: number;
}
来自父实体的所有列(relations,embeds 等)(父级也可以扩展其他实体)将在最终实体中继承和创建。
3.嵌入式实体
假设我们有User
,Employee
和Student
实体。
这些属性都有少量的共同点,first name
和 last name
属性。
我们可以做的是通过创建一个包含firstName
和lastName
的新类:
import { Entity, Column } from "typeorm";
export class Name {
@Column()
first: string;
@Column()
last: string;
}
然后"connect"实体中的这些列:
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Name } from "./Name";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: string;
@Column((type) => Name)
name: Name;
@Column()
isActive: boolean;
}
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Name } from "./Name";
@Entity()
export class Employee {
@PrimaryGeneratedColumn()
id: string;
@Column((type) => Name)
name: Name;
@Column()
salary: number;
}
Name实体中定义的所有列将合并为
user,
employee:
+-------------+--------------+----------------------------+
| user |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| nameFirst | varchar(255) | |
| nameLast | varchar(255) | |
| isActive | boolean | |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| employee |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| nameFirst | varchar(255) | |
| nameLast | varchar(255) | |
| salary | int(11) | |
+-------------+--------------+----------------------------+
注意:名字是反过来了,注意数据库的命名!
这种方式可以减少实体类中的代码重复。 你可以根据需要在嵌入式类中使用尽可能多的列(或关系)。 甚至可以在嵌入式类中嵌套嵌套列。
3.注册实体到 Module
nest
自身设计的还不是很好, 引入搞得好麻烦 /share/src/modules/goods/goods.module.ts
:
import { Module } from '@nestjs/common';
import { GoodsController } from './goods.controller';
import { GoodsService } from './goods.service';
import { Goods } from './entity/goods.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([Goods])],
controllers: [GoodsController],
providers: [GoodsService]
})
export class GoodsModule { }
forFeature()
方法定义在当前范围中注册哪些存储库。
5.引入实体到 Service
/share/src/modules/goods/goods.service.ts
:
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Goods } from "./entity/goods.entity";
import { Repository } from "typeorm";
@Injectable()
export class GoodsService {
constructor(
@InjectRepository(Goods)
private goodsRepository: Repository<Goods>
) {}
getList() {
return this.goodsRepository.find();
}
}
@InjectRepository()
装饰器将goodsRepository
注入GoodsService
中。- 被注入进来的
Repository
都自带属性, 这里使用了自带的find
方法后面会举例出更多。
4.关系
typeorm 的强大之处在于,应该先建立实体,再建立表,会省很多时间!! 不需要根据数据库表来建实体!
而关系可以很好地帮我们处理一对多和多对一关系!免去了一些中立表的建立!所以关系的建立是必要的!
说白了关系的建立,就是为了简化代码,简化表,帮助我们快速开发的!
关系可以帮助你轻松地与相关实体合作。 有几种类型的关系:
多对多关系?
举个例子,假设有一个博客系统,其中Post
表示文章,Category
表示文章的分类。一个文章可以属于多个分类,而一个分类下也可以有多篇文章。这种多对多的关系可以通过中间表来表示,中间表中记录了Post
和Category
之间的对应关系。
1.typeorm 中关系的理解
一开始我认为关系不重要,不需要外键就不需要定义关系,这是个鸡肋的功能,但是当我进行实践之后才发现,关系这种定义正是 typeorm 这种数据库操作工具最强大的地方,虽然使用关系会有点不适应和不容易理解,但是它存在以下的绝佳好处:
1.由于 typeorm 本身就是一种强大的实体与数据库同步的操作系统,它的优势就是可以通过实体自动创建表,将权力进行了反转!
为了发挥这种优势,我们就一定要应用关系这一强大的功能!
关系可以很好地帮我们处理一对多和多对一关系!免去了一些中立表的建立!
2.本质上多对一的关系也至少要进行一次循环(或者联表查询)才可以查询完整数据,关系的建立省去了 for 循环,省略了大量查询代码,提高了查询效率
多对一:避免数组字符串的关联(比如某个字段是[1,3,4]这样的字符串),避免新建一张表
多对多:避免新建一张表

多对一时,利用数组字符串的关联查询,和利用关系查询,代码的区别:
async getHobby(id: number) {
//QueryBuilder
/*
或者这样写:
this.hobbysRepository.findOne({
where: {
id: id,
}
})
*/
let hobby = await this.hobbysRepository.createQueryBuilder().where("id = :id", { id: id }).getOne();
return hobby.name;
}
async getUserWithHobbyList(): Promise<UserType[]> {
let userList = await this.usersRepository.find();
let users = [] as UserType[];
for (let j = 0; j < userList.length; j++) {
let hobbys = JSON.parse(userList[j].hobbyIds.valueOf()).map(Number);
console.log(hobbys)
let theUserHobbys = hobbys.map(e => {
return this.getHobby(e);
})
delete e.hobbyIds;
let user = Object.assign(e, {
hobbys: theUserHobbys
})
users.push(user);
}
return users;
}
现在:省了非常多的代码!
async getUserWithHobbyList(): Promise<UserType[]> {
return this.usersRepository.find({ relations: ["hobbys"] })
}
2.forEach 不能执行 await
1.原因
这是因为async 和 await 必须成对出现,如果调用 await 的地方无法正确匹配到 async 则会报错, forEach属于并发操作,在 forEach 循环内调用异步函数时,下一个循环并不会等到上个循环结束后再被调用,所以以上调用方式才会报错。
2.解决方案
以下将介绍 forEach 和其他循环中 async/await 异步调用方法
- forEach 循环中处理
async detailData (newVal) {
await Promise.all(
newVal.forEach(async line => {
var list = await infoStore(line.id)
})
)
}
- for of 中处理
async detailData (newVal) {
for (let item of newVal){
var list = await infoStore(item.id)
}
}
- for 循环中处理
async detailData (newVal) {
for (var i=0; i<newVal.length; i++) {
var list = await infoStore(newVal[i].id)
}
}
- map 中处理
async detailData (newVal) {
let list = newVal.map(async item => {
return await infoStore(item.id)
})
// 这时候 list 拿到的值还是promise数据
// 需要再使用promise.all()处理一下,最终listEnd就是Promise里面的数据
let listEnd = await Promise.all(list)
}
3.数据库外键的理解
外键是否采用看业务应用场景,以及开发成本的,大致列下什么时候适合,什么时候不适合使用:
互联网行业应用不推荐使用外键:
用户量大,并发度高,为此数据库服务器很容易成为性能瓶颈,尤其受 IO 能力限制,且不能轻易地水平扩展;若是把数据一致性的控制放到事务中,也即让应用服务器承担此部分的压力,而引用服务器一般都是可以做到轻松地水平的伸缩;
为何说外键有性能问题:
1.数据库需要维护外键的内部管理;
2.外键等于把数据的一致性事务实现,全部交给数据库服务器完成;
3.有了外键,当做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,而不得不消耗资源;
4.外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;
外键的使用和必要性:
外键字段和主键字段的名称可以不同,但是类型应该一致。
外键的必要性:
不用外键约束,也可以进行关联查询,但是有了它,MySQL 系统才会保护你的数据,避免出现误删的情况,从而提高系统整体的可靠性。
为什么在 MySQL 里,没有外键约束也可以进行关联查询呢?
(1)原因是外键约束是有成本的,需要消耗系统资源。对于大并发的 SQL 操作,有可能会不适合。比如大型网站的中央数据库,可能会因为外键约束的系统开销而变得非常慢。所以,MySQL 允许你不使用系统自带的外键约束,而是在应用层面完成检查数据一致性的逻辑。也就是说,即使你不用外键约束,也要想办法通过应用层面的附加逻辑,来实现外键约束的功能,确保数据的一致性。
(2)外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
(3)“外键约束,可以简单有效的保证数据的可靠性。适合内部管理系统使用,因为访问量不会太大。如果是面向外界用户使用的应用,外键所带来的性能损耗,可能无法支撑大批用户的同时访问。但是,数据的可靠性和唯一性才是最重要的,在保证一次对多张表操作的过程中,可以使用事务来确保操作的一致性。”===》作者回复: 在系统开销和使用的功能之间需要做好平衡,既要确保数据可靠性和唯一性,又要确保系统可用.
如果你的业务场景因高并发等原因,不能使用外键约束,在这种情况下,你怎么在应用层面确保数据的一致性呢?
应用层面实现外键约束的功能,指的就是在应用里面加上检查。比如删除主表中记录的时候,需要检查从表中是否有记录引用到主表中的这条记录,有的话就不能删除,否则可以删除(或者连着从表中的数据一起删除)。各种不同的框架有自己的特点,要根据实际情况决定如何处理。
实际案例:
注意:带有外键的表称之为副表,不带外键的表称之为主表。
4.一对一
注意:如果我们不想要外键,那么就不要设置这个一对一的关系!
一对一是一种 A 只包含一个 B 实例,而 B 只包含一个 A 实例的关系。 我们以User
和Profile
实体为例。
用户只能拥有一个配置文件,并且一个配置文件仅由一个用户拥有。
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
} from "typeorm";
import { Profile } from "./Profile";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile)
@JoinColumn()
profile: Profile;
}
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from "typeorm";
import { User } from "./User";
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
gender: string;
@Column()
photo: string;
@OneToOne(() => User, (user) => user.profile) // 将另一面指定为第二个参数
user: User;
}
这里我们将@OneToOne
添加到profile
并将目标关系类型指定为Profile
。 我们还添加了**@JoinColumn
,这是必选项并且只能在关系的一侧设置**。 你设置@JoinColumn
的哪一方,哪一方的表将包含一个"relation id"和目标实体表的外键。
注意:@JoinColumn = 外键,外键应该是存在于主表上面的,主表的外键指向附表的主键!
此示例将生成以下表:
+-------------+--------------+----------------------------+
| profile |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| gender | varchar(255) | |
| photo | varchar(255) | |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| user |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
| profileId | int(11) | FOREIGN KEY |
+-------------+--------------+----------------------------+
要加载带有配置文件的用户,必须在FindOptions
中指定关系:
const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ["profile"] });
5.一对多/多对一
多对一/一对多是指 A 包含多个 B 实例的关系,但 B 只包含一个 A 实例。 让我们以User
和 Photo
实体为例。 User 可以拥有多张 photos,但每张 photo 仅由一位 user 拥有。
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
import { User } from "./User";
@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number;
@Column()
url: string;
@ManyToOne(() => User, (user) => user.photos)
user: User;
}
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Photo } from "./Photo";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(() => Photo, (photo) => photo.user)
photos: Photo[];
//特别注意:不要在实体中初始化数组!否则会误删数据!
/**
即不要写:
photos: Photo[] = [];
*/
}
可以在@ManyToOne
/ @OneToMany
关系中省略@JoinColumn
,除非你需要自定义关联列在数据库中的名称。
@ManyToOne
可以单独使用,但@OneToMany
必须搭配@ManyToOne
使用。
在你设置@ManyToOne
的地方,相关实体将有"关联 id"和外键。
注意:关联 id 就会被设置为外键
此示例将生成以下表:
+-------------+--------------+----------------------------+
| photo |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| url | varchar(255) | |
| userId | int(11) | |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| user |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
+-------------+--------------+----------------------------+
要在内部加载带有 photos 的 user,必须在FindOptions
中指定关系:
const userRepository = connection.getRepository(User);
const users = await userRepository.find({ relations: ["photos"] });
// or from inverse side
const photoRepository = connection.getRepository(Photo);
const photos = await photoRepository.find({ relations: ["user"] });
数据库表形式:

注意:userId 字段是设立关系之后自动建立的,一对多中“多”的表,会有重复数据(userId 不同的),避免新建一张表

6.多对多
多对多是一种 A 包含多个 B 实例,而 B 包含多个 A 实例的关系。 我们以Question
和 Category
实体为例。 Question 可以有多个 categories, 每个 category 可以有多个 questions。
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from "typeorm";
import { Question } from "./Question";
@Entity()
export class Category {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Question, (question) => question.categories)
questions: Question[];
}
import {
Entity,
PrimaryGeneratedColumn,
Column,
ManyToMany,
JoinTable,
} from "typeorm";
import { Category } from "./Category";
@Entity()
export class Question {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
text: string;
@ManyToMany(() => Category, (category) => category.questions)
@JoinTable()
categories: Category[];
}
@JoinTable()
是@ManyToMany
关系所必需的。 你必须把@JoinTable
放在关系的一个(拥有)方面。
此示例将生成以下表:
+-------------+--------------+----------------------------+
| category |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| name | varchar(255) | |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| question |
+-------------+--------------+----------------------------+
| id | int(11) | PRIMARY KEY AUTO_INCREMENT |
| title | varchar(255) | |
+-------------+--------------+----------------------------+
+-------------+--------------+----------------------------+
| question_categories_category |
+-------------+--------------+----------------------------+
| questionId | int(11) | PRIMARY KEY FOREIGN KEY |
| categoryId | int(11) | PRIMARY KEY FOREIGN KEY |
+-------------+--------------+----------------------------+
要在 categories 里面加载 question,你必须在FindOptions
中指定关系:
const questionRepository = connection.getRepository(Question);
const questions = await questionRepository.find({ relations: ["categories"] });
7.避免创建外键约束
有时出于性能原因,您可能希望在实体之间建立关系,但不需要外键约束。 您可以使用createForeignKeyConstraints
选项来定义是否应该创建外键约束(默认值: true)。
import { Entity, PrimaryColumn, Column, ManyToOne } from "typeorm";
import { Person } from "./Person";
@Entity()
export class ActionLog {
@PrimaryColumn()
id: number;
@Column()
date: Date;
@Column()
action: string;
@ManyToOne((type) => Person, {
createForeignKeyConstraints: false,
})
person: Person;
}
5.Entity Manager 和 Repository
1.什么是 EntityManager
使用EntityManager
,你可以管理(insert, update, delete, load 等)任何实体。 EntityManager 就像放一个实体存储库的集合的地方。
你可以通过getManager()
或Connection
访问实体管理器。
如何使用它:
import { getManager } from "typeorm";
import { User } from "./entity/User";
const entityManager = getManager(); // 你也可以通过 getConnection().manager 获取
const user = await entityManager.findOne(User, 1);
user.name = "Umed";
await entityManager.save(user);
2.什么是 Repository(推荐)
Repository
就像EntityManager
一样,但其操作仅限于具体实体。
你可以通过getRepository(Entity)
,Connection#getRepository
或EntityManager#getRepository
访问存储库。
例如:
import { getRepository } from "typeorm";
import { User } from "./entity/User";
const userRepository = getRepository(User); // 你也可以通过getConnection().getRepository()或getManager().getRepository() 获取
const user = await userRepository.findOne(1);
user.name = "Umed";
await userRepository.save(user);
有三种类型的存储库:
Repository
- 任何实体的常规存储库。TreeRepository
- 用于树实体的Repository
的扩展存储库(比如标有@ Tree
装饰器的实体)。有特殊的方法来处理树结构。MongoRepository
- 具有特殊功能的存储库,仅用于 MongoDB。
3.Find 查询
所有存储库和管理器find
方法都接受可用于查询所需数据的特殊选项,而无需使用QueryBuilder
:
select
- 表示必须选择对象的哪些属性
userRepository.find({ select: ["firstName", "lastName"] });
where
-查询实体的简单条件。
userRepository.find({ where: { firstName: "Timber", lastName: "Saw" } });
查询嵌入实体列应该根据定义它的层次结构来完成。 例:
userRepository.find({ where: { name: { first: "Timber", last: "Saw" } } });
使用 OR 运算符查询:
userRepository.find({
where: [
{ firstName: "Timber", lastName: "Saw" },
{ firstName: "Stan", lastName: "Lee" },
],
});
将执行以下查询:
SELECT * FROM "user" WHERE ("firstName" = 'Timber' AND "lastName" = 'Saw') OR ("firstName" = 'Stan' AND "lastName" = 'Lee')
order
- 选择排序
userRepository.find({
order: {
name: "ASC",
id: "DESC",
},
});
返回多个实体的find
方法(find
,findAndCount
,findByIds
),同时也接受以下选项:
skip
- 偏移(分页)
userRepository.find({
skip: 5,
});
take
- limit (分页) - 得到的最大实体数。
userRepository.find({
take: 10,
});
注意:进阶选项
TypeORM 提供了许多内置运算符,可用于创建更复杂的查询
例子:
Not
import { Not } from "typeorm";
const loadedPosts = await connection.getRepository(Post).find({
title: Not("About #1"),
});
将执行以下查询:
SELECT * FROM "post" WHERE "title" != 'About #1'
4.关联查询
注意:这里默认是用 id 字段进行 left join 关联的!
relations
- 关系需要加载主体。 也可以加载子关系(join
和leftJoinAndSelect
的简写)
示例一
userRepository.find({ relations: ["profile", "photos", "videos"] });
这行代码大致等价于以下 SQL 查询语句:
SELECT *
FROM userRepository
LEFT JOIN profile ON userRepository.profileId = profile.id
LEFT JOIN photos ON userRepository.photoId = photos.id
LEFT JOIN videos ON userRepository.videoId = videos.id;
这条 SQL 查询使用了 LEFT JOIN 来将userRepository
表与profile
、photos
和videos
表进行连接,并根据相关的外键关系检索所有匹配的记录。
示例二:关联中的关联
videos.video_attributes 的意思就是 videos 与 video_attributes 两个表左连接
userRepository.find({
relations: ["profile", "photos", "videos", "videos.video_attributes"],
});
这行代码则等价于以下 SQL 查询语句:
SELECT *
FROM userRepository
LEFT JOIN profile ON userRepository.profileId = profile.id
LEFT JOIN photos ON userRepository.photoId = photos.id
LEFT JOIN videos ON userRepository.videoId = videos.id
LEFT JOIN video_attributes ON videos.video_attributesId = video_attributes.id;
这条 SQL 查询在第一条基础上,再额外进行了一个 LEFT JOIN,将videos
表与video_attributes
表进行连接,以获取视频的子属性数据。
join
- 需要为实体执行联接,扩展版对的"relations"。(就是完整版,relations 就是简写版)
userRepository.find({
join: {
alias: "user",
leftJoinAndSelect: {
profile: "user.profile",
photo: "user.photos",
video: "user.videos",
},
},
});
5.Left Join 的含义
LEFT JOIN 关键字从左表(table1)返回所有的行,即使右表(table2)中没有匹配。如果右表中没有匹配,则结果为 NULL。
SELECT column_name(s)
FROM table1
LEFT JOIN table2
ON table1.column_name=table2.column_name;
或:
SELECT column_name(s)
FROM table1
LEFT OUTER JOIN table2
ON table1.column_name=table2.column_name;
**注释:**在某些数据库中,LEFT JOIN 称为 LEFT OUTER JOIN。

6.自定义存储库
扩展了标准存储库的定制存储库
创建自定义 repository 的第一种方法是扩展Repository
。 例如:
import { EntityRepository, Repository } from "typeorm";
import { User } from "../entity/User";
@EntityRepository(User)
export class UserRepository extends Repository<User> {
findByName(firstName: string, lastName: string) {
return this.findOne({ firstName, lastName });
}
}
然后你可以这样使用它:
import { getCustomRepository } from "typeorm";
import { UserRepository } from "./repository/UserRepository";
const userRepository = getCustomRepository(UserRepository); // 或connection.getCustomRepository或manager.getCustomRepository()
const user = userRepository.create(); // 和 const user = new User();一样
user.firstName = "Timber";
user.lastName = "Saw";
await userRepository.save(user);
const timber = await userRepository.findByName("Timber", "Saw");
如你所见,你也可以使用getCustomRepository
获取 repository, 并且可以访问在其中创建的任何方法以及标准实体 repository 中的任何方法。
还有两种,不重要,这里不进行学习!
7.Repository API
注意:Entity Manager API 这里我们先不学习,主要使用 Repository
manager
- 存储库使用的EntityManager
。
const manager = repository.manager;
createQueryBuilder
- 创建用于构建 SQL 查询的查询构建器。 更多关于QueryBuilder.——> 重点
const users = await repository
.createQueryBuilder("user")
.where("user.name = :name", { name: "John" })
.getMany();
hasId
- 检查是否定义了给定实体的主列属性。
if (repository.hasId(user)) {
// ... do something
}
getId
- 获取给定实体的主列属性值。复合主键返回的值将是一个具有主列名称和值的对象。
const userId = repository.getId(user); // userId === 1
create
- 创建User
的新实例。接受具有用户属性的对象文字,该用户属性将写入新创建的用户对象(可选)。——> 重点
const user = repository.create(); // 和 const user = new User();一样
const user = repository.create({
id: 1,
firstName: "Timber",
lastName: "Saw",
}); // 和const user = new User(); user.firstName = "Timber"; user.lastName = "Saw";一样
merge
- 将多个实体合并为一个实体。
const user = new User();
repository.merge(user, { firstName: "Timber" }, { lastName: "Saw" }); // 和 user.firstName = "Timber"; user.lastName = "Saw";一样
preload
- 从给定的普通 javascript 对象创建一个新实体。如果实体已存在于数据库中,则它将加载它(以及与之相关的所有内容),并将所有值替换为给定对象中的新值,并返回新实体。 新实体实际上是从数据库加载的所有属性都替换为新对象的实体。就是相当于准备对数据库对象进行修改、新增!先构造一个具有新的属性的对象(但是是通过查询数据库来进行构造的)!
const partialUser = {
id: 1,
firstName: "Rizzrak",
profile: {
id: 1,
},
};
const user = await repository.preload(partialUser);
// user将包含partialUser中具有partialUser属性值的所有缺失数据:
// { id: 1, firstName: "Rizzrak", lastName: "Saw", profile: { id: 1, ... } }
save
- 保存给定实体或实体数组。——> 重点如果该实体已存在于数据库中,则会更新该实体。
如果数据库中不存在该实体,则会插入该实体。
它将所有给定实体保存在单个事务中(在实体的情况下,管理器不是事务性的)。因为跳过了所有未定义的属性,还支持部分更新。
await repository.save(user);
await repository.save([category1, category2, category3]);
remove
- 删除给定的实体或实体数组。——> 重点它将删除单个事务中的所有给定实体(在实体的情况下,管理器不是事务性的)。
await repository.remove(user);
await repository.remove([category1, category2, category3]);
怎么理解?
- 单个实体情况下的事务性:
- 当你调用
remove
方法删除仅涉及单个实体的情况时,这个删除操作并不是在事务中执行的。这意味着它不会自动封装在事务中,也就是说,它不会自动启动一个事务来执行删除操作,并且删除操作不会被回滚。 - 换句话说,如果你删除了一个单个实体,这个操作是立即执行的,而且如果删除失败,它也不会被回滚。
- 当你调用
- 多个实体情况下的事务性:
- 但是,当你调用
remove
方法并传递一个实体数组时,TypeORM 会在单个事务中执行这些删除操作。这意味着如果其中的任何一个删除操作失败,整个事务都会被回滚,从而确保数据的一致性。
- 但是,当你调用
这个概念的要点在于,在处理多个实体时,TypeORM 会自动启动一个事务来确保这些操作的原子性和一致性,而在处理单个实体时,你需要自己考虑是否要将其包装在事务中。
事务
事务(Transaction)是数据库管理系统(DBMS)中用于确保数据库操作的原子性、一致性、隔离性和持久性(ACID)的一种机制。
- 原子性(Atomicity):指数据库操作要么全部执行成功,要么全部不执行,即操作要么完全完成,要么完全不执行,不存在部分执行的情况。
- 一致性(Consistency):指数据库操作使数据库从一个一致性状态转移到另一个一致性状态,不会破坏数据库的完整性约束。
- 隔离性(Isolation):指在并发执行的事务中,一个事务的操作不会受到其他事务的影响,每个事务看到的数据都是一致的。
- 持久性(Durability):指一旦事务提交,对数据库的修改将被永久保存在数据库中,并且不会因为系统崩溃或其他错误而丢失。
在数据库中,事务通常涉及一系列数据库操作,如插入、更新、删除等。事务的开始和结束由特定的事务控制命令来标识,如在 SQL 中通常是 BEGIN TRANSACTION、COMMIT 和 ROLLBACK。
事务可以保证数据库操作的完整性和可靠性,确保数据库在处理多个并发操作时保持一致性,并且可以回滚到之前的状态以避免数据损坏。
insert
- 插入新实体或实体数组。——> 重点
await repository.insert({
firstName: "Timber",
lastName: "Timber",
});
await manager.insert(User, [
{
firstName: "Foo",
lastName: "Bar",
},
{
firstName: "Rizz",
lastName: "Rak",
},
]);
update
- 通过给定的更新选项或实体 ID 部分更新实体。——> 重点
await repository.update({ firstName: "Timber" }, { firstName: "Rizzrak" });
// 执行 UPDATE user SET firstName = Rizzrak WHERE firstName = Timber
await repository.update(1, { firstName: "Rizzrak" });
// 执行 UPDATE user SET firstName = Rizzrak WHERE id = 1
delete
-根据实体 id, ids 或给定的条件删除实体:——> 重点
await repository.delete(1);
await repository.delete([1, 2, 3]);
await repository.delete({ firstName: "Timber" });
count
- 符合指定条件的实体数量。对分页很有用。
const count = await repository.count({ firstName: "Timber" });
increment
- 增加符合条件的实体某些列值。
await manager.increment(User, { firstName: "Timber" }, "age", 3);
decrement
- 减少符合条件的实体某些列值。
await manager.decrement(User, { firstName: "Timber" }, "age", 3);
find
- 查找指定条件的实体。——> 重点
const timbers = await repository.find({ firstName: "Timber" });
findAndCount
- 查找指定条件的实体。还会计算与给定条件匹配的所有实体数量,但是忽略分页设置 (skip
和take
选项)。
const [timbers, timbersCount] = await repository.findAndCount({
firstName: "Timber",
});
findByIds
- 按 ID 查找多个实体。
const users = await repository.findByIds([1, 2, 3]);
findOne
- 查找匹配某些 ID 或查找选项的第一个实体。——> 重点
const user = await repository.findOne(1);
const timber = await repository.findOne({ firstName: "Timber" });
findOneOrFail
- -findOneOrFail
- 查找匹配某些 ID 或查找选项的第一个实体。 如果没有匹配,则 Rejects 一个 promise。
const user = await repository.findOneByOrFail({ id: 1 });
const timber = await repository.findOneOrFail({ firstName: "Timber" });
query
- 执行原始 SQL 查询。——> 重点
const rawData = await repository.query(`SELECT * FROM USERS`);
clear
- 清除给定表中的所有数据(truncates/drops)。
await repository.clear();
6.QueryBuilder
QueryBuilder
(查询构造器)是 TypeORM 最强大的功能之一 ,它允许你使用优雅便捷的语法构建 SQL 查询,执行并获得自动转换的实体。
有几种方法可以创建Query Builder
:
使用 repository:
typescriptimport { getRepository } from "typeorm"; const user = await getRepository(User) .createQueryBuilder("user") .where("user.id = :id", { id: 1 }) .getOne();
还有两种,不学
1.基本使用
有 5 种不同的QueryBuilder
类型可用:
SelectQueryBuilder
- 用于构建和执行SELECT
查询。 例如:typescriptimport { getConnection } from "typeorm"; const user = await getConnection() .createQueryBuilder() .select("user") .from(User, "user") .where("user.id = :id", { id: 1 }) .getOne();
InsertQueryBuilder
- 用于构建和执行INSERT
查询。 例如:typescriptimport { getConnection } from "typeorm"; await getConnection() .createQueryBuilder() .insert() .into(User) .values([ { firstName: "Timber", lastName: "Saw" }, { firstName: "Phantom", lastName: "Lancer" }, ]) .execute();
UpdateQueryBuilder
- 用于构建和执行UPDATE
查询。 例如:typescriptimport { getConnection } from "typeorm"; await getConnection() .createQueryBuilder() .update(User) .set({ firstName: "Timber", lastName: "Saw" }) .where("id = :id", { id: 1 }) .execute();
DeleteQueryBuilder
- 用于构建和执行DELETE
查询。 例如:typescriptimport { getConnection } from "typeorm"; await getConnection() .createQueryBuilder() .delete() .from(User) .where("id = :id", { id: 1 }) .execute();
RelationQueryBuilder
- 用于构建和执行特定于关系的操作[TBD]。
你可以在其中切换任何不同类型的查询构建器,一旦执行,则将获得一个新的查询构建器实例(与所有其他方法不同)。
7.MongoDB 支持
注意:理论上来说,存在一对多和多对多的关系,使用 MongoDB 会简单很多,因为不用联表查询(或者 for 循环查询),但是实际上,这是一种投机取巧的方法,不提倡,因为大多数的数据还是结构化数据,需要用结构化数据库 mysql!
但是 mysql 处理一对多和多对一是会相对比较复杂的!
后面再学
什么时候需要使用 MongoDB,什么时候适合?
- 游戏应用:使用 MongoDB 作为游戏服务器的数据库存储用户信息。用户的游戏装备、积分等直接以内嵌文档的形式存储,方便进行查询与更新。
- 物流应用:使用 MongoDB 存储订单信息,订单状态在运送过程中会不断更新,以 MongoDB 内嵌数组的形式来存储,一次查询就能将订单所有的变更读取出来,方便快捷且一目了然。
- 社交应用:使用 MongoDB 存储用户信息以及用户发表的朋友圈信息,通过地理位置索引实现附近的人、地点等功能。并且 MongoDB 非常适合用来存储聊天记录,因为它提供了非常丰富的查询,并在写入和读取方面都相对较快。
- 视频直播:使用 MongoDB 存储用户信息、礼物信息等。
- 大数据应用:使用 MongoDB 作为大数据的云存储系统,随时进行数据提取分析,掌握行业动态。
8.坑点罗列
1.实体的强替换,莫名删表
以我们上面设置的实体为例:
export class Goods {
@primarygeneratedcolumn ()
id: number;
@column ()
name: string;
}
我们初始化的表里面name
字段对应的类型是varchar(45)
, 但是name: string;
这种方式初始化的类型是varchar(255)
, 此时类型是不一致的, typeorm
选择清空我们的name列
, 是的你没听错name列
被清空了:

并且是只要你运行nest
项目的时候就同步热更新
了, 完全无感, 甚至你都不知道被清空了, 如果此时是线上环境请准备点干粮'跑路'吧。
不光是string
类型, 其他任何类型只要对不上就全给你删了, 毫无提示。
解决方案
原因:类型和数据库列类型不匹配,typeorm 认为不是一样的列,所以 typeorm 认为数据库中不应该有那个原来的列,所以删除了那一列
而且**@Column()什么都不写的话,默认是@Column( {type: “varchar”, length:256} ),即便是数据库中的 length 不一样,也会被认为是不一样的类型,会造成删列!**
1.设置数据库连接时
synchronize: false, //不同步实体到数据库
这样就不会删列了
2.当 length 不一样时,手动设置类型和长度,确保和数据库一致!——> 一定要注意
@Column({ type: "varchar", length: 40 })
特别注意:即使用了方案二,还是建议强烈关闭 synchronize,免得出现意外!!!
2.没有集成现有数据库的方案
我们很多时候数据库都是已有数据的, 全新的空白数据库空白表的情况并不是主流, 在typeorm
官网也并没有找到很好的接入数据库的方案, 全部都是冒着删库的危险在定义类型, 更有甚者你改到一半不小心自动保存
了, 那么你的表就空了...
我们不可能每次都是用空白数据库开发, 这点真难得很难人忍受。
3.entities 的三种设置方式
第一种: 单独定义 在/share/src/app.module.ts
配置链接数据库时:
TypeOrmModule.forRoot({
//...
entities: [Goods, User],
}),],
你用到哪些实体, 就逐一在此处引入, 缺点就是我们每写一个实体就要引入一次否则使用实体时会报错。
第二种: 自动加载我们的实体,每个通过forFeature()
注册的实体都会自动添加到配置对象的 entities 数组中, forFeature()
就是在某个service
中的imports
里面引入的, 这个是比较推荐的:
TypeOrmModule.forRoot({
//...
autoLoadEntities: true,
}),],
第三种: 自定义引入路径, 这个居然是官方推荐...
TypeOrmModule.forRoot({
//...
entities: ['dist/**/*.entity{.ts,.js}'],
}),],
4.entities 的大坑点, 莫名引入
当我们使用上述第三种方式引入实体时, 一个超级 bug 出现了, 情景步骤如下:
- 我要写一个
user
的实体。 - 我直接复制了
goods.entity.ts
实体的文件改名为user.entity.ts
。 - 修改其内部的属性, 比如定义了
userName
,age
,status
等新属性, 删除了商品价格等旧属性。 - 但是我们还没有把导出的
Goods
类名改成User
, 由于编辑器失去焦点等原因导致vscode
自动保存了。 - 惊喜来了, 你的
goods
表被清空了
, 是的你还没有在任何地方引用这个user.entity.ts
文件, 但是它已经生效了, 并且无声无息的把你的goods
表清空了。 - 我当时问该项目的负责人如何避免上述问题, 他研究了一下午, 告诉我关闭自动保存...(告辞)
5.多人开发, 极其混乱
这个多人开发简直是噩梦, 互相删表
的情况逐渐出现, 一个实际的例子比如a同事
优化所有实体
的配置比如统一把varchar(255)
改成varchar(45)
, 所有的相关数据都会被清空, 于此同时你发现了问题, 并把数据补充回来了, 但此时b同事
的电脑里还是varchar(255)
版本, 一起开发时就会导致你不管怎么改数据, 表里的数据都会被反复清除干净...
我们团队当时解决方案是, 每个人都复制一份当前库单独进行开发, 几个人开发就要有几个不同的库, 我们的mysql
里全是已自己姓名命名的库。
每次 git 拉取代码都要修改库名, 否则会把其他人的库清空;
6.多版本开发
比如张三使用的是zhangsan_xxx
库, 但是他同时开发几个版本, 这几个版本之前表的格式有差别, 那么张三要使用zhangsan_xxx_1_1
, zhangsan_xxx_1_2
这种命名格式来进行多个库的开发。
综上所述除非公司已经定了技术选型, 否则我不建议用 nest 开发...
9.Nest 结合最佳实践例子
1.entity 设置
import { Entity, Column, Timestamp, UpdateDateColumn, CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm';
export enum GoodsStatus {
NORMAL = 1,
HOT = 2,
OFFSHELF = 3,
}
@Entity()
export class Goods {
@PrimaryGeneratedColumn()
id: number;
@Column({
unique: true, nullable: false
})
name: string;
@Column({
type: "varchar",
length: 150,
default: '暂无'
})
remarks: string;
@Column({ default: true })
isActive: boolean;
@Column({
type: 'enum',
enum: GoodsStatus,
default: GoodsStatus.NORMAL,
})
status: GoodsStatus;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
putDate: Timestamp;
@CreateDateColumn()
createDate: Timestamp;
@UpdateDateColumn()
updateDate: Timestamp;
}
nullable: false
不可以为空。unique: true
唯一值, 不允许有重复的name
的值出现, 需要注意的是如果当前的表里面已经有重复的name
了typeorm
会报错, 所以如果设置失败请检查表内容。length: 256
限制字符的长度, 对应varchar(256)
。default: '暂无'
默认值, 要注意当你手动设置为空字符串时并不会被设置为默认值。type: 'enum
定义为枚举类型,enum: GoodsStatus
指定枚举值, 当你赋予其非枚举值时会报错。type: 'timestamp'
定义类型为时间格式,CURRENT_TIMESTAMP
默认就是创建时间。@CreateDateColumn()
这个自动就可以为我们设置值为创建时间。@UpdateDateColumn()
以后每次更新数据都会自动的更新这个时间值。
2.find 方法, 简洁的查找命令
上面我们已经将goodsRepository
注入到了GoodsService
里面可以直接使用:
constructor(
@InjectRepository(Goods)
private goodsRepository: Repository<Goods>
) { }
1.无条件查询所有数据
this.goodsRepository.find()
查询goods
表的全部数据, 以及每条数据的信息。
2.只显示name
, createDate
两列数据:
this.goodsRepository.find({
select: ['name', 'createDate']
})
3.搜索名字是'x2'并且 isActive 为'false'的数据
this.goodsRepository.find({
where: {
name: 'x2',
isActive: false
}
})
4.名字等于'x2'或者
等于'x3'都会被匹配出来:
this.goodsRepository.find({
where: [
{
name: "x2",
},
{
name: "x3",
},
],
});
5.排序, 以 name 降序, 创建时间升序排列
this.goodsRepository.find({
order: {
name: "DESC",
createDate: "ASC",
},
});
6.切割, skip
跳过 1 条, take
取出 3 条
this.goodsRepository.find({
skip: 1,
take: 3,
});
7.like
模糊查询名字里带有 2 的项, not
id 不是 1
this.goodsRepository.find({
where: {
id: Not(1),
name: Like("%2%"),
},
});
8.findAndCount 把满足条件的数据总数返回
数据是数组形式, [0]
是匹配到的数组, [1]
是符合条件的总数可能与[0]
的长度不相同。
this.goodsRepository.findAndCount({
select: ["name"],
});
7.findOne 只取配到的第一条, 并且返回形式为对象
只取配到的第一条, 并且返回形式为对象而非数组:
this.goodsRepository.findOne({
select: ["name"],
});
10.findByIds, 传入 id 组成的数组进行匹配
this.goodsRepository.findByIds([1, 2]);
11.前端获取一个需要分页的列表
用户传入需要模糊匹配的name
值, 以及当前第 n 页, 每页 s 条, 总数 total 条。
async getList(query) {
const { keyWords, page, pageSize } = query;
const [list, total] = await this.goodsRepository.findAndCount({
select: ['name', 'createDate'],
where: {
name: Like(`%${keyWords}%`)
},
skip: (page - 1) * pageSize,
take: pageSize
})
return {
list, total
}
}
3.新增与修改的 dto
yarn add class-validator class-transformer -S
1.新增
先建立一个简单的新增 dto 模型/share/src/modules/goods/dto/create-goods.dto.ts
:
import { IsNotEmpty, IsOptional, MaxLength } from "class-validator";
export class CreateGoodsDto {
@IsNotEmpty()
name: string;
@IsOptional()
@MaxLength(256)
remarks: string;
}
使用/share/src/modules/goods/goods.service.ts
create(body) {
const { name, remarks } = body;
const goodsDto = new CreateGoodsDto();
goodsDto.name = name;
goodsDto.remarks = remarks;
return this.goodsRepository.save(goodsDto)
}
2.修改
老样子, 先建立一份更新的 dto, 比如 name 是不可以更新的就不写 name, /share/src/modules/goods/dto/updata-goods.dto.ts
:
import { MaxLength } from "class-validator";
export class UpdataGoodsDto {
@MaxLength(256)
remarks: string;
}
在控制器里面就要限制用户传入的更新数据类型必须与 dto 相同/share/src/modules/goods/goods.controller.ts
:
@Put(':id')
updata(@Param('id') id: string, @Body() updateRoleDto: UpdataGoodsDto) {
return this.goodsService.updata(id, updateRoleDto);
}
先找到对应的数据, 再进行数据的更新/share/src/modules/goods/goods.service.ts
async updata(id, updataGoodsDto: UpdataGoodsDto) {
const goods = await this.goodsRepository.findOne(id)
Object.assign(goods, updataGoodsDto)
return this.goodsRepository.save(goods)
}