立聪堂德州十三局店 发表于 4 天前

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]
查看完整版本: MonkeyCode构建GraphQL服务:从Schema计划到性能优化的完备实战