乐闻世界logo
搜索文章和话题

What are the advanced concepts and architecture design patterns in GraphQL

2月21日 17:00

GraphQL Advanced Concepts and Architecture Design

GraphQL is not just a query language; it also contains many advanced concepts and architecture design patterns that are crucial for building large-scale GraphQL applications.

1. Union Types

Defining Union Types

graphql
union SearchResult = User | Post | Comment type Query { search(query: String!): [SearchResult!]! }

Implementing Union Type Resolvers

javascript
const resolvers = { SearchResult: { __resolveType: (obj) => { if (obj.email) { return 'User'; } if (obj.title) { return 'Post'; } if (obj.text) { return 'Comment'; } return null; } }, Query: { search: async (_, { query }) => { const users = await User.search(query); const posts = await Post.search(query); const comments = await Comment.search(query); return [...users, ...posts, ...comments]; } } };

Using Union Types

graphql
query SearchResults($query: String!) { search(query: $query) { ... on User { id name email } ... on Post { id title author { name } } ... on Comment { id text author { name } } } }

2. Interface Types

Defining Interfaces

graphql
interface Node { id: ID! createdAt: DateTime! } type User implements Node { id: ID! createdAt: DateTime! name: String! email: String! } type Post implements Node { id: ID! createdAt: DateTime! title: String! content: String! } type Query { node(id: ID!): Node }

Implementing Interface Resolvers

javascript
const resolvers = { Node: { __resolveType: (obj) => { if (obj.email) { return 'User'; } if (obj.title) { return 'Post'; } return null; } }, Query: { node: async (_, { id }) => { // Try to get from different data sources const user = await User.findById(id); if (user) return user; const post = await Post.findById(id); if (post) return post; throw new Error('Node not found'); } } };

3. Custom Directives

Creating Custom Directives

graphql
directive @auth(requires: Role) on FIELD_DEFINITION directive @cache(ttl: Int) on FIELD_DEFINITION directive @transform(type: String) on FIELD_DEFINITION enum Role { USER ADMIN }

Implementing Directives

javascript
const { makeExecutableSchema } = require('@graphql-tools/schema'); const { defaultFieldResolver } = require('graphql'); const authDirective = (schema) => { return makeExecutableSchema({ typeDefs: schema, resolvers: { Query: { // ... existing resolvers } }, directiveResolvers: { auth: (next, source, args, context) => { const { requires } = args; if (!context.user || context.user.role !== requires) { throw new Error('Unauthorized'); } return next(); }, cache: async (next, source, args, context) => { const { ttl } = args; const cacheKey = `cache:${JSON.stringify(source)}`; // Try to get from cache const cached = await redis.get(cacheKey); if (cached) { return JSON.parse(cached); } // Execute resolver const result = await next(); // Cache result await redis.setex(cacheKey, ttl, JSON.stringify(result)); return result; } } }); };

4. Subscriptions

Defining Subscriptions

graphql
type Subscription { postCreated: Post! postUpdated(postId: ID!): Post! commentAdded(postId: ID!): Comment! }

Implementing Subscriptions

javascript
const { PubSub } = require('graphql-subscriptions'); const RedisPubSub = require('graphql-redis-subscriptions').RedisPubSub; const pubsub = new RedisPubSub({ connection: { host: 'localhost', port: 6379 } }); const POST_CREATED = 'POST_CREATED'; const POST_UPDATED = 'POST_UPDATED'; const COMMENT_ADDED = 'COMMENT_ADDED'; const resolvers = { Subscription: { postCreated: { subscribe: () => pubsub.asyncIterator([POST_CREATED]) }, postUpdated: { subscribe: (_, { postId }) => { const filteredIterator = pubsub.asyncIterator([POST_UPDATED]); return { [Symbol.asyncIterator]() { return (async function* () { for await (const event of filteredIterator) { if (event.postUpdated.id === postId) { yield event; } } })(); } }; } }, commentAdded: { subscribe: (_, { postId }) => { const filteredIterator = pubsub.asyncIterator([COMMENT_ADDED]); return { [Symbol.asyncIterator]() { return (async function* () { for await (const event of filteredIterator) { if (event.commentAdded.postId === postId) { yield event; } } })(); } }; } } }, Mutation: { createPost: async (_, { input }) => { const post = await Post.create(input); pubsub.publish(POST_CREATED, { postCreated: post }); return post; }, updatePost: async (_, { id, input }) => { const post = await Post.update(id, input); pubsub.publish(POST_UPDATED, { postUpdated: post }); return post; }, addComment: async (_, { input }) => { const comment = await Comment.create(input); pubsub.publish(COMMENT_ADDED, { commentAdded: comment }); return comment; } } };

5. DataLoader Pattern

Batch Loading

javascript
const DataLoader = require('dataloader'); class DataLoaderContext { constructor() { this.userLoader = new DataLoader(this.batchGetUsers); this.postLoader = new DataLoader(this.batchGetPosts); } async batchGetUsers(userIds) { const users = await User.findAll({ where: { id: userIds } }); return userIds.map(id => users.find(user => user.id === id)); } async batchGetPosts(postIds) { const posts = await Post.findAll({ where: { id: postIds } }); return postIds.map(id => posts.find(post => post.id === id)); } }

Using in Resolvers

javascript
const resolvers = { Post: { author: (post, _, context) => { return context.dataLoaders.userLoader.load(post.authorId); }, comments: (post, _, context) => { return context.dataLoaders.commentLoader.load(post.id); } }, Query: { posts: async (_, __, context) => { const posts = await Post.findAll(); return posts; } } };

6. Schema Splitting and Composition

Schema Splitting

graphql
# user.graphql type User { id: ID! name: String! email: String! } extend type Query { user(id: ID!): User users: [User!]! } extend type Mutation { createUser(input: CreateUserInput!): User updateUser(id: ID!, input: UpdateUserInput!): User deleteUser(id: ID!): Boolean }
graphql
# post.graphql type Post { id: ID! title: String! content: String! author: User! } extend type Query { post(id: ID!): Post posts: [Post!]! } extend type Mutation { createPost(input: CreatePostInput!): Post updatePost(id: ID!, input: UpdatePostInput!): Post deletePost(id: ID!): Boolean }

Schema Composition

javascript
const { mergeTypeDefs } = require('@graphql-tools/merge'); const { loadFilesSync } = require('@graphql-tools/load-files'); const typeDefsArray = loadFilesSync(path.join(__dirname, './schemas')); const typeDefs = mergeTypeDefs(typeDefsArray);

7. GraphQL in Microservices Architecture

Gateway Pattern

javascript
const { ApolloServer } = require('apollo-server'); const { ApolloGateway } = require('@apollo/gateway'); const { readFileSync } = require('fs'); const gateway = new ApolloGateway({ supergraphSdl: readFileSync('./supergraph.graphql').toString(), serviceList: [ { name: 'users', url: 'http://localhost:4001/graphql' }, { name: 'posts', url: 'http://localhost:4002/graphql' }, { name: 'comments', url: 'http://localhost:4003/graphql' } ] }); const server = new ApolloServer({ gateway, subscriptions: false }); server.listen().then(({ url }) => { console.log(`Gateway ready at ${url}`); });

Federation Pattern

graphql
# users service extend type Post @key(fields: "id") { id: ID! @external author: User } type User @key(fields: "id") { id: ID! name: String! email: String! }
graphql
# posts service type Post @key(fields: "id") { id: ID! title: String! content: String! authorId: ID! author: User } extend type User @key(fields: "id") { id: ID! @external posts: [Post!]! }

8. Error Handling and Retry

Custom Error Classes

javascript
class GraphQLError extends Error { constructor(message, code, extensions = {}) { super(message); this.name = 'GraphQLError'; this.code = code; this.extensions = extensions; } } class ValidationError extends GraphQLError { constructor(message, field) { super(message, 'VALIDATION_ERROR', { field }); } } class AuthenticationError extends GraphQLError { constructor(message = 'Authentication required') { super(message, 'AUTHENTICATION_ERROR'); } } class AuthorizationError extends GraphQLError { constructor(message = 'Not authorized') { super(message, 'AUTHORIZATION_ERROR'); } }

Error Handling Middleware

javascript
const formatError = (error) => { if (error instanceof GraphQLError) { return { message: error.message, code: error.code, extensions: error.extensions }; } // Don't expose detailed errors in production if (process.env.NODE_ENV === 'production') { return { message: 'Internal server error', code: 'INTERNAL_SERVER_ERROR' }; } return error; };

9. Testing Strategies

Resolver Unit Tests

javascript
const { resolvers } = require('./resolvers'); describe('User resolvers', () => { describe('Query.user', () => { it('should return user by id', async () => { const mockUser = { id: '1', name: 'John', email: 'john@example.com' }; User.findById = jest.fn().mockResolvedValue(mockUser); const result = await resolvers.Query.user(null, { id: '1' }); expect(result).toEqual(mockUser); expect(User.findById).toHaveBeenCalledWith('1'); }); it('should throw error if user not found', async () => { User.findById = jest.fn().mockResolvedValue(null); await expect( resolvers.Query.user(null, { id: '1' }) ).rejects.toThrow('User not found'); }); }); });

Integration Tests

javascript
const { ApolloServer } = require('apollo-server'); const { createTestClient } = require('apollo-server-testing'); const { typeDefs, resolvers } = require('./schema'); describe('GraphQL API', () => { const server = new ApolloServer({ typeDefs, resolvers }); const { query, mutate } = createTestClient(server); describe('Query.users', () => { it('should return all users', async () => { const GET_USERS = ` query GetUsers { users { id name email } } `; const { data, errors } = await query(GET_USERS); expect(errors).toBeUndefined(); expect(data.users).toBeDefined(); expect(Array.isArray(data.users)).toBe(true); }); }); describe('Mutation.createUser', () => { it('should create a new user', async () => { const CREATE_USER = ` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } } `; const variables = { input: { name: 'John Doe', email: 'john@example.com' } }; const { data, errors } = await mutate(CREATE_USER, { variables }); expect(errors).toBeUndefined(); expect(data.createUser).toBeDefined(); expect(data.createUser.name).toBe('John Doe'); }); }); });

10. Advanced Architecture Patterns

CQRS Pattern

javascript
const resolvers = { Query: { // Read operations - use optimized queries user: async (_, { id }, { readDb }) => { return readDb.User.findById(id); } }, Mutation: { // Write operations - use event sourcing createUser: async (_, { input }, { writeDb, eventBus }) => { const user = await writeDb.User.create(input); await eventBus.publish('USER_CREATED', { user }); return user; } } };

Event Sourcing Pattern

javascript
class EventStore { async saveEvent(aggregateId, eventType, payload) { await Event.create({ aggregateId, eventType, payload: JSON.stringify(payload), timestamp: new Date() }); } async getEvents(aggregateId) { const events = await Event.findAll({ where: { aggregateId }, order: [['timestamp', 'ASC']] }); return events.map(event => ({ ...event, payload: JSON.parse(event.payload) })); } }

11. Advanced Concepts Summary

ConceptPurposeAdvantages
Union TypesHandle multiple return typesFlexible data structures
Interface TypesDefine shared fieldsCode reuse, type safety
Custom DirectivesDeclarative functionalityReusable, composable
SubscriptionsReal-time data pushReal-time, low latency
DataLoaderBatch loadingPerformance optimization, reduce queries
Schema CompositionModular designMaintainability, team collaboration
MicroservicesDistributed architectureScalability, independent deployment
Error HandlingUnified error managementConsistency, debuggability
Testing StrategiesQuality assuranceReliability, maintainability
Architecture PatternsSolve complex problemsScalability, flexibility
标签:GraphQL