Schema与类型系统SDL


Schema与类型系统SDL

Schema是GraphQL的宪法

在GraphQL的世界里,Schema的地位类似于编程语言中的类型系统——它定义了所有合法的数据形状和操作边界。没有Schema,查询就是一段无意义的文本;有了Schema,查询才能被验证、被执行、被文档化。

Schema使用SDL(Schema Definition Language)编写。SDL是一种声明式语言,语法简洁,接近于TypeScript的类型定义。一个Schema文件通常包含三类定义:类型定义、操作入口定义和自定义指令。

以一个电商场景为例:

# 标量定义 scalar DateTime scalar Currency # 枚举定义 enum OrderStatus { PENDING PROCESSING SHIPPED DELIVERED CANCELLED } # 输入类型(用于Mutation参数) input CreateProductInput { name: String! description: String price: Currency! categoryId: ID! } # 接口定义(共享字段) interface Node { id: ID! } interface Timestamped { createdAt: DateTime! updatedAt: DateTime! } # 对象类型 type User implements Node & Timestamped { id: ID! name: String! email: String! avatarUrl: String orders(status: OrderStatus): [Order!]! createdAt: DateTime! updatedAt: DateTime! } type Product implements Node & Timestamped { id: ID! name: String! description: String price: Currency! category: Category! reviews(limit: Int = 10): [Review!]! createdAt: DateTime! updatedAt: DateTime! } type Category implements Node { id: ID! name: String! products(limit: Int = 20): [Product!]! } type Order implements Node & Timestamped { id: ID! status: OrderStatus! items: [OrderItem!]! total: Currency! createdAt: DateTime! updatedAt: DateTime! } type OrderItem { product: Product! quantity: Int! subtotal: Currency! } type Review implements Node & Timestamped { id: ID! rating: Int! body: String! author: User! product: Product! createdAt: DateTime! updatedAt: DateTime! } # 操作入口 type Query { node(id: ID!): Node user(id: ID!): User products(categoryId: ID, limit: Int = 20): [Product!]! search(query: String!, limit: Int = 10): [SearchResult!]! } type Mutation { createProduct(input: CreateProductInput!): Product! createReview(input: CreateReviewInput!): Review! cancelOrder(orderId: ID!): Order! } type Subscription { orderStatusChanged(userId: ID!): Order! newReview(productId: ID!): Review! } # 联合类型 union SearchResult = Product | User | Category type Subscription { orderUpdated(userId: ID!): Order! } type Schema { query: Query mutation: Mutation subscription: Subscription } ​

这个Schema展示了几种核心构造:

  • 标量(Scalar):基础值类型如 String、Int、Boolean、ID,以及自定义标量如 DateTime、Currency
  • 枚举(Enum):有限的值集合如 OrderStatus
  • 对象类型(Object Type):核心实体如 User、Product
  • 接口(Interface):共享字段的契约如 Node、Timestamped
  • 输入类型(Input Type):Mutation参数的复合类型如 CreateProductInput
  • 联合类型(Union Type):多态返回值如 SearchResult
  • 操作类型(Root Type):Query、Mutation、Subscription 作为所有操作的入口

图:Schema类型层次结构

图:Schema类型层次结构

SDL语法速查

SDL的语法规则很直接,掌握这几个模式就够用了:

定义对象类型:type TypeName { fieldName: FieldType! }

实现接口:type User implements Node { id: ID! }

定义枚举:enum Status { ACTIVE INACTIVE }

定义输入类型:input CreateUserInput { name: String! email: String! }

定义联合类型:union Media = Photo | Video | Audio

定义标量:scalar DateTime(实现逻辑在服务端代码中)

字段参数:products(limit: Int = 20, sortBy: String = "NEWEST")

非空与列表:! 表示非空,[Type] 表示列表,[Type!]! 表示非空的列表,且每个元素非空。

Schema入口:type Query { ... } type Mutation { ... } type Subscription { ... }

这些语法元素组合起来,就构成了一个完整的Schema。Schema文件通常按业务领域组织——用户相关的类型放一个文件,商品相关的放另一个——然后在构建时合并成一个完整的Schema。

Schema的组织策略

在实际项目中,Schema的组织方式取决于团队规模和项目复杂度:

单文件Schema:适合小型项目或原型阶段,所有类型定义在一个 .graphql 文件中。

按领域拆分:中型项目的常见做法。每个业务领域一个Schema文件(user.graphql、product.graphql、order.graphql),构建时用工具合并。

联邦子图:大型项目在Apollo Federation下,每个微服务维护自己的子图Schema(Subgraph),通过网关组合成超级图(Supergraph)。

无论哪种组织方式,最终合并后的Schema必须是合法的——同一个类型不能有两个冲突的定义,所有引用的类型必须存在,操作入口类型(Query、Mutation、Subscription)必须唯一。

SDL的设计理念是"声明意图,不关心实现"。Schema只描述数据的形状和关系,不包含任何逻辑代码(比如SQL查询、HTTP调用)。这种分离是GraphQL架构的基石:Schema负责声明数据形状,解析器负责按形状取数。

非空与列表:四象限的语义细节

! 和 [] 的组合产生几种截然不同的契约,很多人在这里踩坑:

写法 含义 典型误区
String 可空单值 允许返回 null
String! 非空单值 返回 null 会触发错误传播
[String] 列表可空、元素可空 元素里可能混入 null
[String!] 列表可空、元素非空 单元素不可为 null
[String]! 列表非空、元素可空 列表本身不可为 null
[String!]! 列表非空、元素非空 最严格,生产环境最常用

易错点一:[String] 里混入 null。如果数据库里某一行邮箱为空,[String] 允许返回 ["a", null, "b"],客户端必须逐元素做 null 判断。多数情况下你应该用 [String!] 甚至 [String!]!,让契约更严格、客户端更省心。

易错点二:字段返回 null 与字段不存在是两回事。email: String 返回 null 表示"该用户没有邮箱",这是合法的业务状态;但如果 Schema 里声明的是 email: String! 而解析器返回了 null,执行引擎会把这个字段标记为错误并向上传播——父字段可能因此整体变成 null,整个响应结构被破坏。设计 Schema 时,非空标记要保守:确定"永远有值"才用 !,否则宁可先放空,等数据质量稳定后再收紧。这也是为什么很多团队给数据库里"理论上非空"的字段仍然标可空——因为历史脏数据可能违反理论。

自定义标量的实现

scalar DateTime 只是声明,真正的序列化与反序列化逻辑要在解析器之外实现。以 Node 生态为例:

import { GraphQLScalarType, Kind } from 'graphql'; const DateTime = new GraphQLScalarType({ name: 'DateTime', description: 'ISO 8601 日期时间字符串', // 客户端传入的值 → 内部使用的值 parseValue(value) { const date = new Date(value); if (Number.isNaN(date.getTime())) { throw new TypeError('无效的日期时间'); } return date; }, // 内部值 → 返回给客户端 serialize(value) { return value instanceof Date ? value.toISOString() : String(value); }, // 查询文本里内联的字面量 parseLiteral(ast) { if (ast.kind !== Kind.STRING) throw new TypeError('DateTime 必须是字符串'); return this.parseValue(ast.value); }, }); ​

自定义标量把"格式校验"从业务代码里抽出来,集中在一处维护。常见的自定义标量还有 Email、URL、JSON、BigInt、UUID。注意边界:标量只负责值的转换,不负责业务规则——比如"日期必须晚于今天"属于业务校验,应该放在输入类型或解析器里,而不是塞进标量,否则同一个标量在不同场景下的语义会被迫一致,反而失去灵活性。

Schema 即文档:introspection

Schema 不只是一份给机器执行的契约,它本身就是可以查询的文档。GraphQL 内置 introspection 能力:客户端可以查询 __schema 元字段,拿到所有类型、字段、参数、枚举值的完整描述。GraphiQL 的自动补全、代码生成工具拉取类型、API 文档站点,全都依赖 introspection。

{ __schema { queryType { name } types { name kind fields { name type { name kind ofType { name } } } } } } ​

生产环境通常要关闭 introspection 或加权限保护——它对外暴露了完整的数据模型,可能被攻击者用来测绘攻击面(详见第五章安全防护)。但在内部环境和开发工具里,introspection 是 GraphQL 体验远超 REST 的重要原因:文档永远不会和实现脱节,因为文档就是从实现里实时生成的。

作者与出处
原作者:灏天文库
来源:平台策划编纂
整理人: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U