Ariadne教学:将GraphQL整合到Python中

1,602 阅读9分钟

作为一个前端开发者和一个长期的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 uvicorncd 安装到你的endpoint.py is ,并运行uvicorn endpoint:app. 然后,访问127.0.0.1:8000 ,在那里你会看到Ariadne的GraphQL界面。它看起来很酷。

Ariadne GraphQL Uvicorn Integration

只有一个注意事项:我在这里大致遵循的介绍性文档页面在中途提出了一个很好的观点。"现实世界的解析器很少那么简单:它们通常从某个来源(如数据库)读取数据,处理输入,或者在父对象的上下文中解析值(原文如此)。"

翻译成简单的英语?"我们的API完全没有做任何有用的事情。你给它一个查询,它告诉你,Hello world! ,这既不有趣也没有帮助。我们创建的解析器函数需要接受输入,从某个地方获得数据,并返回一个结果,这样才有价值。"

好了,现在我们已经有了我们的模板,让我们试着通过访问一个由Python数组和字典组成的初级数据库来使这个API有其价值。

构建一个GraphQL API样本

嗯......我们应该建立什么?我的想法是这样的。

  • 输入的查询应该以我最喜欢的情景喜剧之一的名字作为参数。
  • 查询将返回一个Sitcom 类型,其中应该有名字(将是一个字符串)、number_of_seasons (Int)和字符(一个字符数组)的字段。
  • 字符类型将有first_name,last_name, 和actor_name 字段,都是字符串。

这听起来是可行的!我们只有两种类型 (sitcomcharacter),而且我们所暴露的数据可以很容易地存储在一个 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),然后将它与其他一些第三方服务结合在一起。

这样一来,所有难以合并的东西都是网格的问题,而不是你的问题。我已经这样做了好几次,看到这一切,我很欣慰。

结论

这方面的内容不多。我试图尽可能多地解释一些细节,以备你想进一步探索其中的任何一点,但这个技术是相当简单的。