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

手把手带你入门 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 安装依赖

bash 复制代码
npm install @nestjs/typeorm typeorm pg

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

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

typeorm.config.ts

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

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 关系映射示例

ts 复制代码
// 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 多对多标签

ts 复制代码
@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

ts 复制代码
@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 示例

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

ts 复制代码
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)

ts 复制代码
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)

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

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


6. 管理数据库迁移

6.1 生成迁移

bash 复制代码
npm run typeorm migration:generate -- src/migrations/InitSchema -d typeorm.config.ts

package.json 示例:

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 迁移文件示例(片段)

ts 复制代码
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 执行迁移

bash 复制代码
npm run migration:run -- -d typeorm.config.ts

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


7. 与 NestJS 的深度整合

7.1 @nestjs/typeorm Module

ts 复制代码
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)

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

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


8. 单元 / e2e 测试实践

8.1 单元测试 ------ Mock Repository

ts 复制代码
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

ts 复制代码
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 found emitDecoratorMetadata + reflect-metadata
类型错误:Type 'T' does not satisfy ObjectLiteral mock 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 和手动数据转换上。希望这篇文章对你有所帮助,也欢迎把你的实践经验补充到评论里,一起进步!💡

复制代码
相关推荐
用户298698530147 分钟前
.NET 文档自动化:Spire.Doc 设置奇偶页页眉/页脚的最佳实践
后端·c#·.net
序安InToo38 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12339 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记41 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0542 分钟前
VS Code 配置 Markdown 环境
后端
navms1 小时前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang051 小时前
离线数仓的优化及重构
后端
Nyarlathotep01131 小时前
gin01:初探gin的启动
后端·go
JxWang051 小时前
安卓手机配置通用多屏协同及自动化脚本
后端
JxWang051 小时前
Windows Terminal 配置 oh-my-posh
后端