GraphQL 指南
一旦 ERD 就位,GraphQL 就成为一个复用层。驱动 AutoLoad 的同一个关系图也可以生成完整的 GraphQL schema 并执行查询。
概述
flowchart LR
ERD["ERD<br/>(实体 + 关系)"] --> AutoLoad
ERD --> GraphQL["GraphQL Schema"]
GraphQL --> Handler["GraphQLHandler"]
Handler --> Client["客户端查询"]
设置
1. 定义带有查询的实体
使用 @query 装饰器添加根入口点。GraphQL 操作名自动生成为 entityPrefix + MethodCamel(例如 SprintEntity.get_all → sprintEntityGetAll):
from typing import Annotated, Optional
from pydantic import BaseModel
from pydantic_resolve import (
Relationship,
base_entity,
build_list,
build_object,
query,
)
USERS = {
7: {"id": 7, "name": "Ada"},
8: {"id": 8, "name": "Bob"},
}
TASKS = [
{"id": 10, "title": "Design docs", "sprint_id": 1, "owner_id": 7},
{"id": 11, "title": "Refine examples", "sprint_id": 1, "owner_id": 8},
{"id": 12, "title": "Write tests", "sprint_id": 2, "owner_id": 7},
]
SPRINTS = [
{"id": 1, "name": "Sprint 24"},
{"id": 2, "name": "Sprint 25"},
]
async def user_loader(user_ids: list[int]):
users = [USERS.get(uid) for uid in user_ids]
return build_object(users, user_ids, lambda u: u.id)
async def task_loader(sprint_ids: list[int]):
tasks = [t for t in TASKS if t["sprint_id"] in sprint_ids]
return build_list(tasks, sprint_ids, lambda t: t["sprint_id"])
BaseEntity = base_entity()
class UserEntity(BaseModel, BaseEntity):
id: int
name: str
class TaskEntity(BaseModel, BaseEntity):
__relationships__ = [
Relationship(fk='owner_id', target=UserEntity, name='owner', loader=user_loader)
]
id: int
title: str
owner_id: int
class SprintEntity(BaseModel, BaseEntity):
__relationships__ = [
Relationship(fk='id', target=list[TaskEntity], name='tasks', loader=task_loader)
]
id: int
name: str
@query
async def get_all(cls, limit: int = 20) -> list['SprintEntity']:
return [SprintEntity(**s) for s in SPRINTS[:limit]]
diagram = BaseEntity.get_diagram()
2. 执行查询
from pydantic_resolve.graphql import GraphQLHandler
handler = GraphQLHandler(diagram)
result = await handler.execute("""
{
sprintEntityGetAll {
id
name
tasks {
id
title
owner {
id
name
}
}
}
}
""")
print(result)
# {'data': {'sprintEntityGetAll': [
# {'id': 1, 'name': 'Sprint 24', 'tasks': [
# {'id': 10, 'title': 'Design docs', 'owner': {'id': 7, 'name': 'Ada'}},
# {'id': 11, 'title': 'Refine examples', 'owner': {'id': 8, 'name': 'Bob'}},
# ]},
# {'id': 2, 'name': 'Sprint 25', 'tasks': [
# {'id': 12, 'title': 'Write tests', 'owner': {'id': 7, 'name': 'Ada'}},
# ]},
# ]}, 'errors': None}
添加变更
使用 @mutation 装饰器进行写操作:
from pydantic_resolve import mutation
class SprintEntity(BaseModel, BaseEntity):
id: int
name: str
@mutation
async def create(cls, name: str) -> 'SprintEntity':
sprint = await db.create_sprint(name=name)
return SprintEntity.model_validate(sprint)
执行:
result = await handler.execute("""
mutation {
sprintEntityCreate(name: "Sprint 26") {
id
name
}
}
""")
使用 QueryConfig / MutationConfig 的外部配置
@query 和 @mutation 装饰器将根字段直接绑定到实体类内部。如果你希望将查询/变更逻辑与实体定义分离——或者查询函数位于不同模块——可以使用 QueryConfig 和 MutationConfig。
QueryConfig
from pydantic_resolve import QueryConfig
QueryConfig(
method: Callable, # 异步函数,第一个参数是 `cls`
name: str | None = None, # 覆盖操作名中的方法名部分
description: str | None = None, # schema 中的字段描述
)
最终的 GraphQL 操作名始终为 entityPrefix + MethodCamel(例如 SprintEntity + get_all → sprintEntityGetAll)。name 参数仅覆盖方法名部分:name='sprints' → sprintEntitySprints。
method 的第一个参数接收 cls(类似 classmethod),后面是任意 GraphQL 参数:
async def get_all_sprints(cls, limit: int = 20) -> list[SprintEntity]:
return [SprintEntity(**s) for s in SPRINTS[:limit]]
async def get_sprint_by_id(cls, id: int) -> SprintEntity | None:
return SprintEntity(**SPRINTS.get(id, {}))
MutationConfig
from pydantic_resolve import MutationConfig
MutationConfig(
method: Callable, # 异步函数,第一个参数是 `cls`
name: str | None = None, # 覆盖操作名中的方法名部分
description: str | None = None, # schema 中的字段描述
)
async def create_sprint(cls, name: str) -> SprintEntity:
sprint = await db.create_sprint(name=name)
return SprintEntity.model_validate(sprint)
接入 ErDiagram
将 QueryConfig 和 MutationConfig 附加到 ErDiagram 中的 Entity 上:
from pydantic_resolve import Entity, ErDiagram
diagram = ErDiagram(entities=[
Entity(
kls=SprintEntity,
relationships=[...],
queries=[
QueryConfig(method=get_all_sprints), # → sprintEntityGetAllSprints
QueryConfig(method=get_sprint_by_id, name='sprint'), # → sprintEntitySprint
],
mutations=[
MutationConfig(method=create_sprint), # → sprintEntityCreateSprint
],
),
])
装饰器 vs Config:何时用哪个
| 方面 | @query / @mutation |
QueryConfig / MutationConfig |
|---|---|---|
| 定义位置 | 实体类内部 | 外部,可以在任意模块 |
| 耦合度 | 紧密(查询与实体放在一起) | 松散(查询与实体分离) |
| 每个实体的多个查询 | 每个方法一个 | 配置列表 |
| 适用场景 | 简单项目,偏好放在一起 | 共享实体、多模块项目 |
GraphQLHandler
from pydantic_resolve.graphql import GraphQLHandler
handler = GraphQLHandler(
er_diagram=diagram,
enable_from_attribute_in_type_adapter=False, # 可选
)
# 直接执行查询字符串
result = await handler.execute(query_string)
| 参数 | 类型 | 描述 |
|---|---|---|
er_diagram |
ErDiagram |
用于生成 schema 的 ERD |
enable_from_attribute_in_type_adapter |
bool |
启用 Pydantic from_attributes 模式 |
与 FastAPI 集成
与 REST 端点一起提供 GraphQL:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from pydantic_resolve.graphql import GraphQLHandler
app = FastAPI()
handler = GraphQLHandler(diagram)
@app.get("/graphql", response_class=HTMLResponse)
async def graphiql_playground():
return handler.get_graphiql_html()
@app.post("/graphql")
async def graphql_endpoint(request: Request):
body = await request.json()
query = body.get("query", "")
result = await handler.execute(query)
return result
GET /graphql 提供带有 Schema 浏览器和查询历史的交互式 GraphiQL IDE。POST /graphql 处理查询执行。
默认端点为 /graphql。如需使用其他路径,传入 get_graphiql_html:
请求上下文
当 GraphQL handler 运行在 Web 框架(FastAPI、Django 等)中时,通常需要将请求级数据(如从 JWT 解析的当前用户 ID)传入查询方法。handler.execute() 接受一个 context dict 用于此目的。
从 FastAPI 传入上下文
@app.post("/graphql")
async def graphql_endpoint(request: Request):
body = await request.json()
query = body.get("query", "")
# 从 JWT 中提取用户信息(简化示例)
user_id = decode_jwt(request.headers.get("Authorization", ""))
context = {"user_id": user_id}
result = await handler.execute(query, context=context)
return result
在查询方法中接收上下文
在任意 @query 或 @mutation 方法(或 QueryConfig/MutationConfig 函数)中添加 context 参数即可接收。该参数对 GraphQL schema 不可见 — 客户端无法看到它。
async def get_my_tasks(limit: int = 10, context: dict = None) -> list[TaskEntity]:
"""获取当前用户的任务。"""
user_id = context['user_id']
return await fetch_tasks_by_owner(user_id, limit)
Entity(
kls=TaskEntity,
queries=[
QueryConfig(method=get_my_tasks, name='my_tasks'),
],
)
客户端像普通字段一样查询,查询中不包含 context 参数:
DataLoader 中的上下文
同一个 context dict 也会传入内部的 Resolver(context=...),由 Resolver 自动注入到声明了 _context 属性的类式 DataLoader 中。这样 loader 可以按请求级数据(如 user_id)过滤或限定查询范围:
from aiodataloader import DataLoader
class TaskByOwnerLoader(DataLoader):
_context: dict # 由 Resolver 注入
async def batch_load_fn(self, sprint_ids):
user_id = self._context['user_id']
tasks = await db.query(Task).filter(
Task.sprint_id.in_(sprint_ids),
Task.owner_id == user_id,
).all()
return build_list(tasks, sprint_ids, lambda t: t.sprint_id)
函数式 loader(普通异步函数)无法接收 context。需要访问请求级数据时,请使用类式 DataLoader。
工作原理
GraphQLHandler从 ERD 实体和关系生成 GraphQL schema。- 每个
Relationship成为带有自动解析的 GraphQL 字段。 - 根查询来自
@query装饰器或QueryConfig。 - 处理程序内部使用相同的
Resolver和 DataLoader 批处理。
flowchart TB
Q["客户端查询<br/>{ sprintEntityGetAll { tasks { owner } } }"]
P["查询解析器"]
R["Resolver + DataLoader"]
D["DataLoader 批量调用"]
Q --> P
P --> R
R --> D
D --> R
R --> Q
下一步
继续阅读 MCP 服务 了解如何向 AI 代理暴露 GraphQL API。