MonkeyCode构建GraphQL服务:从Schema计划到性能优化的完备实战
REST API的N+1题目、版当地狱、太过获取/获取不敷,GraphQL一次性办理。用MonkeyCode,从0到生产级GraphQL服务。REST的痛点:为什么换GraphQL?
题目1:太过获取(Over-fetching)
// REST: 获取用户信息,返回了你不想要的一堆字段
GET /api/users/123
{
"id": 123,
"name": "张三",
"email": "zhangsan@example.com",
"password_hash": "...", // 敏感信息,前端根本不需要
"created_at": "2024-01-01",
"updated_at": "2024-06-01",
"last_login_ip": "180.200.134.171",
"preferences": {...}, // 几十个字段,前端只需要name
...
}题目2:获取不敷(Under-fetching)与N+1
// 获取一篇博客文章及其作者和评论
const post = await fetch(`/api/posts/${postId}`);// 1次请求
const author = await fetch(`/api/users/${post.authorId}`);// 2次请求
const comments = await fetch(`/api/posts/${postId}/comments`);// 3次请求
const commentAuthors = await Promise.all(// N+1次请求!
comments.map(c => fetch(`/api/users/${c.authorId}`))
);前端为了展示一个页面,发了N+1次哀求。
题目3:版当地狱
/api/v1/users
/api/v2/users
/api/v3/users← 永远删不掉的v1GraphQL一键办理
# 前端精确获取需要的字段,一次请求搞定
query GetPostDetail($postId: ID!) {
post(id: $postId) {
title
content
author {
name # 只取name,不取email/password
avatar
}
comments {
body
author { name }
}
}
}后端一个端点/graphql,前端本身决定要什么字段。
MonkeyCode天生GraphQL服务
第一步:天生项目骨架
在MonkeyCode中:
用Python Strawberry框架生成GraphQL服务骨架,包含:
- 用户、文章、评论三个实体
- SQLite数据库(开发用)
- GraphiQL调试界面
- 单元测试配置MonkeyCode天生:
# app/main.py
import strawberry
from strawberry.asgi import GraphQL
from typing import List, Optional
from dataclasses import dataclass
import sqlite3
import json
# ─── 数据模型 ───
@dataclass
class UserModel:
id: int
name: str
email: str
avatar: Optional
@dataclass
class PostModel:
id: int
title: str
content: str
author_id: int
@dataclass
class CommentModel:
id: int
post_id: int
author_id: int
body: str
# ─── GraphQL类型 ───
@strawberry.type
class User:
id: int
name: str
email: str
avatar: Optional
@strawberry.field
async def posts(self) -> List["Post"]:
# N+1问题解决:DataLoader批量加载
return await load_posts_by_author_id.load(self.id)
@strawberry.type
class Post:
id: int
title: str
content: str
author_id: int
@strawberry.field
async def author(self) -> User:
return await load_user_by_id.load(self.author_id)
@strawberry.field
async def comments(self) -> List["Comment"]:
return await load_comments_by_post_id.load(self.id)
@strawberry.type
class Comment:
id: int
body: str
author_id: int
@strawberry.field
async def author(self) -> User:
return await load_user_by_id.load(self.author_id)
# ─── DataLoader(解决N+1)───
from strawberry.dataloader import DataLoader
async def batch_load_users(ids: List) -> List:
conn = get_db()
placeholders = ",".join("?" * len(ids))
rows = conn.execute(
f"SELECT id, name, email, avatar FROM users WHERE id IN ({placeholders})",
ids
).fetchall()
# 保持顺序与ids一致
user_map = {row: User(*row) for row in rows}
return
async def batch_load_posts(author_ids: List) -> List]:
conn = get_db()
placeholders = ",".join("?" * len(author_ids))
rows = conn.execute(
f"SELECT id, title, content, author_id FROM posts WHERE author_id IN ({placeholders})",
author_ids
).fetchall()
post_map = {}
for row in rows:
post = Post(*row)
post_map.setdefault(row, []).append(post)
return ) for aid in author_ids]
async def batch_load_comments(post_ids: List) -> List]:
conn = get_db()
placeholders = ",".join("?" * len(post_ids))
rows = conn.execute(
f"SELECT id, body, author_id, post_id FROM comments WHERE post_id IN ({placeholders})",
post_ids
).fetchall()
comment_map = {}
for row in rows:
comment = Comment(*row)
comment_map.setdefault(row, []).append(comment)
return ) for pid in post_ids]
load_user_by_id = DataLoader(batch_load_users)
load_posts_by_author_id = DataLoader(batch_load_posts)
load_comments_by_post_id = DataLoader(batch_load_comments)
# ─── Query ───
@strawberry.type
class Query:
@strawberry.query
async def user(self, id: int) -> Optional:
return await load_user_by_id.load(id)
@strawberry.query
async def post(self, id: int) -> Optional:
conn = get_db()
row = conn.execute(
"SELECT id, title, content, author_id FROM posts WHERE id = ?", (id,)
).fetchone()
return Post(*row) if row else None
@strawberry.query
async def posts(self, limit: int = 10, offset: int = 0) -> List:
conn = get_db()
rows = conn.execute(
"SELECT id, title, content, author_id FROM posts LIMIT ? OFFSET ?",
(limit, offset)
).fetchall()
return
@strawberry.query
async def search_posts(self, keyword: str) -> List:
conn = get_db()
rows = conn.execute(
"SELECT id, title, content, author_id FROM posts WHERE title LIKE ? OR content LIKE ?",
(f"%{keyword}%", f"%{keyword}%")
).fetchall()
return
# ─── Mutation ───
@strawberry.input
class CreatePostInput:
title: str
content: str
@strawberry.type
class CreatePostPayload:
post: Optional
success: bool
message: str
@strawberry.type
class Mutation:
@strawberry.mutation
async def create_post(self, input: CreatePostInput, author_id: int) -> CreatePostPayload:
conn = get_db()
try:
cursor = conn.execute(
"INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)",
(input.title, input.content, author_id)
)
conn.commit()
post = Post(cursor.lastrowid, input.title, input.content, author_id)
return CreatePostPayload(post=post, success=True, message="创建成功")
except Exception as e:
return CreatePostPayload(post=None, success=False, message=str(e))
@strawberry.mutation
async def create_comment(self, post_id: int, author_id: int, body: str) -> bool:
conn = get_db()
conn.execute(
"INSERT INTO comments (post_id, author_id, body) VALUES (?, ?, ?)",
(post_id, author_id, body)
)
conn.commit()
return True
# ─── Schema ───
schema = strawberry.Schema(query=Query, mutation=Mutation)
app = GraphQL(schema)
# ─── DB工具 ───
def get_db():
conn = sqlite3.connect("blog.db")
conn.row_factory = sqlite3.Row
return conn
def init_db():
conn = get_db()
conn.executescript("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
avatar TEXT
);
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
author_id INTEGER NOT NULL,
FOREIGN KEY (author_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY,
post_id INTEGER NOT NULL,
author_id INTEGER NOT NULL,
body TEXT NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts(id),
FOREIGN KEY (author_id) REFERENCES users(id)
);
""")
conn.commit()
if __name__ == "__main__":
import uvicorn
init_db()
uvicorn.run(app, host="0.0.0.0", port=8000)运行与调试
pip install strawberry-graphql uvicorn
python main.py欣赏器打开 http://localhost:8000,进入GraphiQL界面。
测试查询
# 查询文章及关联数据(一次请求)
query {
post(id: 1) {
title
author {
name
avatar
}
comments {
body
author { name }
}
}
}
# 创建文章(Mutation)
mutation {
createPost(
input: { title: "GraphQL真香", content: "详细内容..." },
authorId: 1
) {
post { id title }
success
message
}
}性能优化:办理N+1题目
上面的DataLoader已包办理了N+1,原理是批量加载 + 缓存:
没有DataLoader:
Post.author→ SELECT * FROM users WHERE id=1
Post.author→ SELECT * FROM users WHERE id=2
Post.author→ SELECT * FROM users WHERE id=3
...(N次查询)
有DataLoader:
1. 收集所有需要的ID:
2. 批量查询:SELECT * FROM users WHERE id IN (1, 2, 3, ...)
3. 按原顺序返回结果认证与权限
# 在MonkeyCode中让AI添加JWT认证中间件
from fastapi import Request, HTTPException
from strawberry.fastapi.router import GraphQLRouter
async def get_current_user(request: Request) -> Optional:
auth_header = request.headers.get("Authorization")
if not auth_header:
return None
token = auth_header.replace("Bearer ", "")
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
return await load_user_by_id.load(payload["user_id"])
except jwt.InvalidTokenError:
return None
@strawberry.type
class Query:
@strawberry.query
async def me(self, info: strawberry.Info) -> Optional:
user = info.context["request"].state.user
return user
@strawberry.query
async def my_posts(self, info: strawberry.Info) -> List:
user = info.context["request"].state.user
if not user:
raise HTTPException(401, "请先登录")
return await load_posts_by_author_id.load(user.id)生产级设置
MonkeyCode天生生产设置:
# app/settings.py
import os
class Settings:
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///blog.db")
JWT_SECRET = os.getenv("JWT_SECRET", "dev-secret-change-in-prod")
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",")
ENABLE_GRAPHIQL = os.getenv("ENABLE_GRAPHIQL", "true").lower() == "true"
# 性能相关
DATALOADER_CACHE_SIZE = 1000
QUERY_DEPTH_LIMIT = 7 # 防止深度嵌套攻击
QUERY_COMPLEXITY_LIMIT = 1000# 防止复杂查询攻击
settings = Settings()查询复杂度分析(防DoS)
from strawberry.extensions import Extension
class QueryComplexityExtension(Extension):
def on_operation(self, *, execution_context, **kwargs):
# 简单估算:每个字段算1点复杂度
# 生产环境用strawberry.extensions.QueryComplexity更好
pass
# 使用示例
schema = strawberry.Schema(
query=Query,
mutation=Mutation,
extensions=
)MonkeyCode Prompt模板
用Strawberry框架写一个[博客/电商/社交]GraphQL服务,包含:
1. 实体定义:[列出实体及关系]
2. 查询接口:支持按ID查询、列表查询(分页)、搜索
3. 变更接口:创建、更新、删除
4. DataLoader解决N+1问题
5. JWT认证中间件
6. 查询复杂度限制(防DoS)
7. 生成Dockerfile和docker-compose.yml
8. 编写单元测试(pytest)踩坑实录
坑表现办理方案N+1查询一个GraphQL哀求触发几百条SQLDataLoader批量加载深度嵌套攻击恶意查询{ post { author { posts { author { posts ... } } } } }限定查询深度(发起≤7层)复杂查询攻击大量fields叠加导致CPU耗尽限定查询复杂度(算分数)内省走漏生产环境袒露schema给外人生产环境关闭GraphiQL,禁用introspectionSQL注入拼接GraphQL参数到SQL永世用参数化查询总结
GraphQL的焦点代价是让前端准确控制数据获取,告别太过获取和N+1题目。MonkeyCode能帮你:
[*]从0天生GraphQL服务骨架(Strawberry/Graphene)
[*]自动实现DataLoader办理N+1
[*]设置认证、权限、复杂度限定
[*]天生生产级Docker摆设设置
记着三件事:DataLoader必须有,查询深度要限定,生产环境关GraphiQL。
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.
页:
[1]