在上一篇文章中,我们介绍了 NestJS 的基础概念和核心功能。本文将深入探讨如何在 NestJS 中集成 TypeORM,实现数据库操作的最佳实践。
TypeORM 集成配置
1. 安装依赖
首先安装必要的依赖包:
npm install @nestjs/typeorm typeorm pg
# 如果使用 MySQL
# npm install @nestjs/typeorm typeorm mysql2
2. 数据库配置
// src/config/database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
export const databaseConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_DATABASE || 'nestjs_db',
entities: ['dist/**/*.entity{.ts,.js}'],
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV !== 'production',
ssl: process.env.DB_SSL === 'true',
};
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { databaseConfig } from './config/database.config';
@Module({
imports: [
TypeOrmModule.forRoot(databaseConfig),
// 其他模块
],
})
export class AppModule {}
实体设计与关系映射
1. 基础实体设计
// src/entities/base.entity.ts
import {
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn
} from 'typeorm';
export abstract class BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date;
}
// src/users/entities/user.entity.ts
import { Entity, Column, OneToMany } from 'typeorm';
import { BaseEntity } from '../entities/base.entity';
import { Post } from './post.entity';
@Entity('users')
export class User extends BaseEntity {
@Column({ length: 100 })
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
// src/posts/entities/post.entity.ts
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
import { BaseEntity } from '../entities/base.entity';
import { User } from './user.entity';
@Entity('posts')
export class Post extends BaseEntity {
@Column()
title: string;
@Column('text')
content: string;
@Column({ default: false })
published: boolean;
@ManyToOne(() => User, user => user.posts)
@JoinColumn({ name: 'author_id' })
author: User;
}
2. 关系映射策略
// src/users/entities/profile.entity.ts
import { Entity, Column, OneToOne, JoinColumn } from 'typeorm';
import { BaseEntity } from '../entities/base.entity';
import { User } from './user.entity';
@Entity('profiles')
export class Profile extends BaseEntity {
@Column()
avatar: string;
@Column('text')
bio: string;
@OneToOne(() => User)
@JoinColumn({ name: 'user_id' })
user: User;
}
// src/posts/entities/tag.entity.ts
import { Entity, Column, ManyToMany } from 'typeorm';
import { BaseEntity } from '../entities/base.entity';
import { Post } from './post.entity';
@Entity('tags')
export class Tag extends BaseEntity {
@Column({ unique: true })
name: string;
@ManyToMany(() => Post, post => post.tags)
posts: Post[];
}
// 更新 Post 实体,添加标签关系
@Entity('posts')
export class Post extends BaseEntity {
// ... 其他字段
@ManyToMany(() => Tag, tag => tag.posts)
@JoinTable({
name: 'posts_tags',
joinColumn: { name: 'post_id' },
inverseJoinColumn: { name: 'tag_id' }
})
tags: Tag[];
}
数据库操作实现
1. Repository 模式
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto, UpdateUserDto } from './dto';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return await this.usersRepository.save(user);
}
async findAll(): Promise<User[]> {
return await this.usersRepository.find({
relations: ['posts', 'profile']
});
}
async findOne(id: string): Promise<User> {
const user = await this.usersRepository.findOne({
where: { id },
relations: ['posts', 'profile']
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async update(id: string, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id);
Object.assign(user, updateUserDto);
return await this.usersRepository.save(user);
}
async remove(id: string): Promise<void> {
const user = await this.findOne(id);
await this.usersRepository.softRemove(user);
}
}
2. 查询构建器
// src/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Post } from './entities/post.entity';
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post)
private postsRepository: Repository<Post>
) {}
async findPublishedPosts() {
return await this.postsRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.leftJoinAndSelect('post.tags', 'tags')
.where('post.published = :published', { published: true })
.orderBy('post.createdAt', 'DESC')
.getMany();
}
async searchPosts(query: string) {
return await this.postsRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.where('post.title ILIKE :query OR post.content ILIKE :query', {
query: `%${query}%`
})
.orderBy('post.createdAt', 'DESC')
.getMany();
}
async getPostStats() {
return await this.postsRepository
.createQueryBuilder('post')
.select('author.name', 'authorName')
.addSelect('COUNT(*)', 'postCount')
.leftJoin('post.author', 'author')
.groupBy('author.name')
.getRawMany();
}
}
事务处理
1. 事务装饰器
// src/common/decorators/transaction.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { getManager } from 'typeorm';
export const Transaction = createParamDecorator(
async (data: unknown, ctx: ExecutionContext) => {
const queryRunner = getManager().connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
return queryRunner;
}
);
// 使用示例
@Post('transfer')
async transfer(
@Transaction() queryRunner,
@Body() transferDto: TransferDto
) {
try {
// 执行转账操作
await queryRunner.manager.update(Account,
transferDto.fromId,
{ balance: () => `balance - ${transferDto.amount}` }
);
await queryRunner.manager.update(Account,
transferDto.toId,
{ balance: () => `balance + ${transferDto.amount}` }
);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
2. 事务管理器
// src/common/services/transaction.service.ts
import { Injectable } from '@nestjs/common';
import { Connection, QueryRunner } from 'typeorm';
@Injectable()
export class TransactionService {
constructor(private connection: Connection) {}
async executeInTransaction<T>(
callback: (queryRunner: QueryRunner) => Promise<T>
): Promise<T> {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const result = await callback(queryRunner);
await queryRunner.commitTransaction();
return result;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}
// 使用示例
@Injectable()
export class PaymentService {
constructor(
private transactionService: TransactionService,
private ordersService: OrdersService
) {}
async processPayment(paymentDto: PaymentDto) {
return await this.transactionService.executeInTransaction(async queryRunner => {
const order = await this.ordersService.findOne(paymentDto.orderId);
// 更新订单状态
await queryRunner.manager.update(Order, order.id, {
status: 'paid'
});
// 创建支付记录
const payment = queryRunner.manager.create(Payment, {
order,
amount: paymentDto.amount
});
await queryRunner.manager.save(payment);
return payment;
});
}
}
数据库迁移
1. 迁移配置
// ormconfig.js
module.exports = {
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: ['dist/**/*.entity{.ts,.js}'],
migrations: ['dist/migrations/*{.ts,.js}'],
cli: {
migrationsDir: 'src/migrations'
}
};
// package.json
{
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"migration:create": "npm run typeorm migration:create -- -n",
"migration:generate": "npm run typeorm migration:generate -- -n",
"migration:run": "npm run typeorm migration:run",
"migration:revert": "npm run typeorm migration:revert"
}
}
2. 迁移示例
// src/migrations/1642340914321-CreateUsersTable.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class CreateUsersTable1642340914321 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()'
},
{
name: 'name',
type: 'varchar',
length: '100'
},
{
name: 'email',
type: 'varchar',
isUnique: true
},
{
name: 'password',
type: 'varchar'
},
{
name: 'created_at',
type: 'timestamp',
default: 'now()'
},
{
name: 'updated_at',
type: 'timestamp',
default: 'now()'
},
{
name: 'deleted_at',
type: 'timestamp',
isNullable: true
}
]
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
}
}
性能优化
1. 查询优化
// src/posts/posts.service.ts
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post)
private postsRepository: Repository<Post>
) {}
// 使用分页和缓存
async findAll(page = 1, limit = 10) {
const [posts, total] = await this.postsRepository.findAndCount({
relations: ['author', 'tags'],
skip: (page - 1) * limit,
take: limit,
cache: {
id: `posts_page_${page}`,
milliseconds: 60000 // 1分钟缓存
}
});
return {
data: posts,
meta: {
total,
page,
lastPage: Math.ceil(total / limit)
}
};
}
// 使用子查询优化
async findPopularPosts() {
return await this.postsRepository
.createQueryBuilder('post')
.leftJoinAndSelect('post.author', 'author')
.addSelect(subQuery => {
return subQuery
.select('COUNT(*)', 'commentCount')
.from('comments', 'comment')
.where('comment.postId = post.id');
}, 'commentCount')
.orderBy('commentCount', 'DESC')
.limit(10)
.getMany();
}
}
2. 索引优化
// src/posts/entities/post.entity.ts
@Entity('posts')
@Index(['title', 'content']) // 复合索引
export class Post extends BaseEntity {
@Column()
@Index() // 单列索引
title: string;
@Column('text')
content: string;
@Column()
@Index()
authorId: string;
// ... 其他字段
}
写在最后
本文详细介绍了 NestJS 中的数据库操作实践:
- TypeORM 的配置和集成
- 实体设计和关系映射
- Repository 模式的应用
- 事务处理方案
- 数据库迁移管理
- 性能优化策略
在下一篇文章中,我们将探讨 NestJS 的认证与授权实现。
如果觉得这篇文章对你有帮助,别忘了点个赞 👍