手把手带你入门 TypeORM —— 面向新手的实战指南

286 阅读6分钟

手把手带你入门 TypeORM —— 面向新手的实战指南

本文面向第一次接触 ORM / TypeORM 的同学,结合真实项目(NestJS + PostgreSQL),从“为什么需要 ORM”讲起,一步步构建、迁移、查询、测试,帮你真正用起来。


1. 背景:为什么需要 ORM?

方式特点存在的问题
直接写 SQL灵活、性能好每次都写 SQL,易错、难维护,不利于重构
DAO 自封装把 SQL 包在函数里仍需自己管理连接、事务、缓存等细节
ORM“对象 ↔ 数据库”自动映射,类型安全需学习曲线,但维护成本低

ORM(Object Relational Mapping) 是一套让你用“面向对象”的方式操作关系型数据库的工具。开发者只需定义实体类、调用方法,ORM 就会帮你生成 SQL、执行、转换结果。

TypeORM 是 Node.js 世界最常用的 ORM 之一,具备:

  • TypeScript 支持、类型安全;
  • 装饰器定义实体,代码直观;
  • 多数据库支持(PostgreSQL/MySQL/SQLite 等);
  • 自动迁移、事务、关系映射等高级特性;
  • NestJS 官方推荐(@nestjs/typeorm 集成)。

2. 快速上手:搭建 TypeORM + NestJS 环境

2.1 安装依赖

npm install @nestjs/typeorm typeorm pg

如果数据库是 MySQL,则安装 mysql2;SQLite 则安装 sqlite3

2.2 创建数据源配置(支持 CLI / 迁移)

typeorm.config.ts

import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
import { join } from 'path';

config(); // 读取 .env

export const AppDataSource = new DataSource({
  type: 'postgres',
  url: process.env.DATABASE_URL,
  synchronize: false,           // 推荐迁移管理
  logging: false,
  entities: [join(__dirname, 'src/**/*.entity.{ts,js}')],
  migrations: [join(__dirname, 'src/migrations/*.{ts,js}')],
});

synchronize: false,避免上线环境自动改表。我们用“迁移”来管理结构变化。


3. 定义实体(Entity)—— 模型驱动数据库

3.1 基础实体

src/modules/users/entities/user.entity.ts

import {
  Entity, PrimaryGeneratedColumn, Column,
  CreateDateColumn, UpdateDateColumn, Index,
} from 'typeorm';

@Entity({ name: 'users' })
export class User {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column({ length: 128, unique: true })
  @Index('idx_users_email')
  email!: string;

  @Column({ length: 255 })
  password!: string;

  @Column({ length: 64 })
  name!: string;

  @Column({ length: 16, default: 'active' })
  status!: 'active' | 'disabled';

  @CreateDateColumn({ name: 'created_at' })
  createdAt!: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt!: Date;
}

常用装饰器:

装饰器作用
@Entity()声明实体类
@PrimaryGeneratedColumn主键,自增/UUID
@Column()普通字段
@CreateDateColumn自动维护创建时间
@UpdateDateColumn自动维护更新时间
@ManyToOne, @OneToMany, @ManyToMany关系映射

3.2 关系映射示例

// Category 与 Item 是 一对多/多对一
@Entity({ name: 'categories' })
export class Category {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column({ length: 64 })
  name!: string;

  @OneToMany(() => Item, (item) => item.category)
  items!: Item[];
}

@Entity({ name: 'items' })
export class Item {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @ManyToOne(() => Category, (category) => category.items, {
    nullable: false,
    onDelete: 'RESTRICT',
  })
  category!: Category;

  @Column({ length: 128 })
  name!: string;
}

3.3 多对多标签

@Entity({ name: 'tags' })
export class Tag {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column({ length: 64, unique: true })
  name!: string;

  @ManyToMany(() => Item, (item) => item.tags)
  items!: Item[];
}

@Entity({ name: 'items' })
export class Item {
  // ...
  @ManyToMany(() => Tag, (tag) => tag.items)
  @JoinTable({
    name: 'item_tags',
    joinColumn: { name: 'item_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'tag_id', referencedColumnName: 'id' },
  })
  tags!: Tag[];
}

4. 增删改查:Repository & QueryBuilder

TypeORM 提供两种操作方式:Repository APIQueryBuilder

4.1 Repository API

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepo: Repository<User>,
  ) {}

  findByEmail(email: string) {
    return this.usersRepo.findOne({ where: { email } });
  }

  async create(dto: CreateUserDto) {
    const user = this.usersRepo.create(dto);
    return this.usersRepo.save(user);
  }

  async updateStatus(id: string, status: User['status']) {
    await this.usersRepo.update(id, { status });
    return this.findById(id);
  }

  async remove(id: string) {
    await this.usersRepo.delete(id);
  }
}

常用方法:

方法说明
find()查询列表
findOne()单个查询
create() + save()新建并保存
update() / delete()批量更新/删除
count()统计

4.2 QueryBuilder 示例

适合复杂筛选或多表关联:

const qb = this.itemsRepo
  .createQueryBuilder('item')
  .leftJoinAndSelect('item.category', 'category')
  .leftJoinAndSelect('item.tags', 'tag')
  .where('item.name ILIKE :keyword', { keyword: `%${keyword}%` });

if (categoryId) {
  qb.andWhere('category.id = :categoryId', { categoryId });
}

if (tagIds?.length) {
  qb.andWhere('tag.id IN (:...tagIds)', { tagIds }).distinct(true);
}

const items = await qb.orderBy('item.created_at', 'DESC').getMany();

5. 事务与并发控制

5.1 事务(Transaction)

await this.dataSource.transaction(async (manager) => {
  const user = await manager.findOne(User, { where: { id: userId } });

  const stock = manager.create(Stock, { ... });
  await manager.save(stock);

  // 事务内调用其他服务也可以传 EntityManager 进来
});

5.2 悲观锁(Pessimistic Lock)

const stock = await manager.findOne(Stock, {
  where: { id },
  lock: { mode: 'pessimistic_write' },
});

多用户同时调整库存时,锁住记录,保证数据一致性。


6. 管理数据库迁移

6.1 生成迁移

npm run typeorm migration:generate -- src/migrations/InitSchema -d typeorm.config.ts

package.json 示例:

{
  "scripts": {
    "typeorm": "node --loader ts-node/esm ./node_modules/typeorm/cli.js",
    "migration:generate": "npm run typeorm -- migration:generate",
    "migration:run": "npm run typeorm -- migration:run",
    "migration:revert": "npm run typeorm -- migration:revert"
  }
}

6.2 迁移文件示例(片段)

export class InitSchema1234567890123 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE "users" (...)
    `);
    await queryRunner.query(`
      CREATE TABLE "categories" (...)
    `);
    // ...
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "users"`);
    await queryRunner.query(`DROP TABLE "categories"`);
    // ...
  }
}

6.3 执行迁移

npm run migration:run -- -d typeorm.config.ts

用于本地开发 / CI / 上线部署。


7. 与 NestJS 的深度整合

7.1 @nestjs/typeorm Module

TypeOrmModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService<{ app: AppConfigType }, true>) => {
    const { database } = config.getOrThrow<AppConfigType>('app');
    return {
      type: 'postgres',
      url: database.url,
      autoLoadEntities: true,
      synchronize: false,
      logging: database.logging,
    };
  },
});

7.2 测试用内存数据库(e2e)

TypeOrmModule.forRoot({
  type: 'sqlite',
  database: ':memory:',
  entities: [User, Category, Item],
  synchronize: true,
});

让 e2e 测试可以独立执行,不依赖外部数据库。


8. 单元 / e2e 测试实践

8.1 单元测试 —— Mock Repository

const moduleRef = await Test.createTestingModule({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: {
        findOne: jest.fn(),
        create: jest.fn(),
        save: jest.fn(),
        update: jest.fn(),
      },
    },
  ],
}).compile();
  • createQueryBuilder 也可以通过 jest 模拟。
  • 配合类验证器 DTO 可测试参数校验逻辑。

8.2 e2e 测试 —— Supertest + SQLite

const moduleFixture: TestingModule = await Test.createTestingModule({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: ':memory:',
      entities: [User, Category, Item],
      synchronize: true,
    }),
    ItemsModule,
  ],
}).compile();

app = moduleFixture.createNestApplication();
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());
await app.init();

const server = app.getHttpServer();
await request(server)
  .post('/items')
  .send({ ... })
  .expect(201);

9. 项目中遇到的常见问题 & 解决方案

问题描述解决办法
实体改变但忘记迁移,导致线上/本地结构不一致统一使用迁移管理,严禁 synchronize: true 上线
TypeError: decorator metadata not foundemitDecoratorMetadata + reflect-metadata
类型错误:Type 'T' does not satisfy ObjectLiteralmock QueryBuilder 时把泛型约束成 <T extends ObjectLiteral>
多对多关系 JoinTable 属性不支持 onDelete在迁移里设置外键行为,而不是在装饰器里
NestJS 热重载时端口占用(EADDRINUSE)关闭旧进程或启用 --watch 时配置 webpack 热更新

10. 实战经验与最佳实践

  1. 配置集中化:统一在 configuration.ts 管理数据库、Redis、MQ、邮件等配置,方便多环境部署。
  2. DTO + 验证器:结合 class-validatorclass-transformer,确保输入数据合法,再交给 ORM。
  3. 仓储模式(Repository Pattern):业务逻辑封装在 Service,Repository 只做数据访问。
  4. 幂等 / 事务控制:库存、订单类操作要用事务,利用悲观锁或乐观锁确保一致性。
  5. 测试优先:单测保证逻辑正确,e2e 保证接口联通,避免改动带来回归问题。
  6. 迁移文档化:每次迁移都记录在 docs/progress-log.mddocs/mq-explanation.md 等文档里,方便回顾。
  7. 多模块协作:Auth、Users、Items、Stock、Tags、Notifications 等模块各司其职,通过 TypeORM 实体串联。

11. 写在最后

TypeORM 不只是“简化 CRUD”,它能帮助你:

  • 用类型安全的方式管理复杂的数据库;
  • 快速构建实体关系模型,降低维护成本;
  • 统一管理数据库变更(迁移),支持 CI/CD;
  • 与 NestJS 深度融合,搭建企业级服务。

掌握 TypeORM,你就能把更多精力放在业务逻辑,而不是重复的 SQL 和手动数据转换上。希望这篇文章对你有所帮助,也欢迎把你的实践经验补充到评论里,一起进步!💡