介绍
本文是ZOZO Technologies Advent Calendar#2的第16天!
TL; DR
- 解释在REST上使用GraphQL的好处
- 使用ORM操纵Postgres
- 描述在GraphQL中使用的模式,变异和查询
- 使用Graphene使GraphQL在Fast API上可用
- 使用Graphene和pytest测试GraphQL API
为什么在REST上使用GraphQL
REST是用于构建Web API的事实上的标准。为每个CRUD操作设计多个端点(GET,POST,PUT,DELETE)。您可以通过访问这些端点来收集所需的信息。
例如,如果要获取特定的用户信息以及帖子和相关评论,则需要调用四个不同的端点:
每个端点都很简单,但是在获得所需信息之前,您获得的数据超出了您的需要。
在RESTful API中,为了获取没有过多或不足的数据,请请求过度提取(获取包括未使用的数据)和欠获取(由于数据不足而获取下一个端点)是很常见的。
另一方面,GraphQL是用于从API检索数据的查询语言。它没有多个端点,而是围绕单个端点构建的,该端点取决于客户端的需求。
GraphQL配置如下查询,以获取用户信息,帖子和评论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | query { User(userId: 2){ name posts { title comments { body } } comments { body } } } |
这使您可以在一个请求中获取所需的所有数据,而不会超量获取。
制备
创建一个名为
1 2 | $ mkdir fastapi-graphql $ cd fastapi-graphql |
接下来,创建并激活一个新的Python虚拟环境。
1 2 | $ python3.9 -m venv env $ source env/bin/activate |
创建requirements.txt以安装依赖项。
requirements.txt
1 2 | fastapi==0.61.1 uvicorn==0.12.2 |
根据创建的依赖文件安装软件包。
1 | $ pip install -r requirements.txt |
接下来,创建main.py,它描述了一个简单的API流程来检查操作。
main.py
1 2 3 4 5 6 7 8 | from fastapi import FastAPI app = FastAPI() @app.get('/') def ping(): return {'ping': 'pong'} |
启动应用程序
1 | $ uvicorn main:app --reload |
如果运行正常,则应该可以访问以下URL。
http://本地主机:8080 / ping
如果输出这样的结果,则可以。
1 2 3 | { "ping": "pong" } |
另外,在FastAPI中,会自动生成Swagger的API文档。
HTTP:// //本地主机:8000 / docs
Postgres
接下来,下载,安装并启动Postgres。
设定
进行设置以使用PostgreSQL。
*在下文中,以Ubuntu为例。对于其他环境,请根据适当的方法进行设置。
切换到使用Postgres
的帐户
1 | sudo -u postgres -i |
用户和密码设置
1 | createuser -d -U postgres -P db_user |
用户名是
接下来,将要求您输入密码,因此输入
创建数据库
1 | createdb db_name --encoding=UTF-8 --owner=db_user |
创建为
数据库名称
连接测试
执行以下命令后,如果可以确认可以正确输入密码
1 2 3 4 5 6 7 | $ psql -U db_user -h localhost -d db_name Password for user db_user: psql (13.1 (Ubuntu 13.1-1.pgdg20.04+1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type "help" for help. db_name=> |
您可以使用
Python侧面设置
将相关的依赖程序包添加到
requirements.txt
1 2 3 4 | fastapi==0.61.1 uvicorn==0.12.2 orator==0.9.9 # 追加 psycopg2-binary==2.8.6 # 追加 |
安装
1 | $ pip install -r requirements.txt |
创建一个
db.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from orator import DatabaseManager, Schema, Model DATABASES = { "postgres": { "driver": "postgres", "host": "localhost", "database": "db_name", "user": "db_user", "password": "db_password", "prefix": "", "port": 5432 } } db = DatabaseManager(DATABASES) schema = Schema(db) Model.set_connection_resolver(db) |
请根据需要更改以下数据库信息。
-
数据库名称:
db_name -
用户名:
db_user -
密码:
db_password
建立模型
接下来,为用户,帖子和评论创建一个模型。
用户模型
使用
1 | $ orator make:model User -m |
*在此阶段,它尚未应用于数据库。
执行
命令后,如果显示以下消息,则表示成功。
1 2 | Model User successfully created. Created migration: 2020_12_14_150844_create_users_table.py |
另外,将在日历目录中创建
1 2 3 4 5 6 7 8 9 10 11 | . ├── db.py ├── main.py ├── migrations │ ├── 2020_12_14_150844_create_users_table.py │ └── __init__.py ├── models │ ├── __init__.py │ └── user.py ├── requirements.txt └── setting.sh |
向用户模型
添加了属性
将以下各项添加到创建的迁移文件中以添加用户信息。
- 名称
- 街道地址
- 电话号码
- 性别
添加
的位置在
./migrations/2020_12_14_150844_create_users_table.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from orator.migrations import Migration class CreateUsersTable(Migration): def up(self): """ Run the migrations. """ with self.schema.create('users') as table: table.increments('id') table.string('name') # 追加 table.text('address') # 追加 table.string('phone_number', 11) # 追加 table.enum('sex', ['male', 'female']) # 追加 table.timestamps() def down(self): """ Revert the migrations. """ self.schema.drop('users') |
邮编
接下来,创建一个Post模型。
1 | $ orator make:model Post -m |
向邮政模型
添加了属性
将
./migrations/2020_12_14_153522_create_posts_table.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | from orator.migrations import Migration class CreatePostsTable(Migration): def up(self): """ Run the migrations. """ with self.schema.create('posts') as table: table.increments('id') table.integer('user_id').unsigned() # 追加 table.foreign('user_id').references('id').on('users') # 追加 table.string('title') # 追加 table.text('body') # 追加 table.timestamps() def down(self): """ Revert the migrations. """ self.schema.drop('posts') |
在这里,下面的列代表外键。
它引用
1 2 | table.integer('user_id').unsigned() table.foreign('user_id').references('id').on('users') |
评论模型
最后,创建一个注释模型。
1 | $ orator make:model Comments -m |
向模型
添加属性
将属性添加到创建的迁移文件中。
./migrations/2020_12_14_153529_create_comments_table.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from orator.migrations import Migration class CreateCommentsTable(Migration): def up(self): """ Run the migrations. """ with self.schema.create('comments') as table: table.increments('id') table.integer('user_id').unsigned().nullable() # 追加 table.foreign('user_id').references('id').on('users') # 追加 table.integer('post_id').unsigned().nullable() # 追加 table.foreign('post_id').references('id').on('posts') # 追加 table.text('body') # 追加 table.timestamps() def down(self): """ Revert the migrations. """ self.schema.drop('comments') |
执行迁移
使用以下命令执行迁移。
1 | $ orator migrate -c db.py |
这将在数??据库中创建
1 2 3 4 5 6 | Are you sure you want to proceed with the migration? (yes/no) [no] yes Migration table created successfully [OK] Migrated 2020_12_14_150844_create_users_table [OK] Migrated 2020_12_14_153522_create_posts_table [OK] Migrated 2020_12_14_153529_create_comments_table |
关系设置
接下来,编辑在
用户模型
您可以通过应用
下面,我们显示
./models/user.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from orator.orm import has_many from db import Model class User(Model): @has_many def posts(self): from .post import Post return Post @has_many def comments(self): from .comment import Comments return Comments |
邮编
./models/post.py
1 2 3 4 5 6 7 8 9 10 11 12 | from orator.orm import has_many from db import Model class Post(Model): @has_many def comments(self): from .comment import Comments return Comments |
评论模型
./models/comment.py
1 2 3 4 5 6 | from orator import Model class Comments(Model): pass |
GraphQL
要在Fast API上构建GraphQL API,您需要安装Graphene。
石墨烯安装
让我们将
require.txt。
1 2 3 4 5 | fastapi==0.61.1 uvicorn==0.12.2 orator==0.9.9 psycopg2-binary==2.8.6 graphene==2.1.8 # 追加 |
安装。
1 | $ pip install -r requirements.txt |
什么是架构
GraphQL模式是GraphQL API规范的表示。
该模式汇总所有关系和类型定义。
创建架构
在项目根目录中创建
./schema.py
1 2 3 4 5 6 7 8 9 | import graphene class Query(graphene.ObjectType): say_hello = graphene.String(name=graphene.String(default_value='Test Driven')) @staticmethod def resolve_say_hello(parent, info, name): return f'Hello {name}' |
与FastAPI
合作
更新
1 2 3 4 5 6 7 8 9 10 11 12 13 | import graphene from fastapi import FastAPI from starlette.graphql import GraphQLApp from schema import Query app = FastAPI() app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=Query))) @app.get('/') def ping(): return {'ping': 'pong'} |
在这里,我们将模式传递给
1 | app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=Query))) |
重新引导服务器。
1 | $ uvicorn main:app --reload |
GraphiQL
通过访问以下内容,可以使用可以交互执行GraphQL查询的GraphQL。
http://本地主机:8000 / graphql
GraphQL查询执行
在左侧窗口中粘贴以下查询并执行。
1 2 3 | query { sayHello(name: "Taro") } |
确保返回以下内容。
1 2 3 4 5 | { "data": { "sayHello": "Hello Taro" } } |
GraphQL Pydantic
在这里,我们将创建一个Pydantic模型,以验证GraphQL API中的类型提示和查询。
为此,将石墨烯-pydantic软件包添加到
requirements.txt
1 2 3 4 5 6 | fastapi==0.61.1 uvicorn==0.12.2 orator==0.9.9 psycopg2-binary==2.8.6 graphene==2.1.8 graphene-pydantic==0.1.0 # 追加 |
安装
1 | $ pip install -r requirements.txt |
在
项目根目录中创建一个名为
./serializers.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | from typing import List, Optional from graphene_pydantic import PydanticInputObjectType, PydanticObjectType from pydantic import BaseModel class CommentsModel(BaseModel): id: int user_id: int post_id: int body: str class PostModel(BaseModel): id: int user_id: int title: str body: str comments: Optional[List[CommentsModel]] class UserModel(BaseModel): id: int name: str address: str phone_number: str sex: str posts: Optional[List[PostModel]] comments: Optional[List[CommentsModel]] class CommentGrapheneModel(PydanticObjectType): class Meta: model = CommentsModel class PostGrapheneModel(PydanticObjectType): class Meta: model = PostModel class UserGrapheneModel(PydanticObjectType): class Meta: model = UserModel class CommentGrapheneInputModel(PydanticInputObjectType): class Meta: model = CommentsModel exclude_fields = ('id', ) class PostGrapheneInputModel(PydanticInputObjectType): class Meta: model = PostModel exclude_fields = ('id', 'comments') class UserGrapheneInputModel(PydanticInputObjectType): class Meta: model = UserModel exclude_fields = ('id', 'posts', 'comments') |
导入的
此处,在
突变
GraphQL使用突变来修改数据。它主要在创建,更新和删除数据时使用。
创建创建对象
接下来,让我们使用突变来创建
用以下代码更新
./schema.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | import graphene from serializers import ( UserGrapheneInputModel, UserGrapheneModel, PostGrapheneInputModel, PostGrapheneModel, CommentGrapheneInputModel, CommentGrapheneModel, ) from models.comment import Comments from models.post import Post from models.user import User class Query(graphene.ObjectType): say_hello = graphene.String(name=graphene.String(default_value='Test Driven')) @staticmethod def resolve_say_hello(parent, info, name): return f'Hello {name}' class CreateUser(graphene.Mutation): class Arguments: user_details = UserGrapheneInputModel() Output = UserGrapheneModel @staticmethod def mutate(parent, info, user_details): user = User() user.name = user_details.name user.address = user_details.address user.phone_number = user_details.phone_number user.sex = user_details.sex user.save() return user class CreatePost(graphene.Mutation): class Arguments: post_details = PostGrapheneInputModel() Output = PostGrapheneModel @staticmethod def mutate(parent, info, post_details): user = User.find_or_fail(post_details.user_id) post = Post() post.title = post_details.title post.body = post_details.body user.posts().save(post) return post class CreateComment(graphene.Mutation): class Arguments: comment_details = CommentGrapheneInputModel() Output = CommentGrapheneModel @staticmethod def mutate(parent, info, comment_details): user = User.find_or_fail(comment_details.user_id) post = Post.find_or_fail(comment_details.post_id) comment = Comments() comment.body = comment_details.body user.comments().save(comment) post.comments().save(comment) return comment class Mutation(graphene.ObjectType): create_user = CreateUser.Field() create_post = CreatePost.Field() create_comment = CreateComment.Field() |
我为每个类(
突变时应用。
突变
的应用
还要更新
./main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 | import graphene from fastapi import FastAPI from starlette.graphql import GraphQLApp from schema import Query, Mutation app = FastAPI() app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=Query, mutation=Mutation))) @app.get('/') def ping(): return {'ping': 'pong'} |
运行createUser突变
重新启动Uvicorn,重新加载http://本地主机:8000 / graphql,在下面粘贴查询并运行createUser的突变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | mutation createUser { createUser(userDetails: { name: "Max Kun", address: "Tokyo", phoneNumber: "12345678", sex: "male" }) { id name posts { body comments { body } } } } |
如果收到以下响应,则说明您成功。
1 2 3 4 5 6 7 8 9 | { "data": { "createUser": { "id": 2, "name": "Max Kun", "posts": [] } } } |
运行createPost突变
接下来,让我们执行
1 2 3 4 5 6 7 8 9 10 | mutation createPost { createPost(postDetails: { userId: 2, title: "Hello", body: "This is the first post." }) { id } } |
您将收到以下响应。
1 2 3 4 5 6 7 | { "data": { "createPost": { "id": 1 } } } |
createComments运行变异
1 2 3 4 5 6 7 8 9 10 11 | mutation createComment { createComment(commentDetails: { userId: 2, postId: 1, body: "This is a test comment." }) { id body } } |
将返回以下响应。
1 2 3 4 5 6 7 8 | { "data": { "createComment": { "id": 1, "body": "This is a test comment." } } } |
询问
GraphQL使用查询以列表或单个对象的形式检索数据。
创建查询
更新
./schema.py
1 2 3 4 5 6 7 8 9 10 11 | class Query(graphene.ObjectType): say_hello = graphene.String(name=graphene.String(default_value='Test Driven')) list_users = graphene.List(UserGrapheneModel) @staticmethod def resolve_say_hello(parent, info, name): return f'Hello {name}' @staticmethod def resolve_list_users(parent, info): return User.all() |
获取用户列表
重新加载GraphiQL并运行以下查询以获取用户列表。
1 2 3 4 5 6 7 8 9 | query getAllUsers { listUsers { id name posts { title } } } |
结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | { "data": { "listUsers": [ { "id": 1, "name": "John Doe", "posts": [] }, { "id": 2, "name": "Max Kun", "posts": [ { "title": "Hello" } ] } ] } } |
特定用户信息的获取
接下来,创建一个返回特定用户信息的查询。
如示例中一样,更新
./schema.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Query(graphene.ObjectType): say_hello = graphene.String(name=graphene.String(default_value='Test Driven')) list_users = graphene.List(UserGrapheneModel) get_single_user = graphene.Field(UserGrapheneModel, user_id=graphene.NonNull(graphene.Int)) @staticmethod def resolve_say_hello(parent, info, name): return f'Hello {name}' @staticmethod def resolve_list_users(parent, info): return User.all() @staticmethod def resolve_get_single_user(parent, info, user_id): return User.find_or_fail(user_id) |
演说者具有内置的find_or_fail函数,如果传递了无效的ID,则会引发异常。
成功执行查询
让我们通过指定正确的用户ID来获取用户信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | query getUser { getSingleUser(userId: 2) { name posts { title comments { body } } comments { body } } } |
结果:我得到了特定用户信息的列表以及预期的发布和评论。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | { "data": { "getSingleUser": { "name": "Max Kun", "posts": [ { "title": "Hello", "comments": [ { "body": "This is a test comment." } ] } ], "comments": [ { "body": "This is a test comment." } ] } } } |
如果不需要帖子或评论信息,请按以下方式修改查询:
1 2 3 4 5 | query getUser { getSingleUser(userId: 2) { name } } |
结果:我刚得到用户信息。
1 2 3 4 5 6 7 | { "data": { "getSingleUser": { "name": "Max Kun" } } } |
如果您指定了错误的用户ID
如果我使用错误的用户ID运行查询该怎么办?
1 2 3 4 5 | query getUser { getSingleUser(userId: 9999) { name } } |
结果:返回错误。
如前所述,Orator具有内置的find_or_fail函数,如果传递了无效的ID,则会引发异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | { "data": { "getSingleUser": null }, "errors": [ { "message": "No query results found for model [User]", "locations": [ { "line": 2, "column": 3 } ], "path": [ "getSingleUser" ] } ] } |
测试
Graphene提供了用于测试Graphene应用程序的测试客户端。
准备
由于我们使用的是
pytest,因此需要向
requirements.txt
1 2 3 4 5 6 7 | fastapi==0.61.1 uvicorn==0.12.2 orator==0.9.9 psycopg2-binary==2.8.6 graphene==2.1.8 graphene-pydantic==0.1.0 pytest==6.1.1 # 追加 |
安装
1 | $ pip install -r requirements.txt |
创建测试预处理
接下来,在项目根目录中创建一个
在这里,描述执行测试之前要执行的过程。
./test/conftest.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | import graphene import pytest from graphene.test import Client from orator import DatabaseManager, Model, Schema from orator.migrations import DatabaseMigrationRepository, Migrator from models.comment import Comments from models.post import Post from models.user import User from schema import Query, Mutation @pytest.fixture(autouse=True) def setup_database(): DATABASES = { "sqlite": { "driver": "sqlite", "database": "test.db" } } db = DatabaseManager(DATABASES) Schema(db) Model.set_connection_resolver(db) repository = DatabaseMigrationRepository(db, "migrations") migrator = Migrator(repository, db) if not repository.repository_exists(): repository.create_repository() migrator.reset("migrations") migrator.run("migrations") @pytest.fixture(scope="module") def client(): client = Client(schema=graphene.Schema(query=Query, mutation=Mutation)) return client @pytest.fixture(scope="function") def user(): user = User() user.name = "John Doe" user.address = "United States of Nigeria" user.phone_number = 123456789 user.sex = "male" user.save() return user @pytest.fixture(scope="function") def post(user): post = Post() post.title = "Test Title" post.body = "this is the post body and can be as long as possible" user.posts().save(post) return post @pytest.fixture(scope="function") def comment(user, post): comment = Comments() comment.body = "This is a comment body" user.comments().save(comment) post.comments().save(comment) return comment |
下面描述代码的详细信息。
关于数据库固定装置
此处定义了数据库设置和迁移应用程序测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @pytest.fixture(autouse=True) def setup_database(): DATABASES = { "sqlite": { "driver": "sqlite", "database": "test.db" } } db = DatabaseManager(DATABASES) Schema(db) Model.set_connection_resolver(db) repository = DatabaseMigrationRepository(db, "migrations") migrator = Migrator(repository, db) if not repository.repository_exists(): repository.create_repository() migrator.reset("migrations") migrator.run("migrations") |
通过在
在执行每个测试功能之前,将调用
石墨烯客户端夹具
石墨烯这是用于创建测试客户端的装置。
1 2 3 4 | @pytest.fixture(scope="module") def client(): client = Client(schema=graphene.Schema(query=Query, mutation=Mutation)) return client |
在
参数中指定的
<表格>
tr>
header>
<身体>
tr>
tr>
tr>
tr>
tbody>
table>
其他灯具
这是用于创建用户,帖子和评论信息的装置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | @pytest.fixture(scope="function") def user(): user = User() user.name = "John Doe" user.address = "United States of Nigeria" user.phone_number = 123456789 user.sex = "male" user.save() return user @pytest.fixture(scope="function") def post(user): post = Post() post.title = "Test Title" post.body = "this is the post body and can be as long as possible" user.posts().save(post) return post @pytest.fixture(scope="function") def comment(user, post): comment = Comments() comment.body = "This is a comment body" user.comments().save(comment) post.comments().save(comment) return comment |
创建测试
创建一个名为
./test/test_query.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | def test_create_user(client): query = """ mutation { createUser(userDetails: { name: "Test User", sex: "male", address: "My Address", phoneNumber: "123456789", }) { id name address } } """ result = client.execute(query) assert result['data']['createUser']['id'] == 1 assert result['data']['createUser']['name'] == "Test User" def test_get_user_list(client, user): query = """ query { listUsers { name address } } """ result = client.execute(query) assert type(result['data']['listUsers']) == list def test_get_single_user(client, user): query = """ query { getSingleUser(userId: %s){ address } } """ % user.id result = client.execute(query) assert result['data']['getSingleUser'] is not None assert result['data']['getSingleUser']['address'] == user.address |
运行测试
使用以下命令在pytest中运行测试:
1 | $ python -m pytest -s test |
执行
命令后,将执行所有测试项目。
1 2 3 4 5 6 7 | ================================================ test session starts ================================================ platform linux -- Python 3.9.1, pytest-6.1.1, py-1.10.0, pluggy-0.13.1 rootdir: /home/taro/Workspace/Python/fastapi-graphql collected 3 items test/test_query.py ... =========================================== 3 passed in 0.51s ======================================================= |
测试成功通过????
概要
介绍了如何使用FastAPI,Orator ORM和pytest开发和测试GraphQL API。
快速API作为标准提供类型提示和验证,因此它似乎与使用Pydantic的GraphQL API开发兼容。
明天是ZOZO Technologies出现日历#2 @yokawasa的文章!期待!