手把手带你入门 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 和手动数据转换上。希望这篇文章对你有所帮助,也欢迎把你的实践经验补充到评论里,一起进步!💡

复制代码
相关推荐
X***C8625 小时前
SpringBoot:几种常用的接口日期格式化方法
java·spring boot·后端
i***t9195 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
o***74175 小时前
基于SpringBoot的DeepSeek-demo 深度求索-demo 支持流式输出、历史记录
spring boot·后端·lua
9***J6285 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
S***q1925 小时前
Rust在系统工具中的内存安全给代码上了三道保险锁。但正是这种“编译期的严苛”,换来了运行时的安心。比如这段代码:
开发语言·后端·rust
v***7946 小时前
Spring Boot 热部署
java·spring boot·后端
追逐时光者6 小时前
C#/.NET/.NET Core优秀项目和框架2025年11月简报
后端·.net
码事漫谈6 小时前
Reactor网络模型深度解析:从并发困境说起
后端
T***u3336 小时前
Rust在Web中的 Web框架
开发语言·后端·rust
码事漫谈6 小时前
从理论到实践:构建你的AI语音桌面助手(Demo演示)
后端