在之前的文章中,我们介绍了使用 TypeORM 进行数据库操作。本文将介绍另一个强大的 ORM 工具 - Prisma,探讨如何在 NestJS 中集成和使用 Prisma。

Prisma 简介与环境搭建

1. 安装依赖

# 安装 Prisma 相关依赖
npm install prisma @prisma/client
# 初始化 Prisma
npx prisma init

2. 配置数据库连接

# .env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"

3. 项目结构

src/
├── prisma/
│   ├── schema.prisma    # Prisma 模型定义
│   └── migrations/      # 数据库迁移文件
├── modules/
│   └── users/
│       ├── users.service.ts
│       ├── users.controller.ts
│       └── dto/
└── prisma.service.ts    # Prisma 服务

模型设计

1. 定义数据模型

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  tags      Tag[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  user   User    @relation(fields: [userId], references: [id])
  userId Int     @unique
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

2. 生成并应用迁移

# 生成迁移文件
npx prisma migrate dev --name init

# 应用迁移
npx prisma migrate deploy

Prisma 服务集成

1. 创建 Prisma 服务

// src/prisma.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  constructor() {
    super({
      log: ['query', 'info', 'warn', 'error'],
    });
  }

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }

  async cleanDatabase() {
    if (process.env.NODE_ENV === 'test') {
      const models = Reflect.ownKeys(this).filter((key) => key[0] !== '_');
      
      return Promise.all(
        models.map((modelKey) => this[modelKey].deleteMany()),
      );
    }
  }
}

2. 注册 Prisma 模块

// src/prisma/prisma.module.ts
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

CRUD 操作实现

1. 用户服务实现

// src/modules/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';
import { CreateUserDto, UpdateUserDto } from './dto';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async create(data: CreateUserDto) {
    return this.prisma.user.create({
      data: {
        ...data,
        profile: {
          create: data.profile,
        },
      },
      include: {
        profile: true,
      },
    });
  }

  async findAll(params: {
    skip?: number;
    take?: number;
    where?: any;
    orderBy?: any;
  }) {
    const { skip, take, where, orderBy } = params;
    return this.prisma.user.findMany({
      skip,
      take,
      where,
      orderBy,
      include: {
        profile: true,
        posts: true,
      },
    });
  }

  async findOne(id: number) {
    return this.prisma.user.findUnique({
      where: { id },
      include: {
        profile: true,
        posts: {
          include: {
            tags: true,
          },
        },
      },
    });
  }

  async update(id: number, data: UpdateUserDto) {
    return this.prisma.user.update({
      where: { id },
      data: {
        ...data,
        profile: {
          update: data.profile,
        },
      },
      include: {
        profile: true,
      },
    });
  }

  async remove(id: number) {
    return this.prisma.user.delete({
      where: { id },
    });
  }
}

2. 控制器实现

// src/modules/users/users.controller.ts
import { Controller, Get, Post, Body, Param, Delete, Put, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto } from './dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Get()
  findAll(
    @Query('skip') skip?: string,
    @Query('take') take?: string,
    @Query('orderBy') orderBy?: string,
  ) {
    return this.usersService.findAll({
      skip: skip ? Number(skip) : undefined,
      take: take ? Number(take) : undefined,
      orderBy: orderBy ? JSON.parse(orderBy) : undefined,
    });
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(Number(id));
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(Number(id), updateUserDto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(Number(id));
  }
}

高级查询技巧

1. 关系查询

// src/modules/posts/posts.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';

@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {}

  async findPostsWithRelations() {
    // 嵌套关系查询
    return this.prisma.post.findMany({
      include: {
        author: {
          include: {
            profile: true,
          },
        },
        tags: true,
      },
      where: {
        published: true,
        author: {
          profile: {
            bio: {
              contains: 'developer',
            },
          },
        },
      },
    });
  }

  async findPostsByTags(tags: string[]) {
    // 多对多关系查询
    return this.prisma.post.findMany({
      where: {
        tags: {
          some: {
            name: {
              in: tags,
            },
          },
        },
      },
      include: {
        tags: true,
        author: true,
      },
    });
  }
}

2. 事务处理

// src/modules/posts/posts.service.ts
async createPostWithTags(data: CreatePostDto) {
  return this.prisma.$transaction(async (tx) => {
    // 创建文章
    const post = await tx.post.create({
      data: {
        title: data.title,
        content: data.content,
        authorId: data.authorId,
      },
    });

    // 处理标签
    const tags = await Promise.all(
      data.tags.map((tagName) =>
        tx.tag.upsert({
          where: { name: tagName },
          create: { name: tagName },
          update: {},
        }),
      ),
    );

    // 关联文章和标签
    await tx.post.update({
      where: { id: post.id },
      data: {
        tags: {
          connect: tags.map((tag) => ({ id: tag.id })),
        },
      },
    });

    return post;
  });
}

3. 聚合查询

// src/modules/analytics/analytics.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';

@Injectable()
export class AnalyticsService {
  constructor(private prisma: PrismaService) {}

  async getPostStats() {
    // 聚合统计
    const stats = await this.prisma.post.aggregate({
      _count: {
        _all: true,
        published: true,
      },
      _avg: {
        authorId: true,
      },
      where: {
        published: true,
      },
      orderBy: {
        createdAt: 'desc',
      },
    });

    // 分组统计
    const postsByAuthor = await this.prisma.post.groupBy({
      by: ['authorId'],
      _count: {
        _all: true,
      },
      having: {
        _count: {
          _all: {
            gt: 5,
          },
        },
      },
    });

    return {
      stats,
      postsByAuthor,
    };
  }
}

性能优化

1. 查询优化

// src/modules/posts/posts.service.ts
async findPosts(params: {
  skip?: number;
  take?: number;
  where?: any;
}) {
  const { skip, take, where } = params;
  
  // 使用 select 只获取需要的字段
  return this.prisma.post.findMany({
    skip,
    take,
    where,
    select: {
      id: true,
      title: true,
      author: {
        select: {
          id: true,
          name: true,
        },
      },
    },
  });
}

// 批量操作
async publishPosts(postIds: number[]) {
  return this.prisma.post.updateMany({
    where: {
      id: {
        in: postIds,
      },
    },
    data: {
      published: true,
    },
  });
}

2. 中间件与扩展

// src/prisma/middleware/logging.middleware.ts
import { PrismaClient } from '@prisma/client';

export function addLoggingMiddleware(prisma: PrismaClient) {
  prisma.$use(async (params, next) => {
    const before = Date.now();
    const result = await next(params);
    const after = Date.now();
    
    console.log(`Query ${params.model}.${params.action} took ${after - before}ms`);
    return result;
  });
}

// 扩展 Prisma Client
import { Prisma } from '@prisma/client';

const xprisma = prisma.$extends({
  result: {
    user: {
      fullName: {
        needs: { firstName: true, lastName: true },
        compute(user) {
          return `${user.firstName} ${user.lastName}`;
        },
      },
    },
  },
});

测试

1. 单元测试

// src/modules/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { PrismaService } from '../../prisma.service';
import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
import { PrismaClient } from '@prisma/client';

describe('UsersService', () => {
  let service: UsersService;
  let prisma: DeepMockProxy<PrismaClient>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: PrismaService,
          useFactory: () => mockDeep<PrismaClient>(),
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    prisma = module.get(PrismaService);
  });

  it('should create a user', async () => {
    const user = {
      id: 1,
      email: 'test@example.com',
      name: 'Test User',
    };

    prisma.user.create.mockResolvedValue(user);

    expect(await service.create({ email: 'test@example.com', name: 'Test User' }))
      .toEqual(user);
  });
});

2. E2E 测试

// test/users.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/prisma.service';

describe('UsersController (e2e)', () => {
  let app: INestApplication;
  let prisma: PrismaService;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    prisma = app.get<PrismaService>(PrismaService);
    await app.init();
  });

  beforeEach(async () => {
    await prisma.cleanDatabase();
  });

  afterAll(async () => {
    await app.close();
  });

  it('/users (POST)', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({
        email: 'test@example.com',
        name: 'Test User',
      })
      .expect(201)
      .expect((res) => {
        expect(res.body).toHaveProperty('id');
        expect(res.body.email).toBe('test@example.com');
      });
  });
});

写在最后

本文详细介绍了在 NestJS 中使用 Prisma 的各个方面:

  1. 环境搭建与配置
  2. 数据模型设计
  3. CRUD 操作实现
  4. 高级查询技巧
  5. 性能优化
  6. 测试策略

Prisma 作为一个现代化的 ORM 工具,提供了类型安全、直观的 API 和优秀的开发体验。通过本文的学习,你应该能够在 NestJS 项目中熟练使用 Prisma 进行数据库操作。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍