跳到主要内容

Prisma+Nexus详解

Prisma+Nexus概述

Prisma和Nexus是两个强大的开源工具,它们的结合为现代Web应用开发提供了完整的数据库访问和API构建解决方案。Prisma作为现代化的ORM工具,提供了类型安全的数据库访问;而Nexus作为TypeScript-first的GraphQL schema构建工具,则提供了优雅的API定义方式。两者结合,能够显著提高开发效率,确保类型安全,并简化数据库和API的管理。

Prisma由Prisma Labs开发,于2018年首次发布;Nexus由GraphQL Yoga团队开发,是GraphQL生态系统中的重要组成部分。

Prisma的核心特性

1. 类型安全的数据库访问

Prisma Client是一个自动生成的类型安全查询构建器,提供了智能的代码补全和类型检查功能。

2. 直观的数据模型定义

使用Prisma Schema Language(PSL)定义数据模型,支持关系、枚举、默认值等特性。

3. 强大的迁移工具

Prisma Migrate提供了声明式的数据库迁移管理,支持创建、应用和回滚迁移。

4. 数据库可视化

Prisma Studio是一个交互式GUI工具,用于浏览和管理数据库数据。

5. 多数据库支持

支持PostgreSQL、MySQL、SQLite、MongoDB等多种数据库。

Nexus的核心特性

1. TypeScript优先

完全基于TypeScript构建,提供了强大的类型推断和类型安全。

2. 代码优先的API定义

通过代码定义GraphQL schema,支持自动生成类型定义。

3. 模块化设计

支持将schema分割成多个模块,便于大型项目管理。

4. 热重载支持

开发过程中支持schema热重载,提高开发效率。

5. 与Prisma集成

提供了与Prisma的无缝集成,简化数据库访问和API构建。

安装和配置Prisma

1. 安装Prisma CLI

npm install prisma --save-dev

2. 初始化Prisma项目

npx prisma init

此命令会创建以下文件:

  • prisma/schema.prisma:Prisma schema文件
  • .env:环境变量文件(包含数据库连接URL)

3. 配置数据库连接

.env文件中设置数据库连接URL:

DATABASE_URL="postgresql://username:password@localhost:5432/mydb"

4. 定义数据模型

prisma/schema.prisma文件中定义数据模型:

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

generator client {
provider = "prisma-client-js"
}

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

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

5. 创建数据库迁移

npx prisma migrate dev --name init

此命令会:

  • 创建迁移文件
  • 应用迁移(创建数据库表)
  • 生成Prisma Client

6. 安装Prisma Client

npm install @prisma/client

安装和配置Nexus

1. 安装Nexus和相关依赖

npm install nexus graphql

2. 创建GraphQL服务器

创建src/server.ts文件:

import { ApolloServer } from 'apollo-server';
import { makeSchema } from 'nexus';
import { join } from 'path';
import * as types from './graphql';

const schema = makeSchema({
types,
outputs: {
schema: join(__dirname, '../schema.graphql'),
typegen: join(__dirname, '../node_modules/@types/nexus-typegen/index.d.ts'),
},
contextType: {
module: join(__dirname, './context.ts'),
export: 'Context',
},
});

const server = new ApolloServer({
schema,
context: () => ({}),
});

server.listen().then(({ url }) => {
console.log(`GraphQL服务器运行在 ${url}`);
});

3. 创建Context

创建src/context.ts文件:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export interface Context {
prisma: PrismaClient;
}

export function createContext(): Context {
return {
prisma,
};
}

4. 修改服务器以使用Context

更新src/server.ts文件:

import { ApolloServer } from 'apollo-server';
import { makeSchema } from 'nexus';
import { join } from 'path';
import * as types from './graphql';
import { createContext } from './context';

const schema = makeSchema({
types,
outputs: {
schema: join(__dirname, '../schema.graphql'),
typegen: join(__dirname, '../node_modules/@types/nexus-typegen/index.d.ts'),
},
contextType: {
module: join(__dirname, './context.ts'),
export: 'Context',
},
});

const server = new ApolloServer({
schema,
context: createContext,
});

server.listen().then(({ url }) => {
console.log(`GraphQL服务器运行在 ${url}`);
});

定义GraphQL类型和Resolver

1. 创建基础类型

创建src/graphql/User.ts文件:

import { objectType } from 'nexus';

export const User = objectType({
name: 'User',
definition(t) {
t.int('id');
t.string('name', { nullable: true });
t.string('email');
t.list.field('posts', { type: 'Post' });
t.datetime('createdAt');
t.datetime('updatedAt');
},
});

创建src/graphql/Post.ts文件:

import { objectType } from 'nexus';

export const Post = objectType({
name: 'Post',
definition(t) {
t.int('id');
t.string('title');
t.string('content', { nullable: true });
t.boolean('published');
t.int('authorId');
t.field('author', { type: 'User' });
t.datetime('createdAt');
t.datetime('updatedAt');
},
});

2. 创建查询类型

创建src/graphql/Query.ts文件:

import { queryType, intArg, stringArg } from 'nexus';

export const Query = queryType({
definition(t) {
// 获取所有用户
t.list.field('users', {
type: 'User',
resolve(_parent, _args, ctx) {
return ctx.prisma.user.findMany();
},
});

// 根据ID获取用户
t.field('user', {
type: 'User',
args: {
id: intArg({ required: true }),
},
resolve(_parent, { id }, ctx) {
return ctx.prisma.user.findUnique({ where: { id } });
},
});

// 获取所有文章
t.list.field('posts', {
type: 'Post',
resolve(_parent, _args, ctx) {
return ctx.prisma.post.findMany();
},
});

// 根据ID获取文章
t.field('post', {
type: 'Post',
args: {
id: intArg({ required: true }),
},
resolve(_parent, { id }, ctx) {
return ctx.prisma.post.findUnique({ where: { id } });
},
});
},
});

3. 创建变更类型

创建src/graphql/Mutation.ts文件:

import { mutationType, stringArg, intArg, booleanArg } from 'nexus';

export const Mutation = mutationType({
definition(t) {
// 创建用户
t.field('createUser', {
type: 'User',
args: {
name: stringArg(),
email: stringArg({ required: true }),
},
resolve(_parent, { name, email }, ctx) {
return ctx.prisma.user.create({
data: { name, email },
});
},
});

// 更新用户
t.field('updateUser', {
type: 'User',
args: {
id: intArg({ required: true }),
name: stringArg(),
email: stringArg(),
},
resolve(_parent, { id, name, email }, ctx) {
return ctx.prisma.user.update({
where: { id },
data: { name, email },
});
},
});

// 删除用户
t.field('deleteUser', {
type: 'User',
args: {
id: intArg({ required: true }),
},
resolve(_parent, { id }, ctx) {
return ctx.prisma.user.delete({ where: { id } });
},
});

// 创建文章
t.field('createPost', {
type: 'Post',
args: {
title: stringArg({ required: true }),
content: stringArg(),
published: booleanArg({ default: false }),
authorId: intArg({ required: true }),
},
resolve(_parent, { title, content, published, authorId }, ctx) {
return ctx.prisma.post.create({
data: { title, content, published, authorId },
});
},
});

// 更新文章
t.field('updatePost', {
type: 'Post',
args: {
id: intArg({ required: true }),
title: stringArg(),
content: stringArg(),
published: booleanArg(),
},
resolve(_parent, { id, title, content, published }, ctx) {
return ctx.prisma.post.update({
where: { id },
data: { title, content, published },
});
},
});

// 删除文章
t.field('deletePost', {
type: 'Post',
args: {
id: intArg({ required: true }),
},
resolve(_parent, { id }, ctx) {
return ctx.prisma.post.delete({ where: { id } });
},
});
},
});

4. 导出所有类型

创建src/graphql/index.ts文件:

export * from './User';
export * from './Post';
export * from './Query';
export * from './Mutation';

高级查询和关系处理

1. 嵌套查询

使用Prisma可以轻松实现嵌套查询:

t.field('userWithPosts', {
type: 'User',
args: {
id: intArg({ required: true }),
},
resolve(_parent, { id }, ctx) {
return ctx.prisma.user.findUnique({
where: { id },
include: { posts: true }, // 包含相关的文章
});
},
});

2. 过滤和排序

Prisma提供了强大的过滤和排序功能:

t.list.field('publishedPosts', {
type: 'Post',
args: {
orderBy: stringArg({ default: 'createdAt' }),
orderDirection: stringArg({ default: 'desc' }),
},
resolve(_parent, { orderBy, orderDirection }, ctx) {
return ctx.prisma.post.findMany({
where: { published: true },
orderBy: { [orderBy]: orderDirection },
include: { author: true },
});
},
});

3. 聚合查询

t.field('postsAggregate', {
type: 'PostAggregate',
resolve(_parent, _args, ctx) {
return {
count: ctx.prisma.post.count(),
publishedCount: ctx.prisma.post.count({ where: { published: true } }),
draftCount: ctx.prisma.post.count({ where: { published: false } }),
};
},
});

// 需要定义PostAggregate类型
export const PostAggregate = objectType({
name: 'PostAggregate',
definition(t) {
t.int('count');
t.int('publishedCount');
t.int('draftCount');
},
});

事务处理

Prisma支持事务处理,确保数据库操作的原子性:

t.field('createUserWithPost', {
type: 'User',
args: {
name: stringArg(),
email: stringArg({ required: true }),
postTitle: stringArg({ required: true }),
postContent: stringArg(),
},
async resolve(_parent, { name, email, postTitle, postContent }, ctx) {
// 使用事务创建用户和相关文章
return ctx.prisma.$transaction(async (prisma) => {
// 创建用户
const user = await prisma.user.create({
data: { name, email },
});

// 创建文章
await prisma.post.create({
data: {
title: postTitle,
content: postContent,
published: false,
authorId: user.id,
},
});

// 返回包含文章的用户
return prisma.user.findUnique({
where: { id: user.id },
include: { posts: true },
});
});
},
});

数据库迁移管理

1. 创建新的迁移

当数据模型发生变化时,创建新的迁移:

npx prisma migrate dev --name add-profile

2. 查看迁移历史

npx prisma migrate history

3. 回滚迁移

npx prisma migrate reset

注意:reset命令会重置数据库,删除所有数据。在生产环境中应使用更精细的回滚策略。

4. 生产环境部署迁移

npx prisma migrate deploy

使用Prisma Studio

Prisma Studio是一个图形化界面工具,用于浏览和管理数据库数据:

npx prisma studio

启动后,可以通过浏览器访问http://localhost:5555使用Prisma Studio。

Nexus高级特性

1. 输入类型

使用输入类型组织复杂的变更参数:

import { inputObjectType } from 'nexus';

export const CreateUserInput = inputObjectType({
name: 'CreateUserInput',
definition(t) {
t.string('name', { nullable: true });
t.string('email', { required: true });
},
});

export const UpdateUserInput = inputObjectType({
name: 'UpdateUserInput',
definition(t) {
t.string('name', { nullable: true });
t.string('email', { nullable: true });
},
});

然后在变更中使用这些输入类型:

t.field('createUser', {
type: 'User',
args: {
data: arg({ type: 'CreateUserInput', required: true }),
},
resolve(_parent, { data }, ctx) {
return ctx.prisma.user.create({ data });
},
});

t.field('updateUser', {
type: 'User',
args: {
id: intArg({ required: true }),
data: arg({ type: 'UpdateUserInput', required: true }),
},
resolve(_parent, { id, data }, ctx) {
return ctx.prisma.user.update({ where: { id }, data });
},
});

2. 接口和联合类型

Nexus支持GraphQL接口和联合类型:

// 定义接口
const Node = interfaceType({
name: 'Node',
definition(t) {
t.id('id');
t.resolveType(() => null); // 实际实现中需要返回具体类型
},
});

// 实现接口
const User = objectType({
name: 'User',
definition(t) {
t.implements('Node');
t.int('id');
// 其他字段...
},
});

// 定义联合类型
const SearchResult = unionType({
name: 'SearchResult',
definition(t) {
t.members('User', 'Post');
},
resolveType(item) {
if ('email' in item) return 'User';
if ('title' in item) return 'Post';
return null;
},
});

3. 自定义标量类型

const DateTime = scalarType({
name: 'DateTime',
asNexusMethod: 'datetime',
description: '日期时间类型',
serialize(value) {
return new Date(value).toISOString();
},
parseValue(value) {
return new Date(value);
},
parseLiteral(ast) {
if (ast.kind === 'StringValue') {
return new Date(ast.value);
}
return null;
},
});

认证和授权

1. 添加认证中间件

import { ApolloServer } from 'apollo-server';
import { verify } from 'jsonwebtoken';
import { createContext } from './context';

// 认证中间件
const contextWithAuth = ({ req }) => {
const context = createContext();

// 从请求头获取token
const token = req.headers.authorization?.replace('Bearer ', '');

if (token) {
try {
const decoded = verify(token, 'your-secret-key');
context.user = decoded;
} catch (error) {
console.error('无效的token', error);
}
}

return context;
};

const server = new ApolloServer({
schema,
context: contextWithAuth,
});

2. 添加授权检查

t.field('updatePost', {
type: 'Post',
args: {
id: intArg({ required: true }),
data: arg({ type: 'UpdatePostInput', required: true }),
},
async resolve(_parent, { id, data }, ctx) {
// 检查用户是否登录
if (!ctx.user) {
throw new Error('未授权访问');
}

// 检查用户是否是文章的作者
const post = await ctx.prisma.post.findUnique({ where: { id } });
if (!post || post.authorId !== ctx.user.id) {
throw new Error('无权限更新此文章');
}

// 更新文章
return ctx.prisma.post.update({ where: { id }, data });
},
});

性能优化

1. 批量加载数据

使用Prisma的findMany和适当的过滤条件批量加载数据,避免N+1查询问题:

t.list.field('usersWithPosts', {
type: 'User',
resolve(_parent, _args, ctx) {
// 一次性加载所有用户和他们的文章
return ctx.prisma.user.findMany({
include: { posts: true },
});
},
});

2. 选择必要的字段

只选择需要的字段,避免过度获取数据:

t.field('userProfile', {
type: 'User',
args: {
id: intArg({ required: true }),
},
resolve(_parent, { id }, ctx) {
return ctx.prisma.user.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
createdAt: true,
// 只选择需要的字段
},
});
},
});

3. 使用事务处理复杂操作

对于需要执行多个相关操作的情况,使用事务确保数据一致性并提高性能:

// 如前所述的事务示例

测试策略

1. 单元测试

测试单个Resolver函数:

import { createContext } from '../context';
import { resolvers } from '../graphql';

// Mock Prisma Client
jest.mock('@prisma/client', () => ({
PrismaClient: jest.fn().mockImplementation(() => ({
user: {
findMany: jest.fn().mockResolvedValue([{ id: 1, name: '张三', email: 'zhangsan@example.com' }]),
},
})),
}));

describe('Query.users', () => {
it('should return a list of users', async () => {
const ctx = createContext();
const result = await resolvers.Query.users(null, null, ctx);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('张三');
});
});

2. 集成测试

测试完整的API流程:

import { ApolloServer } from 'apollo-server-testing';
import { createTestClient } from 'apollo-server-testing';
import { makeSchema } from 'nexus';
import * as types from '../graphql';
import { createContext } from '../context';

// 创建测试服务器
const schema = makeSchema({ types });
const { query, mutate } = createTestClient(
new ApolloServer({
schema,
context: createContext,
})
);

describe('GraphQL API', () => {
it('should create a new user', async () => {
const CREATE_USER = `
mutation {
createUser(data: { email: "test@example.com" }) {
id
email
}
}
`;

const result = await mutate({ mutation: CREATE_USER });
expect(result.data.createUser.email).toBe('test@example.com');
});
});

部署最佳实践

1. 环境变量配置

在不同环境中使用不同的环境变量:

# 开发环境
DATABASE_URL="postgresql://dev:devpassword@localhost:5432/devdb"

# 生产环境
DATABASE_URL="postgresql://prod:prodpassword@production.db:5432/proddb"
JWT_SECRET="strong-production-secret"

2. 生产环境迁移

在部署到生产环境之前,应用数据库迁移:

# package.json
{
"scripts": {
"migrate:deploy": "prisma migrate deploy",
"start": "node dist/server.js"
}
}

3. 生成Prisma Client

在构建过程中生成Prisma Client:

# package.json
{
"scripts": {
"build": "prisma generate && tsc",
"start": "node dist/server.js"
}
}

Prisma+Nexus的优缺点

优点

  1. 类型安全:从数据库到API的端到端类型安全
  2. 开发效率高:自动生成代码和类型定义,减少样板代码
  3. 灵活的查询:强大的查询构建器,支持复杂查询和关系
  4. 良好的工具链:包含迁移、可视化等多种工具
  5. 模块化设计:支持将schema分割成多个模块

缺点

  1. 学习曲线:需要学习Prisma Schema Language和Nexus API
  2. 性能开销:额外的抽象层可能带来轻微的性能开销
  3. 灵活性限制:对于某些特殊的数据库操作,可能需要使用原始SQL
  4. 依赖于TypeScript:主要设计用于TypeScript项目

总结

Prisma和Nexus的组合为现代Web应用开发提供了强大而灵活的解决方案。Prisma提供了类型安全的数据库访问和强大的迁移工具,而Nexus则提供了优雅的GraphQL schema定义方式。两者结合,能够显著提高开发效率,确保类型安全,并简化数据库和API的管理。

无论是构建中小型应用还是大型复杂系统,Prisma+Nexus都能提供高效、可靠的支持。通过自动生成代码、提供智能的类型检查和强大的查询能力,它们使开发者能够专注于业务逻辑的实现,而不必花费大量时间在基础设施和重复代码上。