TypeORM 学习教程

一、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 排序,ASCDESC

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 时自动级联保存关联 Entity
  • onDelete: '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-tablematerialized-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
相关推荐
触底反弹7 天前
🚀 Next.js 全栈项目数据库连接实战:Drizzle ORM + Supabase 从零到跑通
postgresql·orm·next.js
姚杨7 天前
聊了三年 DDD,代码里全是贫血模型:老陈一句话点破,落地先过这几关
后端·orm
Darling噜啦啦8 天前
从零搭建单词管理系统:Next.js + Supabase + Drizzle ORM 全栈实战
数据库·orm·next.js
不好听6138 天前
ORM:让你不用再手写SQL语句
orm
柒和远方8 天前
V077:Next.js 后台的认证与权限防线:首个超级管理员的事务锁初始化、会话令牌哈希,与四道管理员保护规则
orm·next.js
__zRainy__10 天前
Node系列 · ORM:mysql 驱动程序
数据库·后端·mysql·node.js·orm
在水一缸1 个月前
深入浅出:Node.js 下一代 ORM 架构设计与实战解析
数据库·微服务·云原生·node.js·orm·架构设计
鱼听禅2 个月前
C#学习笔记-Entity Framework Core基础操作学习
c#·orm·ef core