3.6.2 GraphQL Schema 设计 #
GraphQL Schema 是 API 的核心,它定义了数据的结构、类型和操作。良好的 Schema 设计是构建高质量 GraphQL API 的基础。本节将详细介绍如何设计和实现 GraphQL Schema,包括类型定义、关系建模、最佳实践等内容。
Schema 定义语言(SDL) #
GraphQL Schema 使用 Schema 定义语言(SDL)来描述 API 的结构。SDL 是一种简洁、易读的语言,用于定义类型、字段和操作。
基本语法 #
# 标量类型
scalar Date
scalar Email
scalar URL
# 枚举类型
enum UserRole {
ADMIN
MODERATOR
USER
GUEST
}
# 对象类型
type User {
id: ID!
name: String!
email: Email!
role: UserRole!
createdAt: Date!
updatedAt: Date!
}
# 输入类型
input CreateUserInput {
name: String!
email: Email!
password: String!
role: UserRole = USER
}
# 根类型
type Query {
user(id: ID!): User
users(filter: UserFilter): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
type Subscription {
userCreated: User!
}
类型系统详解 #
1. 标量类型 #
标量类型是 GraphQL 类型系统的叶子节点,代表具体的数据值。
# 内置标量类型
scalar ID # 唯一标识符,序列化为字符串
scalar String # UTF-8 字符串
scalar Int # 32位有符号整数
scalar Float # 双精度浮点数
scalar Boolean # true 或 false
# 自定义标量类型
scalar Date # 日期时间
scalar Email # 邮箱地址
scalar URL # URL 地址
scalar JSON # JSON 对象
scalar Upload # 文件上传
在 Go 中实现自定义标量类型:
package main
import (
"fmt"
"regexp"
"time"
"github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/language/ast"
)
// Date 标量类型
var DateType = graphql.NewScalar(graphql.ScalarConfig{
Name: "Date",
Description: "Date custom scalar type",
Serialize: func(value interface{}) interface{} {
switch v := value.(type) {
case time.Time:
return v.Format(time.RFC3339)
case *time.Time:
return v.Format(time.RFC3339)
default:
return nil
}
},
ParseValue: func(value interface{}) interface{} {
switch v := value.(type) {
case string:
t, err := time.Parse(time.RFC3339, v)
if err != nil {
return nil
}
return t
default:
return nil
}
},
ParseLiteral: func(valueAST ast.Value) interface{} {
switch valueAST := valueAST.(type) {
case *ast.StringValue:
t, err := time.Parse(time.RFC3339, valueAST.Value)
if err != nil {
return nil
}
return t
default:
return nil
}
},
})
// Email 标量类型
var EmailType = graphql.NewScalar(graphql.ScalarConfig{
Name: "Email",
Description: "Email custom scalar type",
Serialize: func(value interface{}) interface{} {
return value
},
ParseValue: func(value interface{}) interface{} {
if email, ok := value.(string); ok {
if isValidEmail(email) {
return email
}
}
return nil
},
ParseLiteral: func(valueAST ast.Value) interface{} {
if valueAST, ok := valueAST.(*ast.StringValue); ok {
if isValidEmail(valueAST.Value) {
return valueAST.Value
}
}
return nil
},
})
func isValidEmail(email string) bool {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return emailRegex.MatchString(email)
}
2. 对象类型 #
对象类型是 GraphQL Schema 的核心,定义了数据的结构和字段。
type User {
# 基本字段
id: ID!
name: String!
email: Email!
avatar: URL
# 枚举字段
role: UserRole!
status: UserStatus!
# 时间字段
createdAt: Date!
updatedAt: Date!
lastLoginAt: Date
# 关联字段
profile: UserProfile
posts: [Post!]!
comments: [Comment!]!
# 带参数的字段
posts(
first: Int = 10
after: String
filter: PostFilter
orderBy: PostOrderBy
): PostConnection!
# 计算字段
fullName: String!
postCount: Int!
isOnline: Boolean!
}
type UserProfile {
bio: String
website: URL
location: String
birthday: Date
interests: [String!]!
socialLinks: [SocialLink!]!
}
type SocialLink {
platform: SocialPlatform!
url: URL!
username: String!
}
enum SocialPlatform {
TWITTER
FACEBOOK
INSTAGRAM
LINKEDIN
GITHUB
}
在 Go 中定义对象类型:
// 用户类型定义
var UserType = graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Description: "User object",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
Description: "User ID",
},
"name": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
Description: "User name",
},
"email": &graphql.Field{
Type: graphql.NewNonNull(EmailType),
Description: "User email",
},
"role": &graphql.Field{
Type: graphql.NewNonNull(UserRoleEnum),
Description: "User role",
},
"createdAt": &graphql.Field{
Type: graphql.NewNonNull(DateType),
Description: "User creation date",
},
"profile": &graphql.Field{
Type: UserProfileType,
Description: "User profile",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
user := p.Source.(*User)
return getUserProfile(user.ID)
},
},
"posts": &graphql.Field{
Type: graphql.NewList(graphql.NewNonNull(PostType)),
Description: "User posts",
Args: graphql.FieldConfigArgument{
"first": &graphql.ArgumentConfig{
Type: graphql.Int,
DefaultValue: 10,
Description: "Number of posts to fetch",
},
"after": &graphql.ArgumentConfig{
Type: graphql.String,
Description: "Cursor for pagination",
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
user := p.Source.(*User)
first := p.Args["first"].(int)
after, _ := p.Args["after"].(string)
return getUserPosts(user.ID, first, after)
},
},
"postCount": &graphql.Field{
Type: graphql.NewNonNull(graphql.Int),
Description: "Number of posts by user",
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
user := p.Source.(*User)
return getUserPostCount(user.ID)
},
},
},
})
3. 枚举类型 #
枚举类型定义了一组预定义的值。
enum UserRole {
ADMIN
MODERATOR
USER
GUEST
}
enum UserStatus {
ACTIVE
INACTIVE
SUSPENDED
PENDING
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
DELETED
}
enum OrderDirection {
ASC
DESC
}
在 Go 中定义枚举类型:
var UserRoleEnum = graphql.NewEnum(graphql.EnumConfig{
Name: "UserRole",
Description: "User role enumeration",
Values: graphql.EnumValueConfigMap{
"ADMIN": &graphql.EnumValueConfig{
Value: "admin",
Description: "Administrator role",
},
"MODERATOR": &graphql.EnumValueConfig{
Value: "moderator",
Description: "Moderator role",
},
"USER": &graphql.EnumValueConfig{
Value: "user",
Description: "Regular user role",
},
"GUEST": &graphql.EnumValueConfig{
Value: "guest",
Description: "Guest role",
},
},
})
var PostStatusEnum = graphql.NewEnum(graphql.EnumConfig{
Name: "PostStatus",
Description: "Post status enumeration",
Values: graphql.EnumValueConfigMap{
"DRAFT": &graphql.EnumValueConfig{
Value: "draft",
Description: "Draft post",
},
"PUBLISHED": &graphql.EnumValueConfig{
Value: "published",
Description: "Published post",
},
"ARCHIVED": &graphql.EnumValueConfig{
Value: "archived",
Description: "Archived post",
},
},
})
4. 接口类型 #
接口定义了一组字段,可以被多个对象类型实现。
interface Node {
id: ID!
createdAt: Date!
updatedAt: Date!
}
interface Timestamped {
createdAt: Date!
updatedAt: Date!
}
interface Authored {
author: User!
createdAt: Date!
}
type User implements Node & Timestamped {
id: ID!
createdAt: Date!
updatedAt: Date!
name: String!
email: Email!
}
type Post implements Node & Timestamped & Authored {
id: ID!
createdAt: Date!
updatedAt: Date!
author: User!
title: String!
content: String!
}
type Comment implements Node & Timestamped & Authored {
id: ID!
createdAt: Date!
updatedAt: Date!
author: User!
content: String!
post: Post!
}
在 Go 中定义接口类型:
var NodeInterface = graphql.NewInterface(graphql.InterfaceConfig{
Name: "Node",
Description: "An object with an ID",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
Description: "The ID of the object",
},
"createdAt": &graphql.Field{
Type: graphql.NewNonNull(DateType),
Description: "Creation timestamp",
},
"updatedAt": &graphql.Field{
Type: graphql.NewNonNull(DateType),
Description: "Last update timestamp",
},
},
ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
switch p.Value.(type) {
case *User:
return UserType
case *Post:
return PostType
case *Comment:
return CommentType
default:
return nil
}
},
})
// 确保类型实现接口
var UserType = graphql.NewObject(graphql.ObjectConfig{
Name: "User",
Description: "User object",
Interfaces: []*graphql.Interface{NodeInterface},
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.NewNonNull(graphql.ID),
},
"createdAt": &graphql.Field{
Type: graphql.NewNonNull(DateType),
},
"updatedAt": &graphql.Field{
Type: graphql.NewNonNull(DateType),
},
"name": &graphql.Field{
Type: graphql.NewNonNull(graphql.String),
},
// ... 其他字段
},
})
5. 联合类型 #
联合类型表示一个值可能是几种类型中的一种。
union SearchResult = User | Post | Comment
union MediaContent = Image | Video | Document
type Image {
id: ID!
url: URL!
width: Int!
height: Int!
alt: String
}
type Video {
id: ID!
url: URL!
duration: Int!
thumbnail: URL
}
type Document {
id: ID!
url: URL!
filename: String!
size: Int!
mimeType: String!
}
type Query {
search(query: String!): [SearchResult!]!
media(id: ID!): MediaContent
}
在 Go 中定义联合类型:
var SearchResultUnion = graphql.NewUnion(graphql.UnionConfig{
Name: "SearchResult",
Description: "Search result union",
Types: []*graphql.Object{UserType, PostType, CommentType},
ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
switch p.Value.(type) {
case *User:
return UserType
case *Post:
return PostType
case *Comment:
return CommentType
default:
return nil
}
},
})
6. 输入类型 #
输入类型用于变更操作的参数。
input CreateUserInput {
name: String!
email: Email!
password: String!
role: UserRole = USER
profile: CreateUserProfileInput
}
input CreateUserProfileInput {
bio: String
website: URL
location: String
interests: [String!]
}
input UpdateUserInput {
name: String
email: Email
role: UserRole
profile: UpdateUserProfileInput
}
input UpdateUserProfileInput {
bio: String
website: URL
location: String
interests: [String!]
}
input UserFilter {
role: UserRole
status: UserStatus
createdAfter: Date
createdBefore: Date
search: String
}
input PostFilter {
status: PostStatus
authorId: ID
tags: [String!]
createdAfter: Date
createdBefore: Date
}
input OrderBy {
field: String!
direction: OrderDirection!
}
在 Go 中定义输入类型:
var CreateUserInputType = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "CreateUserInput",
Description: "Input for creating a user",
Fields: graphql.InputObjectConfigFieldMap{
"name": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
Description: "User name",
},
"email": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(EmailType),
Description: "User email",
},
"password": &graphql.InputObjectFieldConfig{
Type: graphql.NewNonNull(graphql.String),
Description: "User password",
},
"role": &graphql.InputObjectFieldConfig{
Type: UserRoleEnum,
DefaultValue: "user",
Description: "User role",
},
},
})
var UserFilterInputType = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "UserFilter",
Description: "Filter for user queries",
Fields: graphql.InputObjectConfigFieldMap{
"role": &graphql.InputObjectFieldConfig{
Type: UserRoleEnum,
Description: "Filter by user role",
},
"status": &graphql.InputObjectFieldConfig{
Type: UserStatusEnum,
Description: "Filter by user status",
},
"search": &graphql.InputObjectFieldConfig{
Type: graphql.String,
Description: "Search in name and email",
},
"createdAfter": &graphql.InputObjectFieldConfig{
Type: DateType,
Description: "Filter users created after this date",
},
"createdBefore": &graphql.InputObjectFieldConfig{
Type: DateType,
Description: "Filter users created before this date",
},
},
})
关系建模 #
1. 一对一关系 #
type User {
id: ID!
name: String!
profile: UserProfile # 一对一关系
}
type UserProfile {
id: ID!
user: User! # 反向关系
bio: String
avatar: URL
}
2. 一对多关系 #
type User {
id: ID!
name: String!
posts: [Post!]! # 一对多关系
}
type Post {
id: ID!
title: String!
author: User! # 多对一关系
}
3. 多对多关系 #
type User {
id: ID!
name: String!
followedTags: [Tag!]! # 多对多关系
}
type Tag {
id: ID!
name: String!
followers: [User!]! # 多对多关系
posts: [Post!]!
}
type Post {
id: ID!
title: String!
tags: [Tag!]! # 多对多关系
}
# 使用连接表建模多对多关系
type UserTagConnection {
user: User!
tag: Tag!
followedAt: Date!
}
分页设计 #
1. 基于游标的分页(推荐) #
type Query {
users(first: Int, after: String, last: Int, before: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
2. 基于偏移的分页 #
type Query {
users(offset: Int = 0, limit: Int = 10): UserList!
}
type UserList {
users: [User!]!
totalCount: Int!
hasMore: Boolean!
}
在 Go 中实现分页:
// 基于游标的分页实现
func resolveUsers(p graphql.ResolveParams) (interface{}, error) {
first, _ := p.Args["first"].(int)
after, _ := p.Args["after"].(string)
if first == 0 {
first = 10 // 默认值
}
// 解析游标
var afterID int
if after != "" {
decoded, err := base64.StdEncoding.DecodeString(after)
if err != nil {
return nil, err
}
afterID, _ = strconv.Atoi(string(decoded))
}
// 查询数据
users, err := getUsersAfter(afterID, first+1) // 多查询一个用于判断是否有下一页
if err != nil {
return nil, err
}
hasNextPage := len(users) > first
if hasNextPage {
users = users[:first] // 移除多查询的那一个
}
// 构建边和游标
edges := make([]UserEdge, len(users))
for i, user := range users {
cursor := base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(user.ID)))
edges[i] = UserEdge{
Node: user,
Cursor: cursor,
}
}
var startCursor, endCursor string
if len(edges) > 0 {
startCursor = edges[0].Cursor
endCursor = edges[len(edges)-1].Cursor
}
return UserConnection{
Edges: edges,
PageInfo: PageInfo{
HasNextPage: hasNextPage,
HasPreviousPage: after != "",
StartCursor: startCursor,
EndCursor: endCursor,
},
TotalCount: getTotalUserCount(),
}, nil
}
错误处理 #
1. 字段级错误 #
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
type CreateUserPayload {
user: User
errors: [UserError!]!
}
type UserError {
field: String
message: String!
code: String!
}
2. 全局错误处理 #
func handleGraphQLError(err error) *gqlerrors.Error {
switch e := err.(type) {
case *ValidationError:
return gqlerrors.NewError(
e.Message,
nil,
"",
nil,
[]int{},
map[string]interface{}{
"code": "VALIDATION_ERROR",
"field": e.Field,
},
)
case *AuthenticationError:
return gqlerrors.NewError(
"Authentication required",
nil,
"",
nil,
[]int{},
map[string]interface{}{
"code": "AUTHENTICATION_ERROR",
},
)
default:
return gqlerrors.NewError(
"Internal server error",
nil,
"",
nil,
[]int{},
map[string]interface{}{
"code": "INTERNAL_ERROR",
},
)
}
}
Schema 最佳实践 #
1. 命名约定 #
# 使用 PascalCase 命名类型
type UserProfile { }
type PostComment { }
# 使用 camelCase 命名字段
type User {
firstName: String!
lastName: String!
createdAt: Date!
}
# 使用 SCREAMING_SNAKE_CASE 命名枚举值
enum UserRole {
SUPER_ADMIN
CONTENT_MODERATOR
REGULAR_USER
}
2. 字段设计原则 #
# 好的设计:语义清晰
type User {
id: ID!
name: String!
email: String!
isActive: Boolean!
createdAt: Date!
}
# 避免:过于技术化的字段名
type User {
pk: ID! # 应该用 id
usr_nm: String! # 应该用 name
email_addr: String! # 应该用 email
is_del: Boolean! # 应该用 isActive
}
3. 版本控制策略 #
type User {
id: ID!
name: String!
email: String!
# 废弃字段
username: String! @deprecated(reason: "Use name instead")
# 新增字段
displayName: String!
# 可选字段用于向后兼容
profile: UserProfile
}
4. 性能考虑 #
# 使用连接模式避免 N+1 问题
type User {
posts(first: Int, after: String): PostConnection!
}
# 提供计数字段避免昂贵的查询
type User {
postCount: Int!
followerCount: Int!
}
# 使用数据加载器批量获取数据
type Post {
author: User! # 使用 DataLoader 批量加载
comments: [Comment!]! # 使用 DataLoader 批量加载
}
通过遵循这些设计原则和最佳实践,你可以创建出结构清晰、性能良好、易于维护的 GraphQL Schema。在下一节中,我们将学习如何在 Go 语言中实现这些 Schema 定义。