feat: 添加设计文件packages
This commit is contained in:
865
packages/design-document/机生文档/NestJS与PostgreSQL集成方案.md
Normal file
865
packages/design-document/机生文档/NestJS与PostgreSQL集成方案.md
Normal file
@@ -0,0 +1,865 @@
|
||||
# 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年
|
||||
|
||||
Reference in New Issue
Block a user