一、TypeORM 快速入门
1. 什么是 TypeORM
TypeORM 是一个传统的 ORM(Object Relational Mapping)框架,把数据库表映射成 Entity 类,表字段映射成类属性,表之间的关联映射成 Entity 之间的关系。之后调用 Repository/EntityManager 的 find、save、delete 等 API 来做 CRUD,TypeORM 会自动生成对应的 SQL 语句。
2. 初始化项目
bash
npx typeorm@latest init --name my-project --database mysql
cd my-project
npm install --save mysql2
npm install
修改 data-source.ts 的数据库连接配置:
typescript
import "reflect-metadata"
import { DataSource } from "typeorm"
import { User } from "./entity/User"
export const AppDataSource = new DataSource({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "password",
database: "test",
synchronize: true, // 根据 Entity 自动同步建表
logging: true, // 打印生成的 SQL
entities: [User], // 注册 Entity
migrations: [], // 迁移文件
subscribers: [], // 生命周期订阅者
poolSize: 10, // 连接池最大连接数
connectorPackage: 'mysql2',
extra: {
authPlugin: 'sha256_password',
}
})
3. DataSource 配置项详解
| 配置项 | 作用 |
|---|---|
type |
数据库类型(mysql、postgres、oracle、sqlite 等) |
host / port |
数据库服务器地址和端口 |
username / password |
数据库用户名和密码 |
database |
操作的 database/schema |
synchronize |
是否自动根据 Entity 创建/修改表(开发环境用,生产环境禁用) |
logging |
是否打印 SQL |
entities |
注册 Entity 类(支持 class 和 glob 路径两种方式) |
migrations |
迁移文件路径 |
subscribers |
Entity 生命周期订阅者(insert/update/remove 前后可加入逻辑) |
poolSize |
数据库连接池最大连接数 |
connectorPackage |
使用的数据库驱动包 |
extra |
额外发送给驱动包的选项 |
4. Entity 与字段映射
typescript
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"
@Entity({ name: 't_aaa' }) // 指定表名
export class Aaa {
@PrimaryGeneratedColumn({ comment: '这是 id' })
id: number
@Column({
name: 'a_aa', // 数据库中的字段名
type: 'text', // 数据库类型
comment: '这是 aaa'
})
aaa: string
@Column({
unique: true, // 唯一约束
nullable: false, // NOT NULL
length: 10, // 长度
type: 'varchar',
default: 'bbb' // 默认值
})
bbb: string
@Column({ type: 'double' })
ccc: number
}
装饰器一览:
| 装饰器 | 作用 |
|---|---|
@Entity |
声明 Entity,name 指定表名 |
@PrimaryGeneratedColumn |
自增主键 |
@Column |
映射普通字段 |
@CreateDateColumn |
自动填充创建时间 |
@UpdateDateColumn |
自动填充更新时间 |
二、CRUD API 全览
1. EntityManager vs Repository
- EntityManager:管理所有 Entity 的增删改查,每个方法需要传入 Entity class
- Repository :通过
dataSource.getRepository(Entity)获取,专门操作单个 Entity,方法不需要传 Entity class
2. 新增与修改
typescript
// 新增(save 不传 id)
const user = new User();
user.firstName = "aaa";
user.lastName = "bbb";
user.age = 25;
await AppDataSource.manager.save(User, user);
// 修改(save 传 id,会先 select 再 update)
const user = new User();
user.id = 1;
user.firstName = "aaa111";
await AppDataSource.manager.save(User, user);
// 批量新增/修改
await AppDataSource.manager.save(User, [
{ firstName: 'ccc', lastName: 'ccc', age: 21 },
{ firstName: 'ddd', lastName: 'ddd', age: 22 },
]);
save会先查询一次数据库来确定是插入还是修改。而insert直接插入、update直接修改,不会先查询。
3. 删除
typescript
// delete:通过 id 删除
await AppDataSource.manager.delete(User, 1);
await AppDataSource.manager.delete(User, [2, 3]);
// remove:通过对象删除
const user = new User();
user.id = 1;
await AppDataSource.manager.remove(User, user);
delete传 id,remove传 entity 对象。
4. 查询
typescript
// find:查询多条
const users = await AppDataSource.manager.find(User);
// findBy:直接指定 where 条件
const users = await AppDataSource.manager.findBy(User, { age: 23 });
// findAndCount:查询多条并返回总数量
const [users, count] = await AppDataSource.manager.findAndCount(User);
// findAndCountBy:带条件的 findAndCount
const [users, count] = await AppDataSource.manager.findAndCountBy(User, { age: 23 });
// findOne:查询单条
const user = await AppDataSource.manager.findOne(User, {
select: { firstName: true, age: true },
where: { id: 4 },
order: { age: 'ASC' }
});
// findOneBy:直接指定 where 条件
const user = await AppDataSource.manager.findOneBy(User, { age: 23 });
// findOneOrFail / findOneByOrFail:找不到抛 EntityNotFoundError
const user = await AppDataSource.manager.findOneOrFail(User, { where: { id: 666 } });
find 的查询选项:
| 选项 | 作用 |
|---|---|
where |
条件过滤,支持 In([4, 8]) 等操作符 |
select |
指定返回的列 |
order |
排序,ASC 或 DESC |
5. 直接执行 SQL
typescript
// query:直接执行 SQL
const users = await AppDataSource.manager.query(
'SELECT * FROM user WHERE age IN (?, ?)', [21, 22]
);
// queryBuilder:构建复杂查询(如多表 JOIN)
const user = await AppDataSource.manager.createQueryBuilder()
.select("user")
.from(User, "user")
.where("user.age = :age", { age: 21 })
.getOne();
6. 事务
typescript
await AppDataSource.manager.transaction(async manager => {
await manager.save(User, { id: 4, firstName: 'eee', age: 20 });
await manager.save(User, { id: 5, firstName: 'fff', age: 21 });
});
7. getRepository 简化调用
typescript
const userRepo = AppDataSource.manager.getRepository(User);
const users = await userRepo.find({ where: { age: 23 } });
await userRepo.save({ id: 1, firstName: 'updated' });
三、一对一关系
1. Entity 映射
以用户(User)和身份证(IdCard)为例:
typescript
// IdCard Entity(维护外键的一方加 @JoinColumn)
@Entity({ name: 'id_card' })
export class IdCard {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 50, comment: '身份证号' })
cardName: string
@OneToOne(() => User, { onDelete: 'CASCADE', onUpdate: 'CASCADE', cascade: true })
@JoinColumn() // 外键列在 IdCard 表里维护
user: User
}
// User Entity(非外键方需要第二个参数指定外键位置)
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 50 })
firstName: string
@OneToOne(() => IdCard, idCard => idCard.user)
idCard: IdCard
}
2. 级联设置
@JoinColumn():声明外键列在当前 Entity 的表中cascade: true:save 时自动级联保存关联 EntityonDelete: 'CASCADE':数据库级联删除onUpdate: 'CASCADE':数据库级联更新
cascade是 TypeORM 级联(save 时自动关联保存),不是数据库的外键级联。
3. 关联 CRUD
typescript
// 创建(cascade: true 时只需保存 idCard)
const user = new User();
user.firstName = 'feng';
const idCard = new IdCard();
idCard.cardName = '1111111';
idCard.user = user;
await AppDataSource.manager.save(idCard); // user 会自动保存
// 查询(需声明 relations 或用 queryBuilder)
const ics = await AppDataSource.manager.find(IdCard, {
relations: { user: true }
});
// 查询方式二:queryBuilder
const ics = await AppDataSource.manager.getRepository(IdCard)
.createQueryBuilder("ic")
.leftJoinAndSelect("ic.user", "u")
.getMany();
// 修改(加 id 再 save)
const user = new User();
user.id = 1;
user.firstName = 'guang111';
const idCard = new IdCard();
idCard.id = 1;
idCard.cardName = '22222';
idCard.user = user;
await AppDataSource.manager.save(idCard);
// 删除(cascade: true + onDelete: 'CASCADE' 时只需删 user)
await AppDataSource.manager.delete(User, 1);
四、一对多关系
1. Entity 映射
以部门(Department)和员工(Employee)为例:
typescript
// 多的一方用 @ManyToOne
@Entity()
export class Employee {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 50 })
name: string
@ManyToOne(() => Department, department => department.employees, { cascade: true })
department: Department
}
// 一的一方用 @OneToMany
@Entity()
export class Department {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 50 })
name: string
@OneToMany(() => Employee, employee => employee.department, { cascade: true })
employees: Employee[]
}
一对多关系中,外键一定在"多"的那一方,不需要
@JoinColumn。可通过@JoinColumn({ name: 'custom_fk_name' })修改外键列名。
2. 注意事项
- 双方只能有一方设置
cascade: true,否则会无限循环 - 删除时如果设置了
onDelete: 'CASCADE'或SET NULL,只需删除"一"的一方,MySQL 会自动处理
3. 关联 CRUD
typescript
// 创建(从"一"方级联保存)
const d1 = new Department();
d1.name = '技术部';
d1.employees = [
{ name: '张三' } as Employee,
{ name: '李四' } as Employee,
];
await AppDataSource.manager.save(Department, d1);
// 查询
const deps = await AppDataSource.manager.find(Department, {
relations: { employees: true }
});
// 查询方式二:queryBuilder
const deps = await AppDataSource.manager.getRepository(Department)
.createQueryBuilder('d')
.leftJoinAndSelect('d.employees', 'e')
.getMany();
// 删除
await AppDataSource.manager.delete(Employee, deps[0].employees);
await AppDataSource.manager.delete(Department, deps[0].id);
五、多对多关系
1. Entity 映射
以文章(Article)和标签(Tag)为例:
typescript
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 100, comment: '文章标题' })
title: string
@Column({ type: 'text', comment: '文章内容' })
content: string
@ManyToMany(() => Tag, tag => tag.articles)
@JoinTable({ name: 'article_tag' }) // 指定中间表名(可选)
tags: Tag[]
}
@Entity()
export class Tag {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 100 })
name: string
@ManyToMany(() => Article, article => article.tags)
articles: Article[]
}
多对多需要中间表,通过
@JoinTable指定(可自定义表名,默认名为article_tags_tag)。双方都不维护外键,所以双方都需要第二个参数指定如何查找当前 Entity。
2. 关联 CRUD
typescript
// 创建(先保存 tag,再保存 article)
const t1 = new Tag(); t1.name = 'TypeScript';
const t2 = new Tag(); t2.name = 'NestJS';
const a1 = new Article(); a1.title = '入门'; a1.tags = [t1, t2];
await entityManager.save(t1);
await entityManager.save(t2);
await entityManager.save(a1);
// 查询
const articles = await entityManager.find(Article, {
relations: { tags: true }
});
// 修改(查出来后修改属性,再 save)
const article = await entityManager.findOne(Article, {
where: { id: 2 },
relations: { tags: true }
});
article.tags = article.tags.filter(tag => tag.name.includes('TS'));
await entityManager.save(article); // TypeORM 自动更新中间表
// 删除(CASCADE 级联删除)
await entityManager.delete(Article, 1);
六、在 NestJS 中集成 TypeORM
1. 安装依赖
bash
npm install --save @nestjs/typeorm typeorm mysql2
2. 配置 TypeOrmModule
typescript
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user/entities/user.entity';
@Module({
imports: [
TypeOrmModule.forRoot({
type: "mysql",
host: "localhost",
port: 3306,
username: "root",
password: "password",
database: "test",
synchronize: true,
logging: true,
entities: [User],
poolSize: 10,
connectorPackage: 'mysql2',
extra: { authPlugin: 'sha256_password' }
}),
UserModule,
],
})
export class AppModule {}
3. 使用 EntityManager 方式
typescript
// user.service.ts
@Injectable()
export class UserService {
@InjectEntityManager()
private manager: EntityManager;
create(dto: CreateUserDto) {
return this.manager.save(User, dto);
}
findAll() {
return this.manager.find(User);
}
findOne(id: number) {
return this.manager.findOne(User, { where: { id } });
}
update(id: number, dto: UpdateUserDto) {
return this.manager.save(User, { id, ...dto });
}
remove(id: number) {
return this.manager.delete(User, id);
}
}
4. 使用 Repository 方式(推荐)
typescript
// user.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])], // 注册 User 的 Repository
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
// user.service.ts
@Injectable()
export class UserService {
@InjectRepository(User)
private userRepo: Repository<User>;
create(dto: CreateUserDto) {
return this.userRepo.save(dto);
}
findAll() {
return this.userRepo.find();
}
findOne(id: number) {
return this.userRepo.findOne({ where: { id } });
}
update(id: number, dto: UpdateUserDto) {
return this.userRepo.save({ id, ...dto });
}
remove(id: number) {
return this.userRepo.delete(id);
}
}
5. 实现原理
TypeOrmModule.forRoot():创建 DataSource 和 EntityManager,作为全局模块导出(@Global),所以任意地方都可以注入TypeOrmModule.forFeature([User]):通过dataSource.getRepository(User)获取 Repository,作为模块内的 provider 导出
七、Tree Entity --- 任意层级关系
1. 适用场景
多级分类(商品分类、地区层级、组织架构等)不需要多个表,一个表通过 parentId 自关联即可。
2. Entity 定义
typescript
import { Entity, Column, PrimaryGeneratedColumn, Tree, TreeChildren, TreeParent,
CreateDateColumn, UpdateDateColumn } from "typeorm";
@Entity()
@Tree('closure-table') // 或 'materialized-path'
export class City {
@PrimaryGeneratedColumn()
id: number
@Column()
name: string
@TreeChildren()
children: City[]
@TreeParent()
parent: City
@CreateDateColumn()
createDate: Date
@UpdateDateColumn()
updateDate: Date
}
3. 存储模式
| 模式 | 说明 |
|---|---|
closure-table |
两个表存储(主表 + closure 表),查询性能好 |
materialized-path |
一个表 + mpath 字段存储路径,更简单 |
adjacency-list |
单表 parentId,有性能限制 |
nested-set |
已废弃,不推荐 |
推荐使用 closure-table 或 materialized-path。
4. 树形查询 API
typescript
const treeRepo = entityManager.getTreeRepository(City);
// 查询完整树结构
const trees = await treeRepo.findTrees();
// 查询所有根节点
const roots = await treeRepo.findRoots();
// 查询某节点的所有后代(树形结构)
const city = await entityManager.findOne(City, { where: { name: '云南' } });
const descendants = await treeRepo.findDescendantsTree(city);
// 查询某节点的所有祖先(树形结构)
const ancestors = await treeRepo.findAncestorsTree(city);
// 扁平结构查询(同 find / findDescendants / findAncestors)
const flat = await treeRepo.find();
const flatDesc = await treeRepo.findDescendants(city);
const flatAnc = await treeRepo.findAncestors(city);
// 计数
const ancestorCount = await treeRepo.countAncestors(city);
const descendantCount = await treeRepo.countDescendants(city);
5. 插入树形数据
typescript
const city = new City();
city.name = '华北';
await entityManager.save(city);
const child = new City();
child.name = '山东';
const parent = await entityManager.findOne(City, { where: { name: '华北' } });
child.parent = parent;
await entityManager.save(child);
八、Migration 迁移
1. 为什么需要 Migration
synchronize: true 在开发时很方便,但生产环境非常危险 ------ 删除 Entity 会直接删掉对应的列和数据,且无法恢复。
生产环境必须关闭 synchronize,使用 migration 管理表结构变更。
2. 四个核心命令
| 命令 | 作用 |
|---|---|
migration:create |
生成空白 migration 文件 |
migration:generate |
连接数据库,对比 Entity 和表的差异,自动生成 migration |
migration:run |
执行 migration(根据 migrations 表记录判断执行哪些) |
migration:revert |
撤销上次 migration,执行 down 方法并删除记录 |
3. 基本使用
bash
# 创建空白 migration
npx ts-node ./node_modules/typeorm/cli migration:create ./src/migration/Aaa
# 自动生成 migration(推荐)
npx ts-node ./node_modules/typeorm/cli migration:generate ./src/migration/Aaa -d ./src/data-source.ts
# 执行 migration
npx ts-node ./node_modules/typeorm/cli migration:run -d ./src/data-source.ts
# 撤销 migration
npx ts-node ./node_modules/typeorm/cli migration:revert -d ./src/data-source.ts
4. Migration 文件结构
typescript
import { MigrationInterface, QueryRunner } from "typeorm";
export class Aaa1708136448263 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE user (
id int NOT NULL AUTO_INCREMENT,
firstName varchar(255) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
// revert 时执行(如 DROP TABLE)
}
}
5. 在 NestJS 项目中使用 Migration
创建 data-source.ts(供 migration 命令使用):
typescript
// src/data-source.ts
import { DataSource } from "typeorm";
import { Article } from "./article/entities/article.entity";
import { config } from 'dotenv';
config({ path: 'src/.env' }); // 读取 .env 配置
export default new DataSource({
type: "mysql",
host: process.env.mysql_server_host,
port: +process.env.mysql_server_port,
username: process.env.mysql_server_username,
password: process.env.mysql_server_password,
database: process.env.mysql_server_database,
synchronize: false, // 关闭自动同步
logging: true,
entities: [Article],
migrations: ['src/migrations/**.ts'],
connectorPackage: 'mysql2',
extra: { authPlugin: 'sha256_password' }
});
package.json 配置 npm scripts:
json
{
"typeorm": "ts-node ./node_modules/typeorm/cli",
"migration:create": "npm run typeorm -- migration:create",
"migration:generate": "npm run typeorm -- migration:generate -d ./src/data-source.ts",
"migration:run": "npm run typeorm -- migration:run -d ./src/data-source.ts",
"migration:revert": "npm run typeorm -- migration:revert -d ./src/data-source.ts"
}
AppModule 同步关闭 synchronize:
typescript
TypeOrmModule.forRoot({
synchronize: false, // 生产环境必须关闭
// ...其他配置
})
初始化数据(Seed): migration:generate 只生成表结构变更的 SQL,数据插入需要用 migration:create 手动创建 migration 并填入 INSERT INTO 语句。
总结
| 章节 | 核心内容 |
|---|---|
| 入门 | DataSource 配置、Entity 装饰器、基本 CRUD |
| CRUD API | save/insert/update/delete/remove/find/findBy/findOne/findOneBy/findAndCount/query/queryBuilder/transaction/getRepository |
| 一对一 | @OneToOne + @JoinColumn + cascade + onDelete |
| 一对多 | @ManyToOne + @OneToMany,外键在多的一方,双方只能一方 cascade |
| 多对多 | @ManyToMany + @JoinTable,中间表,双方都需要第二个参数 |
| Nest 集成 | TypeOrmModule.forRoot / forFeature,注入 EntityManager 或 Repository |
| Tree Entity | @Tree + @TreeParent + @TreeChildren,closure-table / materialized-path,findTrees/findRoots/findDescendantsTree/findAncestorsTree |
| Migration | synchronize 生产禁用,migration:create/generate/run/revert,data-source.ts + npm scripts |