如何使用 Django 和 GraphQL 创建 URL Shortener

_ 作者选择了 Girls Who 代码以作为 Write for Donations计划的一部分接收捐款。

介绍

[GraphQL] (https://graphql.org/)是Facebook作为REST的替代品而创建和开源的API标准. APIs (英语). 相对于REST API,GraphQL使用一个打入的系统来定义其数据结构,所有发送和接收的信息都必须符合预先定义的策略. 也暴露出所有通信的单一端点,而不是针对不同资源的多个URL,并且通过只返回客户端要求的数据来解决了"过度"问题,从而产生更小更简洁的响应.

在本教程中,您将创建一个 URL shortener 的后端 - 一个服务,可以使用任何 URL 并生成更短,更易于阅读的版本 - 同时潜入 GraphQL 概念,如 查询和突变,以及工具,如 GraphiQL 接口.您可能已经使用过此类服务,如 bit.ly

自图表以来 QL是一种语言不可知论技术,它被执行在各种语言和框架之上. 在此,您将使用通用 [Python 编程语言] (https://www.digitalocean.com/community/tutorial_series/how-to-code-in-python-3), [Django web 框架] (https://andsky.com/tags/django?type=tutorials),以及 [Graphene-Django] (https://docs.graphene-python.org/projects/django/en/latest/) 库作为 GraphQL Python 执行,并为 Django 提供特定的集成.

前提条件

第1步:创建Django项目

在此步骤中,您将安装应用程序所需的所有工具,并设置您的Django项目。

一旦您创建了项目目录并启动了虚拟环境,如前提所述,请使用Python包管理器pip安装所需的包,本教程将安装Django版本 2.1.7和Graphene-Django版本 2.2.0或更高版本:

1pip install "django==2.1.7" "graphene-django>==2.2.0"

接下来,您将使用django-admin命令创建一个 Django 项目. 一个项目是默认的 Django 锅炉板 – 一组文件夹和文件,包含开始开发 Web 应用程序所需的一切。

1django-admin startproject shorty .

在创建您的项目后, 您将运行 [Django 迁移 ([ LINK0] ) 。 这些文件包含由Django生成的Python代码,并负责按照Django模型来改变应用程序的结构. 例如,修改可能包括创建表格。 默认情况下,Django会带着自己负责像Django Authority这样的子系统的一套迁移程序来进行,所以需要用以下命令执行它们:

1python manage.py migrate

这个命令使用Python解释器调用名为manage.py的Django脚本,负责管理项目的不同方面,例如创建应用程序或运行迁移。

这将产生类似于以下的产量:

 1[secondary_label Output]
 2Operations to perform:
 3  Apply all migrations: admin, auth, contenttypes, sessions
 4Running migrations:
 5  Applying contenttypes.0001_initial... OK
 6  Applying auth.0001_initial... OK
 7  Applying admin.0001_initial... OK
 8  Applying admin.0002_logentry_remove_auto_add... OK
 9  Applying admin.0003_logentry_add_action_flag_choices... OK
10  Applying contenttypes.0002_remove_content_type_name... OK
11  Applying auth.0002_alter_permission_name_max_length... OK
12  Applying auth.0003_alter_user_email_max_length... OK
13  Applying auth.0004_alter_user_username_opts... OK
14  Applying auth.0005_alter_user_last_login_null... OK
15  Applying auth.0006_require_contenttypes_0002... OK
16  Applying auth.0007_alter_validators_add_error_messages... OK
17  Applying auth.0008_alter_user_username_max_length... OK
18  Applying auth.0009_alter_user_last_name_max_length... OK
19  Applying sessions.0001_initial... OK

一旦Django的数据库准备好运行,请启动其本地开发服务器:

1python manage.py runserver

这将给出:

1[secondary_label Output]
2Performing system checks...
3
4System check identified no issues (0 silenced).
5March 18, 2020 - 15:46:15
6Django version 2.1.7, using settings 'shorty.settings'
7Starting development server at http://127.0.0.1:8000/
8Quit the server with CONTROL-C.

此命令将删除您的终端中的提示,并启动服务器。

请访问本地浏览器中的http://127.0.0.1:8000页面,您将看到此页面:

Django local server front page

要停止服务器并返回终端,请按CTRL+C。每当您需要访问浏览器时,请确保之前的命令正在运行。

接下来,您将通过在项目中启用 Django-Graphene 库来完成此步骤。 Django 具有):

1vim shorty/settings.py

「settings.py」文件管理您项目中的所有设置,在其内部搜索「INSTALLED_APPS」条目,并添加「graphene_django」条目:

 1[label shorty/shorty/settings.py]
 2...
 3INSTALLED_APPS = [
 4    'django.contrib.admin',
 5    'django.contrib.auth',
 6    'django.contrib.contenttypes',
 7    'django.contrib.sessions',
 8    'django.contrib.messages',
 9    'django.contrib.staticfiles',
10    'graphene_django',
11]
12...

此插件告知 Django 您将使用名为graphene_django的应用程序,您在步骤 1 中安装了该应用程序。

在文件的底部,添加以下变量:

1[label shorty/shorty/settings.py]
2...
3GRAPHENE = {
4    'SCHEMA': 'shorty.schema.schema',
5}

在 GraphQL 中,一个 Schema 包含所有对象类型,如资源、查询和突变。

更改后,保存并关闭文件。

现在你已经配置了Django项目,下一步,你将创建一个Django应用程序及其模型。

步骤 2 — 设置 Django 应用程序和模型

Django 平台通常由一个项目和多个应用程序或 apps 组成,一个应用程序描述了项目内部的一组功能,如果设计得很好,可以在 Django 项目中重复使用。

在此步骤中,您将创建一个名为shortener的应用程序,负责实际的URL缩短功能。

1python manage.py startapp shortener

在这里,您使用了startapp app_name的参数,指示manage.py创建一个名为shortener的应用程序。

要完成应用创建,打开shorty/settings.py文件

1vim shorty/settings.py

将应用的名称添加到您之前修改的相同的INSTALLED_APPS条目:

 1[label shorty/shorty/settings.py]
 2...
 3INSTALLED_APPS = [
 4    'django.contrib.admin',
 5    'django.contrib.auth',
 6    'django.contrib.contenttypes',
 7    'django.contrib.sessions',
 8    'django.contrib.messages',
 9    'django.contrib.staticfiles',
10    'graphene_django'
11    'shortener',
12]
13...

保存并关闭文件。

随着你的)是Django的关键功能之一。它们被用来以Pythonic的方式表示数据库,允许你使用Python代码管理,查询和存储数据。

在打开models.py文件以获取更改之前,本教程将概述您将进行的更改。

您的模型文件 - shortener/models.py - 在您更换现有代码后将包含以下内容:

1[label shorty/shortener/models.py]
2from hashlib import md5
3
4from django.db import models

在这里,您将导入所需的代码所需的包,您将添加顶部的行 from hashlib import md5 以导入 Python 标准库,用于创建 URL 的 hash

<$>[警告] 警告: 本教程指的是 hash 作为一个函数的结果,它需要输入并总是返回相同的输出。

请注意,MD5有碰撞问题(https://en.wikipedia.org/wiki/MD5# Collision_vulnerabilities)并应避免在生产中。

接下来,您将添加一个名为URL的模型,其中包含以下字段:

  • full_url:要缩短的 URL. * url_hash:代表完整 URL 的短哈希. * click:短 URL 被访问了多少次. * created_at: URL 创建的日期和时间。
1[label shorty/shortener/models.py]
2...
3
4class URL(models.Model):
5    full_url = models.URLField(unique=True)
6    url_hash = models.URLField(unique=True)
7    clicks = models.IntegerField(default=0)
8    created_at = models.DateTimeField(auto_now_add=True)

您将通过将 MD5 哈希算法应用到)`方法中返回的第 10 个字符,每次 Django 将条目保存到数据库时都会执行。

所提到的操作将被添加到您的URL模型中,使用此代码:

 1[label shorty/shortener/models.py]
 2...
 3
 4    def clicked(self):
 5        self.clicks += 1
 6        self.save()
 7
 8    def save(self, *args, **kwargs):
 9        if not self.id:
10            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]
11
12        return super().save(*args, **kwargs)

现在你已经检查了代码,打开‘shortener/models.py’文件:

1vim shortener/models.py

代替代码为以下内容:

 1[label shorty/shortener/models.py]
 2from hashlib import md5
 3
 4from django.db import models
 5
 6class URL(models.Model):
 7    full_url = models.URLField(unique=True)
 8    url_hash = models.URLField(unique=True)
 9    clicks = models.IntegerField(default=0)
10    created_at = models.DateTimeField(auto_now_add=True)
11
12    def clicked(self):
13        self.clicks += 1
14        self.save()
15
16    def save(self, *args, **kwargs):
17        if not self.id:
18            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]
19
20        return super().save(*args, **kwargs)

请确保保存并关闭文件。

要在数据库中应用这些更改,您需要通过运行以下命令创建迁移:

1python manage.py makemigrations

这将为您带来以下结果:

1[secondary_label Output]
2Migrations for 'shortener':
3  shortener/migrations/0001_initial.py
4    - Create model URL

然后执行迁移:

1python manage.py migrate

您将在您的终端中看到以下输出:

1[secondary_label Output]
2Operations to perform:
3  Apply all migrations: admin, auth, contenttypes, sessions, shortener
4Running migrations:
5  Applying shortener.0001_initial... OK

现在您已经设置了模型,下一步将创建 GraphQL 终端和查询。

第3步:创建查询

REST 架构在不同的终端中呈现不同的资源,每个终端都包含一个明确的数据结构,例如,您可以从 /api/users 获取用户列表,始终期待相同的字段。

首先,创建一个查询以获取所有URL,你需要几个东西:

  • URL 类型,链接到您先前定义的模型。 * 命名为urls的查询声明. * 解决您的查询的方法,即从数据库中获取所有 URL 并将其返回客户端。

创建一个名为shortener/schema.py的新文件:

1vim shortener/schema.py

首先,添加Python的导入语句:

1[label shorty/shortener/schema.py]
2import graphene
3from graphene_django import DjangoObjectType
4
5from .models import URL

第一行导入主要的图形库,其中包含基本的 GraphQL 类型,如列表DjangoObjectType是从任何 Django 模型创建一个 Schema 定义的助手,第三行导入您之前创建的URL模型。

然后,通过添加以下行来为URL模型创建一个新的 GraphQL 类型:

1[label shorty/shortener/schema.py]
2...
3class URLType(DjangoObjectType):
4    class Meta:
5        model = URL

最后,添加这些行来为URL模型创建查询类型:

1[label shorty/shortener/schema.py]
2...
3class Query(graphene.ObjectType):
4    urls = graphene.List(URLType)
5
6    def resolve_urls(self, info, **kwargs):
7        return URL.objects.all()

此代码创建一个Query类,其中有一个名为urls的字段,该字段是以前定义的URLType的列表。

完整的shortener/schema.py文件在这里显示:

 1[label shorty/shortener/schema.py]
 2import graphene
 3from graphene_django import DjangoObjectType
 4
 5from .models import URL
 6
 7class URLType(DjangoObjectType):
 8    class Meta:
 9        model = URL
10
11class Query(graphene.ObjectType):
12    urls = graphene.List(URLType)
13
14    def resolve_urls(self, info, **kwargs):
15        return URL.objects.all()

保存并关闭文件。

现在必须将所有查询添加到主方案中,将其视为所有资源的持有人。

shorty/schema.py路径中创建一个新的文件,并与编辑器一起打开它:

1vim shorty/schema

通过添加以下行来导入下列Python包. 如前所述,第一行包含基本的GraphQL类型. 第二行导入先前创建的Schema文件。

1[label shorty/shorty/schema.py]
2import graphene
3
4import shortener.schema

接下来,添加主Query类,通过继承,它将持有所有创建的查询和未来的操作:

1[label shorty/shorty/schema.py]
2...
3class Query(shortener.schema.Query, graphene.ObjectType):
4    pass

最后,创建方案变量:

1[label shorty/shorty/schema.py]
2...
3schema = graphene.Schema(query=Query)

您在步骤 2 中定义的SCHEMA设置指向您刚刚创建的schema变量。

完整的 shorty/schema.py 文件在这里显示:

1[label shorty/shorty/schema.py]
2import graphene
3
4import shortener.schema
5
6class Query(shortener.schema.Query, graphene.ObjectType):
7    pass
8
9schema = graphene.Schema(query=Query)

保存并关闭文件。

接下来,启用 GraphQL 端点和 GraphiQL接口,这是一个用于与 GraphQL 系统交互的图形 Web 接口。

打开shorty/urls.py文件:

1vim shorty/urls.py

为了学习目的,删除文件内容并将其保存,以便您可以从头开始。

您将添加的第一行是Python导入陈述:

1[label shorty/shorty/urls.py]
2from django.urls import path
3from django.views.decorators.csrf import csrf_exempt
4
5from graphene_django.views import GraphQLView

Django 使用 path 函数为 GraphiQL 接口创建一个可访问的 URL。随后,您将导入 csrf_exempt,允许客户端将数据发送到服务器。 在 [Graphene Documentation] 中可以找到完整的解释(https://docs.graphene-python.org/projects/django/en/latest/installation/# csrf-exempt)。 在最后一行中,您通过 `GraphQLView’导入了接口的实际代码。

然后创建一个名为urlpatterns的变量。

1[label shorty/shorty/urls.py]
2...
3urlpatterns = [
4    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
5]

这将汇集所有必要的代码,使 GraphiQL 接口可用在 graphql/ 路径中:

完整的shortener/urls.py文件在这里显示:

1[label shorty/shorty/urls.py]
2from django.urls import path
3from django.views.decorators.csrf import csrf_exempt
4
5from graphene_django.views import GraphQLView
6
7urlpatterns = [
8    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
9]

保存文件并关闭它。

回到终端,运行):

1python manage.py runserver

打开您的网页浏览器在 http://localhost:8000/graphql 地址. 您将被介绍这个屏幕:

GraphiQL interface

GraphiQL 是一个界面,您可以运行 GraphQL 陈述并查看结果,其中一个功能是右上方的Docs部分,因为 GraphQL 中的所有内容都被键入,因此您可以获得有关所有类型、查询、突变等的免费文档。

浏览页面后,将您的第一个查询插入主文本区域:

1query {
2  urls {
3    id
4    fullUrl
5    urlHash
6    clicks
7    createdAt
8  }
9}

此内容显示了 GraphQL 查询的结构:首先,您使用关键字查询告诉服务器,您只需要一些数据返回。接下来,您使用在Query类内的shortener/schema.py文件中定义的urls字段。

现在,点击左上角的 播放箭头按钮

您将收到以下回复,表示您仍然没有URL:

1[secondary_label Output]
2{
3  "data": {
4    "urls": []
5  }
6}

这表明 GraphQL 正在工作. 在您的终端中,按CTRL+C来停止您的服务器。

在此步骤中,您已经完成了很多工作,创建了 GraphQL 终端,创建了一个查询以获取所有 URL,并启用了 GraphiQL 接口。

第4步:创建突变

大多数应用程序可以通过添加、更新或删除数据来更改数据库状态. 在 GraphQL 中,这些操作称为 Mutations

要创建你的第一个突变,打开‘shortener/schema.py’:

1vim shortener/schema.py

在文件的末尾,开始添加一个名为CreateURL的新类:

1[label shorty/shortener/schema.py]
2...
3class CreateURL(graphene.Mutation):
4    url = graphene.Field(URLType)

该类继承了graphene.Mutation辅助器,以具有 GraphQL 突变的能力。它还具有属性名称url,定义了转换完成后服务器返回的内容。

接下来,将名为论点的子类添加到已经定义的类:

1[label shorty/shortener/schema.py]
2...
3    class Arguments:
4        full_url = graphene.String()

这里,你期待一个名为full_url的参数,具有String的内容:

现在添加以下行来创建变异方法:

1[label shorty/shortener/schema.py]
2...
3
4    def mutate(self, info, full_url):
5        url = URL(full_url=full_url)
6        url.save()

这种变异方法通过从客户端接收数据并将其保存到数据库中来完成很多工作,最终返回包含新创建项目的类本身。

最后,通过添加这些行创建一个突变类,以保持您的应用程序的所有突变:

1[label shorty/shortener/schema.py]
2...
3
4class Mutation(graphene.ObjectType):
5    create_url = CreateURL.Field()

到目前为止,你只有一种突变叫做create_url

完整的shortener/schema.py文件在这里显示:

 1[label shorty/shortener/schema.py]
 2import graphene
 3from graphene_django import DjangoObjectType
 4
 5from .models import URL
 6
 7class URLType(DjangoObjectType):
 8    class Meta:
 9        model = URL
10
11class Query(graphene.ObjectType):
12    urls = graphene.List(URLType)
13
14    def resolve_urls(self, info, **kwargs):
15        return URL.objects.all() 
16
17class CreateURL(graphene.Mutation):
18    url = graphene.Field(URLType)
19
20    class Arguments:
21        full_url = graphene.String()
22
23    def mutate(self, info, full_url):
24        url = URL(full_url=full_url)
25        url.save()
26
27        return CreateURL(url=url)
28
29class Mutation(graphene.ObjectType):
30    create_url = CreateURL.Field()

关闭并保存文件。

要完成添加突变,请更改 shorty/schema.py 文件:

1vim shorty/schema.py

更改文件以包含以下突出代码:

 1[label shorty/shorty/schema.py]
 2
 3import graphene
 4
 5import shortener.schema
 6
 7class Query(shortener.schema.Query, graphene.ObjectType):
 8    pass
 9
10class Mutation(shortener.schema.Mutation, graphene.ObjectType):
11    pass
12
13schema = graphene.Schema(query=Query, mutation=Mutation)

保存并关闭文件. 如果您没有运行本地服务器,请启动它:

1python manage.py runserver

在您的 Web 浏览器中,导航到 http://localhost:8000/graphql. 在 GraphiQL Web 界面中执行您的第一个突变,运行以下语句:

 1mutation {
 2  createUrl(fullUrl:"https://www.digitalocean.com/community") {
 3    url {
 4      id
 5      fullUrl
 6      urlHash
 7      clicks
 8      createdAt
 9    }
10  }
11}

您用createURL名称、fullUrl参数和您在url字段内定义的响应中想要的数据组成了突变。

输出将包含您刚刚在 GraphQL数据字段中创建的 URL 信息,如下所示:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "createUrl": {
 5      "url": {
 6        "id": "1",
 7        "fullUrl": "https://www.digitalocean.com/community",
 8        "urlHash": "077880af78",
 9        "clicks": 0,
10        "createdAt": "2020-01-30T19:15:10.820062+00:00"
11      }
12    }
13  }
14}

随后,一个 URL 被添加到数据库中,其 hashed 版本,正如您可以在urlHash字段中看到的那样。

1query {
2  urls {
3    id
4    fullUrl
5    urlHash
6    clicks
7    createdAt
8  }
9}

输出将显示存储的 URL:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "urls": [
 5      {
 6        "id": "1",
 7        "fullUrl": "https://www.digitalocean.com/community",
 8        "urlHash": "077880af78",
 9        "clicks": 0,
10        "createdAt": "2020-03-18T21:03:24.664934+00:00"
11      }
12    ]
13  }
14}

您也可以尝试执行相同的查询,但只要求您想要的字段。

接下来,再试一次用不同的URL:

 1mutation {
 2  createUrl(fullUrl:"https://www.digitalocean.com/write-for-donations/") {
 3    url {
 4      id
 5      fullUrl
 6      urlHash
 7      clicks
 8      createdAt
 9    }
10  }
11}

产量将是:

 1[secondary_label Output]
 2{
 3  "data": {
 4    "createUrl": {
 5      "url": {
 6        "id": "2",
 7        "fullUrl": "https://www.digitalocean.com/write-for-donations/",
 8        "urlHash": "703562669b",
 9        "clicks": 0,
10        "createdAt": "2020-01-30T19:31:10.820062+00:00"
11      }
12    }
13  }
14}

系统现在可以创建短的URL并列出它们,在下一步,您将允许用户通过其短版本访问URL,将其重定向到正确的页面。

步骤5 - 创建访问终端

在此步骤中,您将使用 Django 观点 - 采取请求并返回响应的方法 - 将访问 http://localhost:8000/url_hash 终端的任何人重定向到其完整的 URL。

打开shortener/views.py文件与您的编辑器:

1vim shortener/views.py

首先,通过用以下行替换内容来导入两个包:

1[label shorty/shortener/views.py]
2from django.shortcuts import get_object_or_404, redirect
3
4from .models import URL

这些将在稍后更详细地解释。

接下来,您将创建一个名为root的Django View。添加这个代码片段,负责您的文件末尾的View:

1[label shorty/shortener/views.py]
2...
3
4def root(request, url_hash):
5    url = get_object_or_404(URL, url_hash=url_hash)
6    url.clicked()
7
8    return redirect(url.full_url)

在函数内部,第一行试图使用url_hash参数从数据库中获取URL。如果没有找到,它会将HTTP 404错误返回客户端,这意味着资源丢失了。之后,它会增加URL输入的点击属性,确保跟踪该URL的访问次数。

完整的shortener/views.py文件在这里显示:

 1[label shorty/shortener/views.py]
 2from django.shortcuts import get_object_or_404, redirect
 3
 4from .models import URL
 5
 6def root(request, url_hash):
 7    url = get_object_or_404(URL, url_hash=url_hash)
 8    url.clicked()
 9
10    return redirect(url.full_url)

保存并关闭文件。

接下来,打开shorty/urls.py:

1vim shorty/urls.py

添加以下突出代码以启用视图。

 1[label shorty/shorty/urls.py]
 2
 3from django.urls import path
 4from django.views.decorators.csrf import csrf_exempt
 5
 6from graphene_django.views import GraphQLView
 7
 8from shortener.views import root
 9
10urlpatterns = [
11    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
12    path('<str:url_hash>/', root, name='root'),
13]

root视图将可在您的服务器的/路径中访问,接受一个url_hash作为字符串参数。

如果您没有运行本地服务器,请通过执行python manage.py runserver命令启动该命令。

要测试你的新添加,打开你的网页浏览器,并访问http://localhost:8000/077880af78 URL. 请注意,URL的最后一部分是由突变从步骤5创建的哈希,你将被重定向到哈希的URL页面,在这种情况下,DigitalOcean社区网站。

现在你有 URL 重定向工作,你会通过实施错误处理执行突变来使应用程序更安全。

第6步:实施错误处理

处理错误是所有应用程序的最佳做法,因为开发人员通常不控制会发送到服务器的内容。在这种情况下,您可以尝试预测错误并最大限度地减少其影响。

作为一个编写的系统,GraphQL可以在一个叫做 Schema Validation 的操作中验证客户端要求和接收的一切。

在您的浏览器中再次导航到http://localhost:8000/graphql,并在 GraphiQL 界面内执行下一个查询,使用iDontExist字段:

 1query {
 2  urls {
 3    id
 4    fullUrl
 5    urlHash
 6    clicks
 7    createdAt
 8    iDontExist
 9  }
10}

由于查询中没有定义的iDontExist字段,因此 GraphQL 会返回一个错误消息:

 1[secondary_label Output]
 2
 3{
 4  "errors": [
 5    {
 6      "message": "Cannot query field \"iDontExist\" on type \"URLType\".",
 7      "locations": [
 8        {
 9          "line": 8,
10          "column": 5
11        }
12      ]
13    }
14  ]
15}

这很重要,因为在GraphQL打字系统中,目标是发送和接收已经在方案中定义的信息。

目前的应用程序接受任何full_url字段中的任意字符串。问题是,如果有人发送了一个构建不良的URL,你会在尝试存储的信息时将用户重定向到无处。

首先,打开shortener/models.py文件:

1vim shortener/models.py

在导入部分中添加突出的行:

1[label shorty/shortener/models.py]
2from hashlib import md5
3
4from django.db import models
5from django.core.validators import URLValidator
6from django.core.exceptions import ValidationError
7
8from graphql import GraphQLError
9...

URLValidator是一个 Django 辅助程序来验证 URL 字符串,而GraphQLError则被 Graphene 用来用自定义消息提到例外。

接下来,请确保在将其保存到数据库之前验证用户接收的 URL。 通过在shortener/models.py文件中添加突出的代码来启用此操作:

 1[label shorty/shortener/models.py]
 2class URL(models.Model):
 3    full_url = models.URLField(unique=True)
 4    url_hash = models.URLField(unique=True)
 5    clicks = models.IntegerField(default=0)
 6    created_at = models.DateTimeField(auto_now_add=True)
 7
 8    def clicked(self):
 9        self.clicks += 1
10        self.save()
11
12    def save(self, *args, **kwargs):
13        if not self.id:
14            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]
15
16        validate = URLValidator()
17        try:
18            validate(self.full_url)
19        except ValidationError as e:
20            raise GraphQLError('invalid url')
21
22        return super().save(*args, **kwargs)

首先,此代码在)收到的 URL,并在错误 URL的自定义消息中提出GraphQLError`。

完整的 shortener/models.py 文件在这里显示:

 1[label shorty/shortener/models.py]
 2from hashlib import md5
 3
 4from django.db import models
 5from django.core.validators import URLValidator
 6from django.core.exceptions import ValidationError
 7
 8from graphql import GraphQLError
 9
10class URL(models.Model):
11    full_url = models.URLField(unique=True)
12    url_hash = models.URLField(unique=True)
13    clicks = models.IntegerField(default=0)
14    created_at = models.DateTimeField(auto_now_add=True)
15
16    def clicked(self):
17        self.clicks += 1
18        self.save()
19
20    def save(self, *args, **kwargs):
21        if not self.id:
22            self.url_hash = md5(self.full_url.encode()).hexdigest()[:10]
23
24        validate = URLValidator()
25        try:
26            validate(self.full_url)
27        except ValidationError as e:
28            raise GraphQLError('invalid url')
29
30        return super().save(*args, **kwargs)

如果您没有运行本地服务器,请使用python manage.py runserver命令启动它。

接下来,测试你的新错误处理在http://localhost:8000/graphql. 尝试在 GraphiQL 界面中创建一个新的 URL 与无效的 full_url`:

 1mutation {
 2  createUrl(fullUrl:"not_valid_url"){
 3    url {
 4      id
 5      fullUrl
 6      urlHash
 7      clicks
 8      createdAt
 9    }
10  }
11}

当发送无效的 URL 时,您的例外将被提到自定义消息:

 1[secondary_label Output]
 2
 3{
 4  "errors": [
 5    {
 6      "message": "invalid url",
 7      "locations": [
 8        {
 9          "line": 2,
10          "column": 3
11        }
12      ],
13      "path": [
14        "createUrl"
15      ]
16    }
17  ],
18  "data": {
19    "createUrl": null
20  }
21}

如果您在您的终端中查看python manage.py runserver命令正在运行,则会出现一个错误:

1[secondary_label Output]
2
3...
4graphql.error.located_error.GraphQLLocatedError: invalid url
5
6[30/Jan/2020 19:46:32] "POST /graphql/ HTTP/1.1" 200 121

GraphQL 终端总是会与 HTTP 200 状态代码失败,这通常意味着成功. 请记住,尽管 GraphQL 是基于 HTTP 的,但它并不像 REST 那样使用 HTTP 状态代码或 HTTP 方法的概念。

随着错误处理的实施,您现在可以设置一个机制来过滤您的查询,尽量减少服务器返回的信息。

第7步:使用过滤器

假设你已经开始使用URL缩写器添加自己的链接,过了一段时间,会有那么多的条目,以至于找到合适的条目将变得困难。

过滤是REST API中常见的概念,通常在URL中附有一个带有字段和值的 _Query Parameter。

在 GraphQL 中,您将使用 Query Arguments 作为过滤器,它们创建了一个漂亮而干净的界面。

您可以通过允许客户端使用full_url字段来按名称过滤 URL 来解决很难找到 URL问题,然后在您最喜欢的编辑器中打开shortener/schema.py文件。

1vim shortener/schema.py

首先,在突出的行中导入Q方法:

1[label shorty/shortener/schema.py]
2import graphene
3from graphene_django import DjangoObjectType
4from django.db.models import Q
5
6from .models import URL
7...

这将用于过滤您的数据库查询。

接下来,重写整个Query类,包含以下内容:

 1[label shorty/shortener/schema.py]
 2...
 3class Query(graphene.ObjectType):
 4    urls = graphene.List(URLType, url=graphene.String())
 5
 6    def resolve_urls(self, info, url=None, **kwargs):
 7        queryset = URL.objects.all()
 8
 9        if url:
10            _filter = Q(full_url__icontains=url)
11            queryset = queryset.filter(_filter)
12
13        return queryset
14...

你正在做的修改是:

  • 将)`方法。

完整的shortener/schema.py文件在这里显示:

 1[label shorty/shortener/schema.py]
 2import graphene
 3from graphene_django import DjangoObjectType
 4from django.db.models import Q
 5
 6from .models import URL
 7
 8class URLType(DjangoObjectType):
 9    class Meta:
10        model = URL
11
12class Query(graphene.ObjectType):
13    urls = graphene.List(URLType, url=graphene.String())
14
15    def resolve_urls(self, info, url=None, **kwargs):
16        queryset = URL.objects.all()
17
18        if url:
19            _filter = Q(full_url__icontains=url)
20            queryset = queryset.filter(_filter)
21
22        return queryset
23
24class CreateURL(graphene.Mutation):
25    url = graphene.Field(URLType)
26
27    class Arguments:
28        full_url = graphene.String()
29
30    def mutate(self, info, full_url)
31        url = URL(full_url=full_url)
32        url.save()
33
34        return CreateURL(url=url)
35
36class Mutation(graphene.ObjectType):
37    create_url = CreateURL.Field()

如果您没有运行本地服务器,请使用python manage.py runserver启动。

在 http://localhost:8000/graphql. 在 GraphiQL 接口中,写下列声明,它将用 community 来过滤所有 URL:

1query {
2  urls(url:"community") {
3    id
4    fullUrl
5    urlHash
6    clicks
7    createdAt
8  }
9}

输出只是一个输入,因为你刚刚添加了一个URL,其中包含社区字符串. 如果你以前添加了更多的URL,你的输出可能会有所不同。

 1[secondary_label Output]
 2
 3{
 4  "data": {
 5    "urls": [
 6      {
 7        "id": "1",
 8        "fullUrl": "https://www.digitalocean.com/community",
 9        "urlHash": "077880af78",
10        "clicks": 1,
11        "createdAt": "2020-01-30T19:27:36.243900+00:00"
12      }
13    ]
14  }
15}

但是,如果链接太多,您的客户可能会抱怨URL列表返回的数据比他们的应用程序能够处理的更多。

步骤8 - 实施页面化

使用您的后端的客户可能会抱怨响应时间过长,或者如果有太多 URL 条目,其大小太大,即使您的数据库也可能难以组合大量信息,以解决此问题,您可以允许客户端使用称为 pagination 的技术在每个请求中指定想要的项目数量。

即使在 REST API 中,您可能会在 HTTP 标题或查询参数中看到它,具有不同的名称和行为。

在此应用程序中,您将通过向查询 URL 启用两个额外的参数来实现页面化:‘first’和‘skip’。‘first’将选择第一个可变元素数目,‘skip’将指定从开始跳过的元素数目。

实施此解决方案类似于添加过滤器。

打开shortener/schema.py文件:

1vim shortener/schema.py

在文件中,通过将两个新的参数添加到urls变量和resolve_urls方法中,更改Query类别,如下代码所示:

 1[label shorty/shortener/schema.py]
 2import graphene
 3from graphene_django import DjangoObjectType
 4from django.db.models import Q
 5
 6from .models import URL
 7
 8class Query(graphene.ObjectType):
 9    urls = graphene.List(URLType, url=graphene.String(), first=graphene.Int(), skip=graphene.Int())
10
11    def resolve_urls(self, info, url=None, first=None, skip=None, **kwargs):
12        queryset = URL.objects.all()
13
14        if url:
15            _filter = Q(full_url__icontains=url)
16            queryset = queryset.filter(_filter)
17
18        if first:
19            queryset = queryset[:first]
20
21        if skip:
22            queryset = queryset[skip:]
23
24        return queryset
25...

此代码使用resolve_urls方法中新创建的firstskip参数来过滤数据库查询。

如果您没有运行本地服务器,请使用python manage.py runserver启动。

要测试页面化,请在 http://localhost:8000/graphql 的 GraphiQL 界面中发出以下查询:

1query {
2  urls(first: 2, skip: 1) {
3    id
4    fullUrl
5    urlHash
6    clicks
7    createdAt
8  }
9}

您的 URL 缩短器将返回您数据库中创建的第二个 URL:

 1[secondary_label Output]
 2
 3{
 4  "data": {
 5    "urls": [
 6      {
 7        "id": "2",
 8        "fullUrl": "https://www.digitalocean.com/write-for-donations/",
 9        "urlHash": "703562669b",
10        "clicks": 0,
11        "createdAt": "2020-01-30T19:31:10.820062+00:00"
12      }
13    ]
14  }
15}

这表明页面功能是有效的. 通过添加更多的 URL 和测试不同的第一跳跃功能,您可以自由玩。

结论

整个GraphQL生态系统每天都在增长,背后有一个活跃的社区,它已经被GitHub和Facebook等公司证明了,并且现在您可以将该技术应用到自己的项目中。

在本教程中,您使用 GraphQL、Python 和 Django 创建了一个 URL 缩短服务,使用了查询和突变等概念。

您可以探索更多关于 GraphQL 和这里使用的工具在 GraphQL 网站Graphene 文档 网站. 此外,DigitalOcean 有额外的教程为 PythonDjango,如果你想了解更多有关。

Published At
Categories with 技术
comments powered by Disqus