跳到主要内容

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都能提供高效、可靠的支持。通过自动生成代码、提供智能的类型检查和强大的查询能力,它们使开发者能够专注于业务逻辑的实现,而不必花费大量时间在基础设施和重复代码上。