# NestJS 与 PostgreSQL 集成方案 ## 一、兼容性说明 ### ✅ PostgreSQL 完全支持 NestJS **简短回答**:PostgreSQL 与 NestJS **完全兼容**,并且是 NestJS 官方推荐和广泛使用的数据库组合。 ### 1.1 官方支持 NestJS 官方文档中明确支持 PostgreSQL,主要通过以下 ORM 库: 1. **TypeORM** - NestJS 官方推荐,最常用 2. **Prisma** - 现代化 ORM,类型安全 3. **Sequelize** - 成熟的 ORM 库 4. **Knex.js** - SQL 查询构建器 5. **原生 pg 驱动** - 直接使用 PostgreSQL 驱动 ### 1.2 为什么选择 NestJS + PostgreSQL 1. **完美兼容**:NestJS 的依赖注入和模块化架构与 PostgreSQL 配合良好 2. **TypeScript 支持**:两者都原生支持 TypeScript,类型安全 3. **生态成熟**:有丰富的库和工具支持 4. **性能优秀**:PostgreSQL 性能强大,NestJS 框架高效 5. **开发体验好**:代码生成、迁移工具完善 --- ## 二、推荐方案对比 ### 2.1 TypeORM(最推荐) **优势:** - ✅ NestJS 官方文档示例主要使用 TypeORM - ✅ 装饰器语法,与 NestJS 风格一致 - ✅ 支持数据库迁移(Migration) - ✅ 支持实体关系映射(ORM) - ✅ 支持事务管理 - ✅ 支持查询构建器 **适用场景:** - 需要完整的 ORM 功能 - 团队熟悉装饰器语法 - 需要数据库迁移管理 ### 2.2 Prisma(现代化选择) **优势:** - ✅ 类型安全,TypeScript 支持最好 - ✅ 代码生成,自动生成类型定义 - ✅ 迁移工具强大 - ✅ 性能优秀 - ✅ 开发体验极佳 **适用场景:** - 重视类型安全 - 需要快速开发 - 团队喜欢现代化工具 ### 2.3 Sequelize(成熟稳定) **优势:** - ✅ 非常成熟,生态丰富 - ✅ 文档完善 - ✅ 支持多种数据库 **劣势:** - ⚠️ TypeScript 支持不如 Prisma - ⚠️ 语法相对传统 --- ## 三、TypeORM 集成方案(推荐) ### 3.1 安装依赖 ```bash npm install @nestjs/typeorm typeorm pg npm install --save-dev @types/pg ``` ### 3.2 项目结构 ``` src/ ├── app.module.ts ├── database/ │ ├── database.module.ts │ └── database.config.ts ├── users/ │ ├── entities/ │ │ └── user.entity.ts │ ├── users.module.ts │ ├── users.service.ts │ └── users.controller.ts ├── accounts/ │ ├── entities/ │ │ └── account.entity.ts │ └── ... └── transactions/ ├── entities/ │ └── transaction.entity.ts └── ... ``` ### 3.3 配置数据库连接 #### database/database.config.ts ```typescript import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { ConfigService } from '@nestjs/config'; export const getDatabaseConfig = ( configService: ConfigService, ): TypeOrmModuleOptions => ({ type: 'postgres', host: configService.get('DB_HOST', 'localhost'), port: configService.get('DB_PORT', 5432), username: configService.get('DB_USERNAME', 'postgres'), password: configService.get('DB_PASSWORD', 'password'), database: configService.get('DB_NAME', 'vestmind'), entities: [__dirname + '/../**/*.entity{.ts,.js}'], synchronize: configService.get('NODE_ENV') !== 'production', // 生产环境设为 false logging: configService.get('NODE_ENV') === 'development', migrations: [__dirname + '/../migrations/**/*{.ts,.js}'], migrationsRun: false, ssl: configService.get('DB_SSL', false), }); ``` #### database/database.module.ts ```typescript import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { getDatabaseConfig } from './database.config'; @Module({ imports: [ TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: getDatabaseConfig, inject: [ConfigService], }), ], }) export class DatabaseModule {} ``` #### app.module.ts ```typescript import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from './database/database.module'; import { UsersModule } from './users/users.module'; import { AccountsModule } from './accounts/accounts.module'; import { TransactionsModule } from './transactions/transactions.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), DatabaseModule, UsersModule, AccountsModule, TransactionsModule, ], }) export class AppModule {} ``` ### 3.4 实体定义示例 #### users/entities/user.entity.ts ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany, } from 'typeorm'; import { Account } from '../../accounts/entities/account.entity'; @Entity('users') export class User { @PrimaryGeneratedColumn('increment') id: number; @Column({ type: 'varchar', length: 50, unique: true }) username: string; @Column({ type: 'varchar', length: 100, unique: true, nullable: true }) email: string; @Column({ type: 'varchar', length: 20, unique: true, nullable: true }) phone: string; @Column({ type: 'varchar', length: 255 }) passwordHash: string; @Column({ type: 'varchar', length: 50, nullable: true }) nickname: string; @Column({ type: 'varchar', length: 255, nullable: true }) avatarUrl: string; @Column({ type: 'varchar', length: 20, default: 'active' }) status: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; @Column({ type: 'timestamp', nullable: true }) lastLoginAt: Date; // 关系 @OneToMany(() => Account, (account) => account.user) accounts: Account[]; } ``` #### accounts/entities/account.entity.ts ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, OneToMany, JoinColumn, } from 'typeorm'; import { User } from '../../users/entities/user.entity'; import { Position } from '../../positions/entities/position.entity'; import { Transaction } from '../../transactions/entities/transaction.entity'; @Entity('accounts') export class Account { @PrimaryGeneratedColumn('increment') id: number; @Column({ type: 'int' }) userId: number; @Column({ type: 'varchar', length: 100 }) name: string; @Column({ type: 'varchar', length: 20 }) type: string; // stock, fund, cash, mixed @Column({ type: 'varchar', length: 10, default: 'CNY' }) currency: string; @Column({ type: 'text', nullable: true }) description: string; @Column({ type: 'varchar', length: 20, default: 'active' }) status: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; // 关系 @ManyToOne(() => User, (user) => user.accounts) @JoinColumn({ name: 'userId' }) user: User; @OneToMany(() => Position, (position) => position.account) positions: Position[]; @OneToMany(() => Transaction, (transaction) => transaction.account) transactions: Transaction[]; } ``` #### transactions/entities/transaction.entity.ts ```typescript import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index, } from 'typeorm'; import { User } from '../../users/entities/user.entity'; import { Account } from '../../accounts/entities/account.entity'; import { Position } from '../../positions/entities/position.entity'; import { TradingPlan } from '../../trading-plans/entities/trading-plan.entity'; @Entity('transactions') @Index(['userId', 'date', 'createdAt']) @Index(['accountId']) @Index(['positionId']) export class Transaction { @PrimaryGeneratedColumn('increment') id: number; @Column({ type: 'int' }) userId: number; @Column({ type: 'int' }) accountId: number; @Column({ type: 'int', nullable: true }) positionId: number; @Column({ type: 'int', nullable: true }) tradingPlanId: number; @Column({ type: 'varchar', length: 20 }) type: string; // buy, sell, dividend, split, bonus, rights, deposit, withdraw @Column({ type: 'date' }) date: Date; @Column({ type: 'varchar', length: 20, nullable: true }) symbol: string; @Column({ type: 'varchar', length: 100, nullable: true }) name: string; @Column({ type: 'varchar', length: 20, nullable: true }) market: string; @Column({ type: 'decimal', precision: 18, scale: 4, nullable: true }) shares: number; @Column({ type: 'decimal', precision: 18, scale: 4, nullable: true }) price: number; @Column({ type: 'decimal', precision: 18, scale: 2 }) amount: number; @Column({ type: 'decimal', precision: 18, scale: 2, default: 0 }) fee: number; @Column({ type: 'varchar', length: 10, default: 'CNY' }) currency: string; @Column({ type: 'decimal', precision: 10, scale: 6, default: 1 }) exchangeRate: number; @Column({ type: 'jsonb', nullable: true }) metadata: Record; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; // 关系 @ManyToOne(() => User) @JoinColumn({ name: 'userId' }) user: User; @ManyToOne(() => Account) @JoinColumn({ name: 'accountId' }) account: Account; @ManyToOne(() => Position, { nullable: true }) @JoinColumn({ name: 'positionId' }) position: Position; @ManyToOne(() => TradingPlan, { nullable: true }) @JoinColumn({ name: 'tradingPlanId' }) tradingPlan: TradingPlan; } ``` ### 3.5 Service 使用示例 #### transactions/transactions.service.ts ```typescript import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, Between } from 'typeorm'; import { Transaction } from './entities/transaction.entity'; import { CreateTransactionDto } from './dto/create-transaction.dto'; @Injectable() export class TransactionsService { constructor( @InjectRepository(Transaction) private transactionRepository: Repository, ) {} async create(createTransactionDto: CreateTransactionDto): Promise { const transaction = this.transactionRepository.create(createTransactionDto); return await this.transactionRepository.save(transaction); } async findAll(userId: number, page: number = 1, limit: number = 20) { const [data, total] = await this.transactionRepository.findAndCount({ where: { userId }, relations: ['account', 'position', 'tradingPlan'], order: { date: 'DESC', createdAt: 'DESC' }, skip: (page - 1) * limit, take: limit, }); return { data, total, page, limit, totalPages: Math.ceil(total / limit), }; } async findOne(id: number, userId: number): Promise { return await this.transactionRepository.findOne({ where: { id, userId }, relations: ['account', 'position', 'tradingPlan'], }); } async findByDateRange( userId: number, startDate: Date, endDate: Date, ): Promise { return await this.transactionRepository.find({ where: { userId, date: Between(startDate, endDate), }, order: { date: 'DESC' }, }); } async update(id: number, userId: number, updateData: Partial) { await this.transactionRepository.update({ id, userId }, updateData); return this.findOne(id, userId); } async remove(id: number, userId: number): Promise { await this.transactionRepository.delete({ id, userId }); } } ``` ### 3.6 Module 配置 #### transactions/transactions.module.ts ```typescript import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TransactionsService } from './transactions.service'; import { TransactionsController } from './transactions.controller'; import { Transaction } from './entities/transaction.entity'; @Module({ imports: [TypeOrmModule.forFeature([Transaction])], controllers: [TransactionsController], providers: [TransactionsService], exports: [TransactionsService], }) export class TransactionsModule {} ``` ### 3.7 事务处理示例 ```typescript import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; import { Transaction } from './entities/transaction.entity'; import { Position } from '../positions/entities/position.entity'; import { CashAccount } from '../cash-accounts/entities/cash-account.entity'; @Injectable() export class TradingService { constructor( private dataSource: DataSource, @InjectRepository(Transaction) private transactionRepository: Repository, ) {} async buyStock(buyDto: CreateBuyDto) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // 1. 创建交易记录 const transaction = this.transactionRepository.create({ ...buyDto, type: 'buy', }); await queryRunner.manager.save(transaction); // 2. 更新持仓 const position = await queryRunner.manager.findOne(Position, { where: { accountId: buyDto.accountId, symbol: buyDto.symbol }, }); if (position) { // 更新现有持仓 const newShares = position.shares + buyDto.shares; const newCostPrice = (position.shares * position.costPrice + buyDto.shares * buyDto.price + buyDto.fee) / newShares; position.shares = newShares; position.costPrice = newCostPrice; await queryRunner.manager.save(position); } else { // 创建新持仓 const newPosition = queryRunner.manager.create(Position, { accountId: buyDto.accountId, symbol: buyDto.symbol, name: buyDto.name, market: buyDto.market, shares: buyDto.shares, costPrice: buyDto.price, currentPrice: buyDto.price, }); await queryRunner.manager.save(newPosition); } // 3. 更新现金账户 const cashAccount = await queryRunner.manager.findOne(CashAccount, { where: { accountId: buyDto.accountId }, }); cashAccount.balance -= buyDto.amount + buyDto.fee; await queryRunner.manager.save(cashAccount); await queryRunner.commitTransaction(); return transaction; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } } ``` --- ## 四、Prisma 集成方案(备选) ### 4.1 安装依赖 ```bash npm install @prisma/client npm install -D prisma npx prisma init ``` ### 4.2 Prisma Schema 示例 #### prisma/schema.prisma ```prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id BigInt @id @default(autoincrement()) username String @unique @db.VarChar(50) email String? @unique @db.VarChar(100) phone String? @unique @db.VarChar(20) passwordHash String @db.VarChar(255) nickname String? @db.VarChar(50) avatarUrl String? @db.VarChar(255) status String @default("active") @db.VarChar(20) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lastLoginAt DateTime? accounts Account[] transactions Transaction[] @@map("users") } model Account { id BigInt @id @default(autoincrement()) userId BigInt name String @db.VarChar(100) type String @db.VarChar(20) currency String @default("CNY") @db.VarChar(10) description String? status String @default("active") @db.VarChar(20) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) positions Position[] transactions Transaction[] @@map("accounts") } model Transaction { id BigInt @id @default(autoincrement()) userId BigInt accountId BigInt positionId BigInt? tradingPlanId BigInt? type String @db.VarChar(20) date DateTime @db.Date symbol String? @db.VarChar(20) name String? @db.VarChar(100) market String? @db.VarChar(20) shares Decimal? @db.Decimal(18, 4) price Decimal? @db.Decimal(18, 4) amount Decimal @db.Decimal(18, 2) fee Decimal @default(0) @db.Decimal(18, 2) currency String @default("CNY") @db.VarChar(10) exchangeRate Decimal @default(1) @db.Decimal(10, 6) metadata Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) @@index([userId, date, createdAt]) @@index([accountId]) @@index([positionId]) @@map("transactions") } ``` ### 4.3 Prisma Service #### prisma/prisma.service.ts ```typescript import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { async onModuleInit() { await this.$connect(); } async onModuleDestroy() { await this.$disconnect(); } } ``` --- ## 五、环境配置 ### 5.1 .env 文件 ```env # 数据库配置 DB_HOST=localhost DB_PORT=5432 DB_USERNAME=postgres DB_PASSWORD=your_password DB_NAME=vestmind DB_SSL=false # 或者使用连接字符串(Prisma) DATABASE_URL="postgresql://postgres:password@localhost:5432/vestmind?schema=public" # 应用配置 NODE_ENV=development PORT=3000 ``` ### 5.2 package.json 脚本 ```json { "scripts": { "start": "nest start", "start:dev": "nest start --watch", "start:prod": "node dist/main", "typeorm": "typeorm-ts-node-commonjs", "migration:generate": "typeorm-ts-node-commonjs migration:generate", "migration:run": "typeorm-ts-node-commonjs migration:run", "migration:revert": "typeorm-ts-node-commonjs migration:revert", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio" } } ``` --- ## 六、数据库迁移 ### 6.1 TypeORM 迁移 #### 创建迁移 ```bash npm run typeorm migration:generate -- -n CreateUsersTable ``` #### 迁移文件示例 ```typescript import { MigrationInterface, QueryRunner } from 'typeorm'; export class CreateUsersTable1234567890 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DROP TABLE users;`); } } ``` ### 6.2 Prisma 迁移 ```bash # 创建迁移 npx prisma migrate dev --name init # 应用迁移 npx prisma migrate deploy # 查看迁移状态 npx prisma migrate status ``` --- ## 七、性能优化建议 ### 7.1 连接池配置 ```typescript // TypeORM { type: 'postgres', // ... 其他配置 extra: { max: 20, // 最大连接数 min: 5, // 最小连接数 idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }, } ``` ### 7.2 查询优化 ```typescript // 使用 select 只查询需要的字段 const users = await userRepository.find({ select: ['id', 'username', 'email'], }); // 使用 relations 预加载关联数据 const transaction = await transactionRepository.findOne({ where: { id }, relations: ['account', 'position'], }); // 使用 QueryBuilder 进行复杂查询 const result = await transactionRepository .createQueryBuilder('transaction') .leftJoinAndSelect('transaction.account', 'account') .where('transaction.userId = :userId', { userId }) .andWhere('transaction.date BETWEEN :start AND :end', { start, end }) .orderBy('transaction.date', 'DESC') .getMany(); ``` --- ## 八、总结 ### 8.1 推荐方案 **对于您的项目,推荐使用 TypeORM + PostgreSQL:** 1. ✅ NestJS 官方推荐,文档完善 2. ✅ 装饰器语法,与 NestJS 风格一致 3. ✅ 支持复杂的实体关系 4. ✅ 支持事务管理 5. ✅ 迁移工具完善 ### 8.2 快速开始 ```bash # 1. 创建 NestJS 项目 npm i -g @nestjs/cli nest new vest-mind-backend # 2. 安装依赖 cd vest-mind-backend npm install @nestjs/typeorm typeorm pg npm install --save-dev @types/pg # 3. 配置数据库连接 # 参考上面的 database.module.ts # 4. 创建实体 nest g module users nest g service users nest g controller users # 5. 运行项目 npm run start:dev ``` ### 8.3 参考资料 - [NestJS 官方文档 - TypeORM](https://docs.nestjs.com/techniques/database) - [TypeORM 官方文档](https://typeorm.io/) - [PostgreSQL 官方文档](https://www.postgresql.org/docs/) --- **文档版本**:v1.0 **创建日期**:2024年