作为一个前端开发者和一个长期的Jamstacker,我有足够多的时间对我们使用API的方式感到沮丧。REST协议似乎是在正确的方向上迈出的一步(确实如此),但我仍然不怀好意地抱怨它的局限性,尽管有改进。
因此,当我听到GraphQL时,我被震惊了。
这个想法很简单:API本身定义了它能理解的数据种类,并向用户公开了一个单一的端点。然后,用户向该端点提供一个查询,该查询看起来类似于JSON,没有所有讨厌的值、引号和逗号。
API会返回该查询的JSON版本,其中的值是由你要求的所有数据填写的。这是一个令人难以置信的简单想法,但它几乎解决了我在使用API时遇到的所有问题。
什么是Ariadne?
通常,GraphQL APIs是用JavaScript创建的,但我的第一爱好是Python,这就是为什么我看了Ariadne。Ariadne是一个Python库,可以帮助你创建GraphQL API而不需要额外的负担。
在这篇文章中,我将记录在Python 3.8中制作Ariadne GraphQL API的过程,这将使用户能够访问一个简单的数组/dictionary结构。
开始使用Ariadne
我将假设你已经在你的电脑上设置了Python,并且你已经用pip3 install ariadne 安装了Ariadne。
不过我想在这里给你一个小小的提示:坚持使用单一的数据源(比如一个数据库、一层业务逻辑或一个Python dict)。当我第一次听说GraphQL时,我的第一个想法是,我可以用它把我正在使用的所有其他API合并到一个端点中--我可以摆脱REST和SOAP API的所有不一致之处,并毫无困难地获得我需要的所有数据和功能。
这是有可能的,但要自己去开发,麻烦就大了。这个概念被称为API网格,它是由TakeShape.io的人开创的。如果你有兴趣了解更多关于TakeShape的信息,请随时查看他们的新文档页面,但为了简单起见,我将坚持在这里公开一个单一的数据源。
Ariadne如何工作
现在,我们来看看Ariadne是如何工作的。你可以跟着他们的快速入门指南走,但我要把它简化。它是这样的。
首先,使用GraphQL的特殊模式定义语言来定义一个类型。它类似于TypeScript接口,你定义一个对象的键和每个键的值的类型。
Ariadne中的每个应用程序都需要一个叫做Query 的类型,因为这将与程序的输入进行比较,所以我们现在就来做这个。它看起来会像这样。
type Query {
hello: String!
}
这是一个非常基本的定义。简单地说,我们定义了一个叫做Query 的类型。它有一个键,叫做hello ,它将永远是一个字符串。这里有一个奖励:该行末尾的!意味着如果对象符合这个类型,hello 将永远在对象中。如果你省略了感叹号,那么hello 将是可选的。
现在,在我们的 Python 文件中 (我把它叫做endpoint.py),我们要把这个类型定义粘到一个字符串中,并把它传给 Ariadne 的gql 函数。到目前为止,我们的文件看起来像这样。
from ariadne import gql
typedefs = """
type Query {
hello: String!
}
"""
typedefs = gql(type_defs)
这将验证我们的类型定义,如果我们写得不对,就抛出一个错误。
接下来,Ariadne 希望我们创建一个ObjectType 类的实例,并传入我们类型的名称。简而言之,这将是我们要做的类型的 Python 表示。
我们还将在最后添加一些模板,并将我们的类型定义移到那里。现在endpoint.py 看起来像这样。
from ariadne import ObjectType, gql, make_executable_schema
from ariadne.asgi import GraphQL
basetype = ObjectType("Query") # there is a shortcut for this, but explicit is better than implicit
type_defs = """
type Query {
hello: String!
}
"""
app = GraphQL(
make_executable_schema(
gql(type_defs),
basetype
),
debug=True
)
Ariadne 的主要目的是扫描输入的查询,对于每个键,运行一个解析器函数来获得该键的值。它通过装饰器来实现这一点,这是一种很酷的Pythonic方式,可以把你的函数交给Ariadne而不需要更多的模板。这是我们的endpoint.py ,其中有一个用于我们的hello 键的解析器函数。
from ariadne import ObjectType, gql, make
这就差不多了。Ariadne有许多迷人的和有用的功能(说真的,翻翻他们的文档),但这就是你需要开始和了解它如何工作的全部。如果你有兴趣测试这个,但它需要放在一个服务器上。
你可以用Uvicorn把你的本地机器暂时变成一个服务器。简而言之,你要用pip install uvicorn ,cd 安装到你的endpoint.py is ,并运行uvicorn endpoint:app. 然后,访问127.0.0.1:8000 ,在那里你会看到Ariadne的GraphQL界面。它看起来很酷。
只有一个注意事项:我在这里大致遵循的介绍性文档页面在中途提出了一个很好的观点。"现实世界的解析器很少那么简单:它们通常从某个来源(如数据库)读取数据,处理输入,或者在父对象的上下文中解析值(原文如此)。"
翻译成简单的英语?"我们的API完全没有做任何有用的事情。你给它一个查询,它告诉你,Hello world! ,这既不有趣也没有帮助。我们创建的解析器函数需要接受输入,从某个地方获得数据,并返回一个结果,这样才有价值。"
好了,现在我们已经有了我们的模板,让我们试着通过访问一个由Python数组和字典组成的初级数据库来使这个API有其价值。
构建一个GraphQL API样本
嗯......我们应该建立什么?我的想法是这样的。
- 输入的查询应该以我最喜欢的情景喜剧之一的名字作为参数。
- 查询将返回一个
Sitcom类型,其中应该有名字(将是一个字符串)、number_of_seasons(Int)和字符(一个字符数组)的字段。 - 字符类型将有
first_name,last_name, 和actor_name字段,都是字符串。
这听起来是可行的!我们只有两种类型 (sitcom 和character),而且我们所暴露的数据可以很容易地存储在一个 Python 字典结构中。下面是我将使用的字典。
characters = {
"jeff-winger": {
"first_name": "Jeffrey",
"last_name": "Winger",
"actor_name": "Joel McHale"
},
"michael-scott": {
"first_name": "Michael",
"last_name": "Scott",
"actor_name": "Steve Carell"
},
...
}
sitcoms = {
"office": {
"name": "The Office (US)",
"number_of_seasons": 9, # but let's be real, 7
"characters": [
"michael-scott",
"jim-halpert",
"pam-beesly",
"dwight-schrute",
...
]
},
"community": {
"name": "Community",
"number_of_seasons": 6, #sixseasonsandamovie
"characters": [
"jeff-winger",
"britta-perry",
"abed-nadir",
"ben-chang",
...
]
},
...
}
我们要定义我们的类型,就像我们先前对我们的query 类型所做的那样。让我们试试这个。
query = ObjectType("Query")
sitcom = ObjectType("Sitcom")
character = ObjectType("Character")
type_defs = """
type Query {
result(name: String!): Sitcom
}
type Sitcom {
name: String!
number_of_seasons: Int!
characters: [Character!]!
}
type Character {
first_name: String!
last_name: String!
actor_name: String!
}
"""
app = GraphQL(
make_executable_schema(
gql(type_defs),
query,
sitcom,
character
),
debug=True
)
括号里是query 类型,它是一个参数。我们在query 类型的result 键上传入一个名称(它将总是一个字符串),这将被发送到我们的解析器上。我一会儿会更多地讨论这个问题。
如果你想知道[Character!]! ,这只是意味着数组是必需的,以及数组中的字符。在实践中,数组必须存在,而且里面必须有字符。
另外,在最后的模板中,我们将三种类型都传给了make_executable_schema 函数。这告诉Ariadne,它可以开始使用它们。事实上,我们可以在那里添加任意多的类型。
所以,这将是如何工作的。客户端将发送一个看起来像这样的请求。
<code>{
result(name:"community")
}</code>
服务器将接受这个请求,向结果字段的解析器发送"community" ,并不只是返回任何情景剧,而是返回正确的情景剧。现在让我们建立这些解析器。
这里是我们完整的endpoint.py 。
from ariadne import ObjectType, gql, make_executable_schema
from ariadne.asgi import GraphQL
import json
with open('sitcoms.json') as sitcom_file:
sitcom_list = json.loads(sitcom_file.read())
with open('characters.json') as character_file:
character_list = json.loads(character_file.read())
query = ObjectType("Query")
sitcom = ObjectType("Sitcom")
character = ObjectType("Character")
type_defs = """
type Query {
result(name: String!): Sitcom
}
type Sitcom {
name: String!
number_of_seasons: Int!
characters: [Character!]!
}
type Character {
first_name: String!
last_name: String!
actor_name: String!
}
"""
@query.field("result")
def getSitcom(*_, name):
return sitcom_list[name] if name in sitcom_list else None
@sitcom.field("characters")
def getCharacters(sitcom, _):
characters = []
for name in sitcom["characters"]:
characters.append(character_list[name] if name in character_list else None)
return characters
app = GraphQL(
make_executable_schema(
gql(type_defs),
query,
sitcom,
character
),
debug=True
)
这就是整个程序!我们正在使用JSON文件中的数据来填写对输入的GraphQL查询的响应。
使用Ariadne的其他好处
虽然我们不一定要做完!这里有一些我想到的关于下一步要做什么的想法。
我们只是使用了一个初级的JSON数据存储结构,这是很糟糕的做法,但对于像这样的样本应用来说是合理的。对于比这个玩具应用更大的东西,我们想使用一个更坚固的数据源,比如一个合适的数据库。
我们可以有一个MySQL数据库,为情景喜剧和人物各设一个表,并在解析器函数中获取这些数据。另外,查询本身只是我们用GraphQL和Ariadne可以做的事情的一半。突变是另一半。它们可以让你更新现有的记录,添加新的记录,或者有可能删除行。在Ariadne中设置这些是相当容易的。
当然,创建一个API来跟踪情景喜剧和人物是有点无意义的,但这是一个有趣的实验。如果我们围绕更有用的数据建立这样的GraphQL服务,这一切都可以被更有效地利用。比如你正在运行一个现有的REST API--为什么不用GraphQL来提供这些数据?
最后,当我们创建一个GraphQL API时,往往会试图从我们自己的数据库中获取数据,并从外部来源(如一些第三方API)中合并数据,这很诱人。你可以通过在解析器中通过HTTP向这些外部API发出请求来做到这一点,但这将大大减少你的程序,让你自己去担心边缘情况和错误处理。
相信我,这比它的价值更麻烦。然而,为了进一步推进这个项目,你可以让你的Ariadne应用程序从你的内部数据库中获取数据,将你刚刚创建的API插入到一个API网格中(如TakeShape),然后将它与其他一些第三方服务结合在一起。
这样一来,所有难以合并的东西都是网格的问题,而不是你的问题。我已经这样做了好几次,看到这一切,我很欣慰。
结论
这方面的内容不多。我试图尽可能多地解释一些细节,以备你想进一步探索其中的任何一点,但这个技术是相当简单的。