Files
invest-mind-store/packages/design-document/机生文档/NestJS与PostgreSQL集成方案.md
2026-02-11 16:01:42 +08:00

866 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<string, any>;
@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<Transaction>,
) {}
async create(createTransactionDto: CreateTransactionDto): Promise<Transaction> {
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<Transaction> {
return await this.transactionRepository.findOne({
where: { id, userId },
relations: ['account', 'position', 'tradingPlan'],
});
}
async findByDateRange(
userId: number,
startDate: Date,
endDate: Date,
): Promise<Transaction[]> {
return await this.transactionRepository.find({
where: {
userId,
date: Between(startDate, endDate),
},
order: { date: 'DESC' },
});
}
async update(id: number, userId: number, updateData: Partial<Transaction>) {
await this.transactionRepository.update({ id, userId }, updateData);
return this.findOne(id, userId);
}
async remove(id: number, userId: number): Promise<void> {
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<Transaction>,
) {}
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<void> {
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<void> {
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年