第 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 |
动手练习
- 创建 User / Post / Comment 三个实体
- 给 User 添加 email 唯一索引
- 实现一个自定义 UserRepository
- 用 QueryBuilder 写一个分页查询