Skip to content
第 176 / 250 章Node⏱ 10 分钟阅读

第 176 章:TypeORM 集成与实体

学习目标

  • 安装和配置 TypeORM
  • 学会定义 Entity
  • 掌握常用列装饰器
  • 理解 Repository 模式

一、TypeORM 简介

TypeORM 是 Node.js 最成熟的 ORM 之一,装饰器风格与 Java JPA/Hibernate 类似。

二、安装

bash
# 基础包
pnpm add @nestjs/typeorm typeorm

# 数据库驱动(按需)
pnpm add mysql2
pnpm add pg
pnpm add better-sqlite3

三、配置

3.1 同步配置

typescript
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',                              // 或 'postgres' / 'sqlite'
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'root',
      database: 'myapp',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,                          // ⚠️ 仅开发环境用,生产必须 false
      logging: true,
    }),
  ],
})
export class AppModule {}

3.2 异步配置(推荐)

typescript
// data-source.ts
import { DataSource } from 'typeorm';

export const dataSourceOptions: DataSourceOptions = {
  type: 'mysql',
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT),
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
  migrations: [__dirname + '/migrations/*{.ts,.js}'],
  synchronize: false,   // 生产关闭,改用 migration
};

// app.module.ts
@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'mysql',
        host: config.get('DB_HOST'),
        port: config.get('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASS'),
        database: config.get('DB_NAME'),
        entities: [__dirname + '/**/*.entity{.ts,.js}'],
        synchronize: config.get('NODE_ENV') !== 'production',
      }),
    }),
  ],
})
export class AppModule {}

3.3 多数据库

typescript
TypeOrmModule.forRootAsync({
  name: 'default',
  // ...
});

TypeOrmModule.forRootAsync({
  name: 'log',
  useFactory: () => ({
    type: 'postgres',
    // ...
  }),
});

// 注入特定数据库
@InjectRepository(Entity, 'log')
private repo: Repository<Entity>,

四、定义 Entity

4.1 基础实体

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

@Entity('users')   // 表名
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 50 })
  name: string;

  @Column({ unique: true })
  email: string;

  @Column({ default: 18 })
  age: number;

  @Column({ nullable: true })
  avatar?: string;

  @Column({ type: 'enum', enum: ['admin', 'user'], default: 'user' })
  role: 'admin' | 'user';

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()   // 软删除
  deletedAt?: Date;
}

4.2 列装饰器

typescript
@Column()                  // 默认 varchar
@Column({ type: 'text' })  // 长文本
@Column({ type: 'int' })
@Column({ type: 'decimal', precision: 10, scale: 2 })
@Column({ type: 'boolean' })
@Column({ type: 'date' })
@Column({ type: 'datetime' })
@Column({ type: 'json' })   // JSON 字段
@Column({ type: 'simple-array' })   // 数组 → 逗号分隔字符串
@Column({ type: 'simple-json' })    // 对象 → JSON 字符串

4.3 主键策略

typescript
// 自增
@PrimaryGeneratedColumn()
id: number;

// UUID
@PrimaryGeneratedColumn('uuid')
id: string;

// 自定义
@PrimaryColumn()
id: string;

// 复合主键
@Entity()
@Index(['tenantId', 'id'])
export class Order {
  @PrimaryColumn()
  tenantId: string;

  @PrimaryColumn()
  id: string;
}

五、索引

typescript
import { Index } from 'typeorm';

@Entity()
@Index(['email', 'tenantId'], { unique: true })   // 复合唯一索引
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  @Index()                  // 单列索引
  email: string;

  @Column()
  @Index('idx-name')        // 命名索引
  name: string;

  @Column()
  @Index({ fulltext: true })
  bio: string;              // 全文索引
}

六、枚举

typescript
// 直接用 TS enum
export enum UserRole {
  ADMIN = 'admin',
  USER = 'user',
}

@Entity()
export class User {
  @Column({
    type: 'enum',
    enum: UserRole,
    default: UserRole.USER,
  })
  role: UserRole;
}

// 用 union type
@Column({ type: 'enum', enum: ['admin', 'user'], default: 'user' })
role: 'admin' | 'user';

七、Repository 注入

7.1 模块注册

typescript
@Module({
  imports: [
    TypeOrmModule.forFeature([User, Post]),  // 注册多个 Entity
  ],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

7.2 Service 中使用

typescript
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  findAll() {
    return this.userRepository.find();
  }

  findOne(id: number) {
    return this.userRepository.findOneBy({ id });
  }

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

  update(id: number, dto: UpdateUserDto) {
    return this.userRepository.update(id, dto);
  }

  remove(id: number) {
    return this.userRepository.delete(id);
  }
}

八、自定义 Repository

8.1 创建自定义 Repository

typescript
// user.repository.ts
import { EntityRepository, Repository } from 'typeorm';
import { User } from './user.entity';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  findByEmail(email: string): Promise<User | null> {
    return this.findOne({ where: { email } });
  }

  findActive(): Promise<User[]> {
    return this.find({ where: { status: 'active' } });
  }

  async findWithPagination(page: number, size: number) {
    const [items, total] = await this.findAndCount({
      skip: (page - 1) * size,
      take: size,
    });
    return { items, total, page, size };
  }
}

8.2 注册使用

typescript
// module
TypeOrmModule.forFeature([UserRepository]),

// service
constructor(private userRepo: UserRepository) {}

findByEmail(email: string) {
  return this.userRepo.findByEmail(email);
}

九、QueryBuilder 入门

typescript
@Injectable()
export class UserService {
  constructor(@InjectRepository(User) private repo: Repository<User>) {}

  findByName(name: string) {
    return this.repo
      .createQueryBuilder('user')
      .where('user.name LIKE :name', { name: `%${name}%` })
      .getMany();
  }

  findAdults() {
    return this.repo
      .createQueryBuilder('user')
      .where('user.age >= :age', { age: 18 })
      .orderBy('user.age', 'DESC')
      .getMany();
  }
}

十、完整 CRUD 示例

typescript
// user.service.ts
@Injectable()
export class UserService {
  constructor(@InjectRepository(User) private repo: Repository<User>) {}

  async findAll(pagination: PaginationDto) {
    const [items, total] = await this.repo.findAndCount({
      skip: (pagination.page - 1) * pagination.size,
      take: pagination.size,
      order: { id: 'DESC' },
    });
    return { items, total, ...pagination };
  }

  async findOne(id: number) {
    const user = await this.repo.findOne({ where: { id } });
    if (!user) throw new NotFoundException(`User ${id} 不存在`);
    return user;
  }

  async create(dto: CreateUserDto) {
    const exists = await this.repo.findOne({ where: { email: dto.email } });
    if (exists) throw new ConflictException('邮箱已存在');
    return this.repo.save(this.repo.create(dto));
  }

  async update(id: number, dto: UpdateUserDto) {
    await this.findOne(id);
    await this.repo.update(id, dto);
    return this.findOne(id);
  }

  async remove(id: number) {
    const user = await this.findOne(id);
    await this.repo.softDelete(id);   // 软删除
    return user;
  }
}

十一、本章小结

装饰器作用
@Entity()标记实体
@Column()列定义
@PrimaryGeneratedColumn()主键
@CreateDateColumn()创建时间
@UpdateDateColumn()更新时间
@DeleteDateColumn()软删除
@Index()索引
@InjectRepository()注入 Repository

动手练习

  1. 创建 User / Post / Comment 三个实体
  2. 给 User 添加 email 唯一索引
  3. 实现一个自定义 UserRepository
  4. 用 QueryBuilder 写一个分页查询

推荐阅读


下一章:第 177 章:实体关系(一对一、一对多、多对多)

本站基于 VitePress 构建 · 由 Codebook 团队维护