Django Python Web 应用程序框架简介

在本系列比较 Python 框架的最后一篇文章中,了解 Django。
324 位读者喜欢这篇文章。
Getting to know the Django ORM

Christian Holmér。由 Opensource.com 修改。CC BY-SA 4.0

在这个由四部分组成的系列文章的前三篇中,我们比较了不同的 Python Web 框架,涵盖了 PyramidFlaskTornado Web 框架。我们已经构建了三次相同的应用程序,最终来到了 Django。Django 是目前 Python 开发人员主要使用的 Web 框架,原因不难理解。它擅长隐藏大量的配置逻辑,让您能够专注于快速构建大型项目。

话虽如此,当涉及到小型项目时,例如我们的待办事项应用,Django 可能有点像用消防水管打水枪战。让我们看看它是如何组合在一起的。

关于 Django

Django 将自己定位为“一个高级 Python Web 框架,鼓励快速开发和简洁、务实的设计。由经验丰富的开发人员构建,它处理了 Web 开发中的许多麻烦,因此您可以专注于编写您的应用程序,而无需重新发明轮子。” 他们真的是认真的!这个庞大的 Web 框架附带了如此多的内置功能,以至于在开发过程中,有时会感到困惑,不知道所有东西是如何协同工作的。

除了框架本身非常庞大之外,Django 社区也绝对庞大。事实上,它非常庞大且活跃,以至于有一个 专门的网站 致力于人们设计的第三方软件包,用于插入 Django 以完成各种各样的事情。这包括从身份验证和授权,到完整的 Django 驱动的内容管理系统,到电子商务附加组件,再到与 Stripe 的集成。谈到不重新发明轮子;很可能如果您想用 Django 完成某件事,已经有人做过了,您只需将其拉入您的项目即可。

为此,我们想使用 Django 构建一个 REST API,因此我们将利用始终流行的 Django REST framework。它的工作是将 Django 框架(原本是为了服务于使用 Django 自己的模板引擎构建的完全渲染的 HTML 页面)转变为一个专门用于有效处理 REST 交互的系统。让我们开始吧。

Django 启动和配置

$ mkdir django_todo
$ cd django_todo
$ pipenv install --python 3.6
$ pipenv shell
(django-someHash) $ pipenv install django djangorestframework

作为参考,我们正在使用 django-2.0.7djangorestframework-3.8.2

与 Flask、Tornado 和 Pyramid 不同,我们不需要编写自己的 setup.py 文件。我们不是在制作可安装的 Python 发行版。 与许多事情一样,Django 以其 Django 特有的方式为我们处理了这个问题。我们仍然需要一个 requirements.txt 文件来跟踪我们在其他地方部署所需的所有安装。但是,就针对我们的 Django 项目中的模块而言,Django 将允许我们列出我们想要访问的子目录,然后允许我们从这些目录导入,就像它们是已安装的软件包一样。

首先,我们必须创建一个 Django 项目。

当我们安装 Django 时,我们也安装了命令行脚本 django-admin。它的工作是管理所有各种 Django 相关的命令,这些命令帮助我们组装项目并在我们继续开发时维护它。django-admin 不会让我们从头开始构建整个 Django 生态系统,而是让我们开始使用标准 Django 项目所需的所有绝对必要的文件(以及更多)。

调用 django-admin 的 start-project 命令的语法是 django-admin startproject <项目名称> <我们想要文件的目录>。我们希望文件存在于我们当前的工作目录中,所以

(django-someHash) $ django-admin startproject django_todo .

输入 ls 将显示一个新文件和一个新目录。

(django-someHash) $ ls
manage.py   django_todo

manage.py 是一个命令行可执行的 Python 文件,它最终只是 django-admin 的一个包装器。因此,它的工作是相同的:帮助我们管理我们的项目。因此得名 manage.py

它创建的目录,django_tododjango_todo 内部,代表我们项目的配置根目录。现在让我们深入了解一下。

配置 Django

我们将 django_todo 目录称为“配置根目录”,我们的意思是这个目录保存了通常用于配置我们的 Django 项目的文件。几乎所有在这个目录之外的东西都将只专注于与项目的模型、视图、路由等相关的“业务逻辑”。所有将项目连接在一起的点都将指向这里。

django_todo 中调用 ls 会显示四个文件

(django-someHash) $ cd django_todo
(django-someHash) $ ls
__init__.py settings.py urls.py     wsgi.py
  • __init__.py 是空的,仅用于将此目录变成可导入的 Python 包。
  • settings.py 是大多数配置项将被设置的地方,例如项目是否处于 DEBUG 模式、正在使用的数据库、Django 应该在哪里查找文件等等。它是配置根目录的“主要配置”部分,我们稍后会深入探讨。
  • urls.py,顾名思义,是设置 URL 的地方。虽然我们不必在此文件中显式编写项目的每个 URL,但我们确实需要让此文件知道任何其他声明了 URL 的地方。如果此文件没有指向其他 URL,则这些 URL 不存在。就是这样。
  • wsgi.py 用于在生产环境中提供应用程序服务。就像 Pyramid、Tornado 和 Flask 公开了一些“app”对象,这些对象是要服务的已配置应用程序一样,Django 也必须公开一个。这就是在这里完成的。然后可以使用 GunicornWaitressuWSGI 之类的工具来提供服务。

设置设置

查看 settings.py 内部将显示其相当大的尺寸——而这些只是默认设置!这甚至不包括数据库、静态文件、媒体文件、任何云集成或 Django 项目可以配置的其他数十种方式的钩子。让我们从上到下看看我们得到了什么

  • BASE_DIR 设置基本目录的绝对路径,或 manage.py 所在的目录。这对于定位文件很有用。
  • SECRET_KEY 是用于 Django 项目中加密签名的密钥。在实践中,它用于会话、cookies、CSRF 保护和身份验证令牌等。尽快,最好在第一次提交之前,应该更改 SECRET_KEY 的值并将其移动到环境变量中。
  • DEBUG 告诉 Django 是在开发模式还是生产模式下运行项目。这是一个极其关键的区别。
    • 在开发模式下,当出现错误时,Django 将显示导致错误的完整堆栈跟踪,以及运行项目所涉及的所有设置和配置。如果在生产环境中将 DEBUG 设置为 True,则这可能是一个巨大的安全问题。
    • 在生产环境中,当出现问题时,Django 会显示一个普通的错误页面。除了错误代码之外,没有提供任何信息。
    • 保护我们项目的一种简单方法是将 DEBUG 设置为环境变量,例如 bool(os.environ.get('DEBUG', ''))
  • ALLOWED_HOSTS 是应用程序正在服务的主机名的字面列表。在开发中,这可以是空的,但在生产环境中,如果服务项目的主机不在 ALLOWED_HOSTS 列表中,我们的 Django 项目将无法运行。这是环境变量的另一个内容。
  • INSTALLED_APPS 是 Django “应用”(可以将它们视为子目录;稍后会详细介绍)的列表,我们的 Django 项目可以访问这些应用。默认情况下,我们得到了一些来提供……
    • 内置的 Django 管理网站
    • Django 的内置身份验证系统
    • Django 的用于数据模型的一体化管理器
    • 会话管理
    • 基于 Cookie 和会话的消息传递
    • 站点固有的静态文件的使用,例如 css 文件、js 文件、我们站点设计中包含的任何图像等。
  • MIDDLEWARE 顾名思义:帮助我们的 Django 项目运行的中间件。其中大部分用于处理各种类型的安全问题,尽管我们可以根据需要添加其他中间件。
  • ROOT_URLCONF 设置了我们基本级别的 URL 配置文件的导入路径。我们之前看到的 urls.py?默认情况下,Django 指向该文件以收集我们所有的 URL。如果我们希望 Django 在其他地方查找,我们将在此处设置该位置的导入路径。
  • TEMPLATES 是 Django 将用于我们站点前端的模板引擎列表(如果我们依赖 Django 来构建我们的 HTML)。由于我们不是,所以它无关紧要。
  • WSGI_APPLICATION 设置了我们的 WSGI 应用的导入路径——在生产环境中提供服务的东西。默认情况下,它指向 wsgi.py 中的 application 对象。这很少(如果有的话)需要修改。
  • DATABASES 设置我们的 Django 项目将访问哪些数据库。必须 设置 default 数据库。我们可以按名称设置其他数据库,只要我们提供 HOSTUSERPASSWORDPORT、数据库 NAME 和适当的 ENGINE。正如人们可能想象的那样,这些都是敏感信息,因此最好将它们隐藏在环境变量中。查看 Django 文档 了解更多详细信息。
    • 注意:如果与其提供数据库位置的各个部分,您更愿意提供完整的数据库 URL,请查看 dj_database_url
  • AUTH_PASSWORD_VALIDATORS 实际上是一个函数列表,这些函数运行以检查输入密码。我们默认获得了一些,但如果我们有其他更复杂的验证需求——不仅仅是检查密码是否与用户的属性匹配、是否超过最小长度、是否是 1,000 个最常见密码之一,或者密码是否完全是数字——我们可以在此处列出它们。
  • LANGUAGE_CODE 将设置站点的语言。默认情况下是美国英语,但我们可以将其切换为其他语言。
  • TIME_ZONE 是我们 Django 项目中任何自动生成的时间戳的时区。我再怎么强调坚持使用 UTC 的重要性也不为过,并在其他地方执行任何特定于时区的处理,而不是尝试重新配置此设置。正如 这篇文章 所述,UTC 是所有时区中的公分母,因为无需担心偏移量。如果偏移量非常重要,我们可以根据需要使用与 UTC 的适当偏移量来计算它们。
  • USE_I18N 将允许 Django 使用其自身的翻译服务来翻译前端的字符串。I18N = 国际化(“i” 和 “n” 之间有 18 个字符)
  • 如果 USE_L10N (L10N = 本地化 [“l” 和 “n” 之间有 10 个字符]) 设置为 True,则将使用数据的通用本地格式。日期就是一个很好的例子:在美国是 MM-DD-YYYY。在欧洲,日期倾向于写成 DD-MM-YYYY
  • STATIC_URL 是用于提供静态文件的更大范围设置的一部分。我们将构建 REST API,因此我们无需担心静态文件。总的来说,这为每个静态文件设置域名后的根路径。因此,如果我们有一个徽标图像要提供服务,它将是 http://<域名>/<STATIC_URL>/logo.gif

这些设置默认情况下几乎已准备就绪。我们需要更改的一件事是 DATABASES 设置。首先,我们使用以下命令创建我们将要使用的数据库

(django-someHash) $ createdb django_todo

我们想使用像我们在 Flask、Pyramid 和 Tornado 中所做的那样的 PostgreSQL 数据库。这意味着我们将不得不更改 DATABASES 设置以允许我们的服务器访问 PostgreSQL 数据库。首先:引擎。默认情况下,数据库引擎是 django.db.backends.sqlite3。我们将将其更改为 django.db.backends.postgresql

有关 Django 可用引擎的更多信息,请查看文档。请注意,虽然从技术上讲可以将 NoSQL 解决方案整合到 Django 项目中,但 Django 开箱即用,强烈偏向于 SQL 解决方案。

接下来,我们必须为连接参数的不同部分指定键值对。

  • NAME 是我们刚刚创建的数据库的名称。
  • USER 是个人的 Postgres 数据库用户名
  • PASSWORD 是访问数据库所需的密码
  • HOST 是数据库的主机。localhost127.0.0.1 将起作用,因为我们正在本地开发。
  • PORT 是我们为 Postgres 打开的任何 PORT;通常是 5432

settings.py 期望我们为每个键提供字符串值。但是,这是高度敏感的信息。这对于任何负责任的开发人员来说都是行不通的。有几种方法可以解决这个问题,但我们只设置环境变量。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME', ''),
        'USER': os.environ.get('DB_USER', ''),
        'PASSWORD': os.environ.get('DB_PASS', ''),
        'HOST': os.environ.get('DB_HOST', ''),
        'PORT': os.environ.get('DB_PORT', ''),
    }
}

在继续之前,请确保设置环境变量,否则 Django 将无法工作。此外,我们需要将 psycopg2 安装到此环境中,以便我们可以与数据库对话。

Django 路由和视图

让我们在这个项目中实现一些功能。我们将使用 Django REST Framework 来构建我们的 REST API,因此我们必须通过将 rest_framework 添加到 settings.py 中的 INSTALLED_APPS 末尾来确保我们可以使用它。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework'
]

虽然 Django REST Framework 并非完全要求基于类的视图(如 Tornado)来处理传入的请求,但它是编写视图的首选方法。让我们定义一个。

让我们在 django_todo 中创建一个名为 views.py 的文件。在 views.py 中,我们将创建我们的“Hello, world!”视图。

# in django_todo/views.py
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

class HelloWorld(APIView):
    def get(self, request, format=None):
        """Print 'Hello, world!' as the response body."""
        return JsonResponse("Hello, world!")

每个 Django REST Framework 基于类的视图都直接或间接地继承自 APIViewAPIView 处理大量内容,但对于我们的目的,它执行以下特定操作

  • 设置基于 HTTP 方法(例如 GET、POST、PUT、DELETE)定向流量所需的方法
  • 使用我们将需要的所有数据和属性填充 request 对象,以便解析和处理任何传入的请求
  • 获取每个调度方法(即,名为 getpostputdelete 的方法)返回的 ResponseJsonResponse,并构造格式正确的 HTTP 响应。

太棒了,我们有一个视图!它本身什么也不做。我们需要将其连接到路由。

如果我们跳到 django_todo/urls.py,我们将到达我们的默认 URL 配置文件。如前所述:如果我们的 Django 项目中的路由未包含在此处,则它不存在

我们通过将所需的 URL 添加到给定的 urlpatterns 列表来添加它们。默认情况下,我们为 Django 的内置站点管理后端获得了一整套 URL。我们将完全删除它。

我们还获得了一些非常有用的文档字符串,这些文档字符串准确地告诉我们如何向我们的 Django 项目添加路由。我们将需要调用 path() 并带有三个参数

  • 所需的路由,作为字符串(不带前导斜杠)
  • 将处理该路由的视图函数(始终只是一个函数!)
  • 我们的 Django 项目中路由的名称

让我们导入我们的 HelloWorld 视图并将其附加到主页路由 "/"。我们也可以从 urlpatterns 中删除 admin 的路径,因为我们不会使用它。

# django_todo/urls.py, after the big doc string
from django.urls import path
from django_todo.views import HelloWorld

urlpatterns = [
    path('', HelloWorld.as_view(), name="hello"),
]

嗯,这有点不同。我们指定的路由只是一个空字符串。为什么会这样?Django 假设我们声明的每个路径都以一个前导斜杠开头。我们只是在指定初始域名之后到资源的路由。如果路由不是指向特定资源,而只是主页,则路由只是 "",或者实际上是“没有资源”。

HelloWorld 视图是从我们刚刚创建的 views.py 文件导入的。为了执行此导入,我们需要更新 settings.py 以在 INSTALLED_APPS 列表中包含 django_todo。是的,这有点奇怪。这是思考它的一种方式。

INSTALLED_APPS 指的是 Django 视为可导入的目录或软件包的列表。这是 Django 将项目的各个组件视为已安装软件包而无需通过 setup.py 的方式。我们希望将 django_todo 目录视为可导入的软件包,因此我们将该目录包含在 INSTALLED_APPS 中。现在,该目录中的任何模块也是可导入的。所以我们得到了我们的视图。

path 函数将仅将视图函数作为第二个参数,而不仅仅是它自己的基于类的视图。幸运的是,所有有效的 Django 基于类的视图都包含此 .as_view() 方法。它的工作是将基于类的视图的所有优点汇总到一个视图函数中,并返回该视图函数。因此,我们永远不必担心进行这种转换。相反,我们只需要考虑业务逻辑,让 Django 和 Django REST Framework 处理其余的事情。

让我们在浏览器中打开它!

Django 自带本地开发服务器,可以通过 manage.py 访问。让我们导航到包含 manage.py 的目录并输入

(django-someHash) $ ./manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
August 01, 2018 - 16:47:24
Django version 2.0.7, using settings 'django_todo.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

当执行 runserver 时,Django 会进行检查以确保项目(或多或少)正确连接在一起。它不是万无一失的,但它确实捕获了一些明显的错误。它还会通知我们数据库是否与我们的代码不同步。毫无疑问,我们的数据库是不同步的,因为我们尚未将任何应用程序的内容提交到我们的数据库,但这目前没问题。让我们访问 http://127.0.0.1:8000 以查看 HelloWorld 视图的输出。

嗯。这不是我们在 Pyramid、Flask 和 Tornado 中看到的纯文本数据。当使用 Django REST Framework 时,HTTP 响应(在浏览器中查看时)是这种渲染的 HTML,以红色显示我们实际的 JSON 响应。

但不要担心!如果我们快速 curl 查看命令行中的 http://127.0.0.1:8000,我们不会得到任何花哨的 HTML。只有内容。

# Note: try this in a different terminal window, outside of the virtual environment above
$ curl http://127.0.0.1:8000
"Hello, world!"

太棒了!

Django REST Framework 希望我们在使用浏览器时拥有人性化的界面。这是有道理的;如果在浏览器中查看 JSON,通常是因为人们想检查它看起来是否正确,或者在设计 API 的某些使用者时了解 JSON 响应的外观。这很像您从 Postman 之类的服务获得的服务。

无论如何,我们知道我们的视图正在工作!太棒了!让我们回顾一下我们所做的事情

  1. 使用 django-admin startproject <项目名称> 启动项目
  2. 更新 django_todo/settings.py 以对 DEBUGSECRET_KEYDATABASES 字典中的值使用环境变量
  3. 安装 Django REST Framework 并将其添加到 INSTALLED_APPS 列表中
  4. 创建 django_todo/views.py 以包含我们的第一个视图类,向世界问好
  5. 使用指向我们新的主页路由的路径更新 django_todo/urls.py
  6. 更新 django_todo/settings.py 中的 INSTALLED_APPS 以包含 django_todo

创建模型

现在让我们创建我们的数据模型。

Django 项目的整个基础设施都围绕数据模型构建。 它的编写方式使每个数据模型都可以拥有自己的小宇宙,其中包含自己的视图、自己的与资源相关的一组 URL,甚至还有自己的测试(如果我们愿意的话)。

如果我们想构建一个简单的 Django 项目,我们可以通过在 django_todo 目录中编写我们自己的 models.py 文件并将其导入到我们的视图中来绕过这一点。但是,我们正在尝试以“正确”的方式编写 Django 项目,因此我们应该尽可能将我们的模型划分为它们自己的小包,即 Django Way™。

Django Way 涉及创建所谓的 Django “应用”。Django “应用” 本身不是单独的应用程序;它们没有自己的设置等等(尽管它们可以有)。但是,它们可以拥有几乎人们可能想到的独立应用程序中的所有其他内容

  • 一组独立的 URL
  • 一组独立的 HTML 模板(如果我们想提供 HTML)
  • 一个或多个数据模型
  • 一组独立的视图
  • 一组独立的测试

它们被制造成独立的,因此可以像独立应用程序一样轻松共享。事实上,Django REST Framework 就是 Django 应用的一个例子。它附带了自己的视图和 HTML 模板,用于提供我们的 JSON。我们只是利用该 Django 应用将我们的项目变成一个完整的 RESTful API,从而减少麻烦。

要为我们的待办事项创建 Django 应用,我们将需要将 startapp 命令与 manage.py 一起使用。

(django-someHash) $ ./manage.py startapp todo

startapp 命令将静默成功。我们可以使用 ls 检查它是否完成了它应该做的事情。

(django-someHash) $ ls
Pipfile      Pipfile.lock django_todo  manage.py    todo

看看那个:我们有了一个全新的 todo 目录。让我们看看里面!

(django-someHash) $ ls todo
__init__.py admin.py    apps.py     migrations  models.py   tests.py    views.py

以下是 manage.py startapp 创建的文件

  • __init__.py 是空的;它的存在是为了使此目录可以被视为模型、视图等的有效导入路径。
  • admin.py 并非完全为空;它用于在 Django 管理员中格式化此应用的模型,我们不会在本文中深入探讨。
  • apps.py … 这里也没什么工作要做;它有助于格式化 Django 管理员的模型。
  • migrations 是一个目录,它将包含我们数据模型的快照;它用于更新我们的数据库。这是少数几个带有内置数据库管理的框架之一,其中一部分是允许我们更新数据库,而不必拆除并重建它来更改模式。
  • models.py 是数据模型所在的位置。
  • tests.py 是测试的存放位置——如果我们编写了任何测试。
  • views.py 用于我们编写的与此应用中的模型相关的视图。它们不必写在这里。例如,我们可以将所有视图都写在 django_todo/views.py 中。但是,它在这里,因此更容易分离我们的关注点。这对于涵盖许多概念空间的庞大应用程序来说变得更加相关。

为我们创建的不是此应用的 urls.py 文件。我们可以自己制作一个。

(django-someHash) $ touch todo/urls.py

在继续之前,我们应该帮自己一个忙,并将这个新的 Django 应用添加到 django_todo/settings.py 中的 INSTALLED_APPS 列表中。

# in settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'django_todo',
    'todo' # <--- the line was added
]

检查 todo/models.py 表明 manage.py 已经为我们编写了一些代码以开始使用。与 Flask、Tornado 和 Pyramid 实现中创建模型的方式不同,Django 没有利用第三方来管理数据库会话或构建其对象实例。这一切都融入到 Django 的 django.db.models 子模块中。

但是,构建模型的方式或多或少是相同的。要在 Django 中创建模型,我们需要构建一个继承自 models.Modelclass。将应用于该模型实例的所有字段都应显示为类属性。与过去一样,我们不再从 SQLAlchemy 导入列和字段类型,我们所有的字段都将直接来自 django.db.models

# todo/models.py
from django.db import models

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)

虽然 Django 需要的东西与基于 SQLAlchemy 的系统需要的东西之间存在一些明显的差异,但总体内容和结构或多或少是相同的。让我们指出差异。

我们不再需要为对象实例的自动递增 ID 号声明单独的字段。Django 为我们构建了一个,除非我们将不同的字段指定为主键。

我们不再实例化作为数据类型对象传递的 Column 对象,而是直接将数据类型引用为列本身。

Unicode 字段变为 models.CharFieldmodels.TextFieldCharField 用于特定最大长度的小文本字段,而 TextField 用于任何数量的文本。

TextField 应该能够为空,我们以两种方式指定这一点。blank=True 表示在构造此模型的实例时,并且正在验证附加到此字段的数据时,该数据可以为空。这与 null=True 不同,后者表示在构建此模型类的表时,与 note 对应的列将允许空白或 NULL 条目。因此,总结一下,blank=True 控制数据如何添加到模型实例,而 null=True 控制首先如何构建保存该数据的数据库表。

DateTime 字段增长了一些肌肉,并且能够为我们做一些工作,而不是我们必须修改类的 __init__ 方法。对于 creation_date 字段,我们指定 auto_now_add=True。这在实践意义上意味着当创建一个新的模型实例时,Django 将自动记录现在的日期和时间作为该字段的值。这很方便!

auto_now_add 及其近亲 auto_now 都没有设置为 True 时,DateTimeField 将像任何其他字段一样期望数据。它需要被馈送一个正确的 datetime 对象才能有效。due_date 列的 blanknull 都设置为 True,这样待办事项列表上的项目就可以只是将来某个时候要完成的项目,而没有定义的日期或时间。

BooleanField 最终只是一个可以接受两个值之一的字段:TrueFalse。在这里,默认值设置为 False

管理数据库

如前所述,Django 有其自身的数据库管理方式。我们不必编写…… 实际上任何关于我们数据库的代码,而是利用 Django 在构建时提供的 manage.py 脚本。它不仅管理我们数据库表的构建,还管理我们希望对这些表进行的任何更新,而无需必须完全删除所有内容!

因为我们构建了一个新的模型,所以我们需要使我们的数据库意识到它。首先,我们需要将与此模型对应的模式放入代码中。manage.pymakemigrations 命令将获取我们构建的模型类及其所有字段的快照。它将获取该信息并将其打包到一个 Python 脚本中,该脚本将位于此特定 Django 应用的 migrations 目录中。永远不会有理由直接运行此迁移脚本。 它的存在仅仅是为了 Django 可以使用它作为更新我们的数据库表或在更新我们的模型类时继承信息的基础。

(django-someHash) $ ./manage.py makemigrations
Migrations for 'todo':
  todo/migrations/0001_initial.py
    - Create model Task

这将查看 INSTALLED_APPS 中列出的每个应用,并检查这些应用中是否存在模型。然后,它将检查相应的 migrations 目录以查找迁移文件,并将它们与每个 INSTALLED_APPS 应用中的模型进行比较。如果模型已升级到超出最新迁移所说的应存在的范围,则将创建一个新的迁移文件,该文件继承自最新的迁移文件。它将被自动命名,并且还会获得一条消息,说明自上次迁移以来发生了什么变化。

如果距离您上次处理 Django 项目已经有一段时间了,并且不记得您的模型是否与您的迁移同步,则您无需担心。makemigrations 是一个幂等操作;无论您运行 makemigrations 一次还是 20 次,您的 migrations 目录都将只有一个当前模型配置的副本。甚至比这更好的是,当我们运行 ./manage.py runserver 时,Django 将检测到我们的模型与我们的迁移不同步,并且它会以彩色文本直接告诉我们,以便我们可以做出适当的选择。

下一个要点是至少会让每个人绊倒一次的事情:创建迁移文件不会立即影响我们的数据库。当我们运行 makemigrations 时,我们准备了我们的 Django 项目来定义应该如何创建给定的表并最终看起来如何。仍然由我们来将这些更改应用到我们的数据库。这就是 migrate 命令的用途。

(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, todo
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying sessions.0001_initial... OK
  Applying todo.0001_initial... OK

当我们应用我们的迁移时,Django 首先检查其他 INSTALLED_APPS 是否有要应用的迁移。它大致按照它们列出的顺序检查它们。我们希望我们的应用列在最后,因为我们希望确保,如果我们的模型依赖于任何 Django 的内置模型,我们所做的数据库更新不会受到依赖性问题的影响。

我们还有另一个模型要构建:User 模型。但是,自从我们使用 Django 以来,游戏规则发生了一些变化。许多应用程序都需要某种 User 模型,以至于 Django 的 django.contrib.auth 包为我们构建了自己的模型以供使用。如果不是因为我们用户需要的身份验证令牌,我们可以直接继续使用它,而不是重新发明轮子。

但是,我们需要该令牌。我们可以通过几种方式处理这个问题。

  • 从 Django 的 User 对象继承,创建我们自己的对象,通过添加 token 字段来扩展它
  • 创建一个与 Django 的 User 对象存在一对一关系的新对象,其唯一目的是保存令牌

我有构建对象关系的习惯,所以让我们选择第二种方案。让我们称之为 Owner,因为它基本上具有与 User 相似的含义,这正是我们想要的。

出于纯粹的惰性,我们可以直接将这个新的 Owner 对象包含在 todo/models.py 中,但我们还是克制一下Owner 并非必须与任务列表项的创建或维护显式相关。从概念上讲,Owner 仅仅是任务的所有者。甚至将来我们可能希望扩展这个 Owner,以包含与其他任务完全无关的数据。

为了安全起见,让我们创建一个 owner 应用,其职责是容纳和处理这个 Owner 对象。

(django-someHash) $ ./manage.py startapp owner

不要忘记将其添加到 settings.py 中的 INSTALLED_APPS 列表中。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'django_todo',
    'todo',
    'owner'
]

如果我们查看 Django 项目的根目录,我们现在有两个 Django 应用

(django-someHash) $ ls
Pipfile      Pipfile.lock django_todo  manage.py    owner        todo

owner/models.py 中,让我们构建这个 Owner 模型。如前所述,它将与 Django 内置的 User 对象具有一对一的关系。我们可以使用 Django 的 models.OneToOneField 来强制执行这种关系

# owner/models.py
from django.db import models
from django.contrib.auth.models import User
import secrets

class Owner(models.Model):
    """The object that owns tasks."""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=256)

    def __init__(self, *args, **kwargs):
        """On construction, set token."""
        self.token = secrets.token_urlsafe(64)
        super().__init__(*args, **kwargs)

这表示 Owner 对象链接到 User 对象,每个 user 实例对应一个 owner 实例。on_delete=models.CASCADE 指示如果相应的 User 被删除,则与其链接的 Owner 实例也将被删除。让我们运行 makemigrationsmigrate,将这个新模型烘焙到我们的数据库中。

(django-someHash) $ ./manage.py makemigrations
Migrations for 'owner':
  owner/migrations/0001_initial.py
    - Create model Owner
(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, owner, sessions, todo
Running migrations:
  Applying owner.0001_initial... OK

现在我们的 Owner 需要拥有一些 Task 对象。它将与上面看到的 OneToOneField 非常相似,只不过我们将在 Task 对象上贴上一个指向 OwnerForeignKey 字段。

# todo/models.py
from django.db import models
from owner.models import Owner

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE)

每个待办事项列表任务都只有一个所有者,该所有者可以拥有多个任务。当该所有者被删除时,他们拥有的任何任务都将随之消失。

现在让我们运行 makemigrations 以获取数据模型设置的新快照,然后运行 migrate 将这些更改应用到我们的数据库。

(django-someHash) django $ ./manage.py makemigrations
You are trying to add a non-nullable field 'owner' to task without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

哦不!我们遇到了问题!发生了什么?嗯,当我们创建 Owner 对象并将其作为 ForeignKey 添加到 Task 时,我们基本上要求每个 Task 都需要一个 Owner。但是,我们为 Task 对象创建的第一个迁移并未包含该要求。因此,即使我们的数据库表中没有数据,Django 也在对我们的迁移进行预检查,以确保它们兼容,而我们提议的这个新迁移不兼容

有几种方法可以处理这类问题

  1. 删除当前的迁移,并构建一个新的迁移,其中包含当前的模型配置
  2. Task 对象上的 owner 字段添加一个默认值
  3. 允许任务的 owner 字段具有 NULL 值。

选项 2 在这里没有多大意义;我们相当于提议,任何创建的 Task 都将默认链接到某个默认所有者,尽管不一定存在这样的所有者。

选项 1 将要求我们销毁并重建我们的迁移。我们应该保持这些迁移不变

让我们选择选项 3。在这种情况下,如果我们允许 Task 表的 owners 字段具有空值,也不会是世界末日;从现在开始创建的任何任务都必然会有一个所有者。如果您的情况不允许数据库表使用这种模式,请删除您的迁移,删除表,然后重建迁移。

# todo/models.py
from django.db import models
from owner.models import Owner

class Task(models.Model):
    """Tasks for the To Do list."""
    name = models.CharField(max_length=256)
    note = models.TextField(blank=True, null=True)
    creation_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField(blank=True, null=True)
    completed = models.BooleanField(default=False)
    owner = models.ForeignKey(Owner, on_delete=models.CASCADE, null=True)
(django-someHash) $ ./manage.py makemigrations
Migrations for 'todo':
  todo/migrations/0002_task_owner.py
    - Add field owner to task
(django-someHash) $ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, owner, sessions, todo
Running migrations:
  Applying todo.0002_task_owner... OK

哇!我们有了模型!欢迎来到 Django 的对象声明方式。

为了以防万一,让我们确保每当创建一个 User 时,它都会自动链接到一个新的 Owner 对象。我们可以使用 Django 的 signals 系统来做到这一点。基本上,我们明确地说明我们的意图:“当我们收到信号,表明已构建一个新的 User 时,构建一个新的 Owner,并将这个新的 User 设置为该 Owneruser 字段。”在实践中,它看起来像

# owner/models.py
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver

import secrets


class Owner(models.Model):
    """The object that owns tasks."""
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    token = models.CharField(max_length=256)

    def __init__(self, *args, **kwargs):
        """On construction, set token."""
        self.token = secrets.token_urlsafe(64)
        super().__init__(*args, **kwargs)


@receiver(post_save, sender=User)
def link_user_to_owner(sender, **kwargs):
    """If a new User is saved, create a corresponding Owner."""
    if kwargs['created']:
        owner = Owner(user=kwargs['instance'])
        owner.save()

我们设置一个函数来监听从 Django 内置的 User 对象发送的信号。它正在等待 User 对象保存之后。这可能来自新的 User 或对现有 User 的更新;我们在监听函数中区分这两种情况。

如果发送信号的是新创建的实例,则 kwargs['created'] 的值为 True。我们只希望在 True 的情况下执行某些操作。如果是新实例,我们创建一个新的 Owner,将其 user 字段设置为新创建的 User 实例。之后,我们 save() 新的 Owner。如果一切顺利,这将把我们的更改提交到数据库。如果数据未通过我们声明的字段的验证,则会失败。

现在让我们讨论一下我们将如何访问数据。

访问模型数据

在 Flask、Pyramid 和 Tornado 框架中,我们通过针对某些数据库会话运行查询来访问模型数据。也许它附加request 对象,也许它是一个独立的 session 对象。无论如何,我们都必须建立与数据库的实时连接,并在该连接上进行查询。

这不是 Django 的工作方式。默认情况下,Django 不利用任何第三方对象关系映射 (ORM) 与数据库对话。相反,Django 允许模型类维护它们自己与数据库的对话。

每个从 django.db.models.Model 继承的模型类都将附加一个 objects 对象。这将取代我们已经非常熟悉的 sessiondbsession。让我们打开 Django 给我们提供的特殊 shell,并研究这个 objects 对象是如何工作的。

(django-someHash) $ ./manage.py shell
Python 3.7.0 (default, Jun 29 2018, 20:13:13) 
[Clang 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>

Django shell 与普通的 Python shell 不同,因为它感知到我们一直在构建的 Django 项目,并且可以轻松导入我们的模型、视图、设置等,而无需担心安装包。我们可以使用简单的 import 来访问我们的模型。

>>> from owner.models import Owner
>>> Owner
<class 'owner.models.Owner'>

目前,我们没有 Owner 实例。我们可以通过使用 Owner.objects.all() 查询它们来判断

>>> Owner.objects.all()
<QuerySet []>

任何时候我们在 <Model>.objects 对象上运行查询方法时,我们都会得到一个 QuerySet 返回值。对于我们的目的而言,它实际上是一个 list,而这个 list 正在告诉我们它是空的。让我们通过创建一个 User 来创建一个 Owner

>>> from django.contrib.auth.models import User
>>> new_user = User(username='kenyattamurphy', email='kenyatta.murphy@gmail.com')
>>> new_user.set_password('wakandaforever')
>>> new_user.save()

如果我们现在查询我们所有的 Owner,我们应该会找到 Kenyatta。

>>> Owner.objects.all()
<QuerySet [<Owner: Owner object (1)>]>

耶!我们有数据了!

序列化模型

我们将不仅仅是传递“Hello World”来回传递数据。因此,我们希望看到某种 JSON 格式化的输出,很好地表示这些数据。获取该对象的数据并将其转换为 JSON 对象,以便通过 HTTP 提交,这是数据序列化的一种形式。在序列化数据时,我们正在获取当前拥有的数据,并将其重新格式化以适应某种标准、更易于理解的形式。

如果我在 Flask、Pyramid 和 Tornado 中这样做,我会在每个模型上创建一个新方法,以便用户可以直接调用 to_json()to_json() 的唯一工作是返回一个 JSON 可序列化的(即数字、字符串、列表、字典)字典,其中包含我希望在问题对象中显示的任何字段。

对于 Task 对象,它可能看起来像这样

class Task(Base):
    ...all the fields...

    def to_json(self):
        """Convert task attributes to a JSON-serializable dict."""
        return {
            'id': self.id,
            'name': self.name,
            'note': self.note,
            'creation_date': self.creation_date.strftime('%m/%d/%Y %H:%M:%S'),
            'due_date': self.due_date.strftime('%m/%d/%Y %H:%M:%S'),
            'completed': self.completed,
            'user': self.user_id
        }

它并不花哨,但它能完成工作。

然而,Django REST Framework 为我们提供了一个对象,它不仅可以为我们做到这一点,而且还可以在我们想要创建新对象实例或更新现有实例时验证输入。它被称为 ModelSerializer

Django REST Framework 的 ModelSerializer 实际上是我们模型的文档。如果没有模型附加,它们就没有自己的生命(为此有 Serializer 类)。它们的主要工作是准确地表示我们的模型,并在我们的模型数据需要序列化并通过网络发送时,使转换为 JSON 变得毫不费力

Django REST Framework 的 ModelSerializer 最适合简单对象。例如,假设我们的 Task 对象上没有 ForeignKey。我们可以为我们的 Task 创建一个序列化器,它将根据需要将其字段值转换为 JSON,声明如下

# todo/serializers.py
from rest_framework import serializers
from todo.models import Task

class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""

    class Meta:
        model = Task
        fields = ('id', 'name', 'note', 'creation_date', 'due_date', 'completed')

在我们的新 TaskSerializer 内部,我们创建一个 Meta 类。Meta 在这里的工作只是保存关于我们尝试序列化的事物的信息(或元数据)。然后,我们记录我们想要显示的特定字段。如果我们想显示所有字段,我们可以简化该过程并使用 '__all__'。或者,我们可以使用 exclude 关键字而不是 fields 来告诉 Django REST Framework 我们想要除少数几个字段之外的所有字段。我们可以拥有任意数量的序列化器,所以也许我们想要一个用于字段的小子集,一个用于所有字段?在这里尽情发挥

在我们的例子中,每个 Task 与其所有者 Owner 之间存在关系,这种关系必须在此处反映出来。因此,我们需要借用 serializers.PrimaryKeyRelatedField 对象来指定每个 Task 将有一个 Owner,并且这种关系是一对一的。它的所有者将从现有的所有所有者的集合中找到。我们通过查询这些所有者并返回我们希望与此序列化器关联的结果来获得该集合:Owner.objects.all()。我们还需要在字段列表中包含 owner,因为我们始终需要与 Task 关联的 Owner

# todo/serializers.py
from rest_framework import serializers
from todo.models import Task
from owner.models import Owner

class TaskSerializer(serializers.ModelSerializer):
    """Serializer for the Task model."""
    owner = serializers.PrimaryKeyRelatedField(queryset=Owner.objects.all())

    class Meta:
        model = Task
        fields = ('id', 'name', 'note', 'creation_date', 'due_date', 'completed', 'owner')

现在这个序列化器已经构建好了,我们可以将它用于我们想要对对象执行的所有 CRUD 操作

  • 如果我们想要 GET 特定 Task 的 JSON 格式版本,我们可以执行 TaskSerializer(some_task).data
  • 如果我们想接受一个带有适当数据的 POST 来创建一个新的 Task,我们可以使用 TaskSerializer(data=new_data).save()
  • 如果我们想使用 PUT 更新一些现有数据,我们可以说 TaskSerializer(existing_task, data=data).save()

我们没有包含 delete,因为我们实际上不需要对 delete 操作的信息做任何事情。如果您有权访问要删除的对象,只需说 object_instance.delete()

以下是一些序列化数据可能看起来像的示例

>>> from todo.models import Task
>>> from todo.serializers import TaskSerializer
>>> from owner.models import Owner
>>> from django.contrib.auth.models import User
>>> new_user = User(username='kenyatta', email='kenyatta@gmail.com')
>>> new_user.save_password('wakandaforever')
>>> new_user.save() # creating the User that builds the Owner
>>> kenyatta = Owner.objects.first() # grabbing the Owner that is kenyatta
>>> new_task = Task(name="Buy roast beef for the Sunday potluck", owner=kenyatta)
>>> new_task.save()
>>> TaskSerializer(new_task).data
{'id': 1, 'name': 'Go to the supermarket', 'note': None, 'creation_date': '2018-07-31T06:00:25.165013Z', 'due_date': None, 'completed': False, 'owner': 1}

您可以使用 ModelSerializer 对象做更多的事情,我建议查看 文档 以了解这些更强大的功能。否则,这就是我们所需要的全部内容。现在是深入研究一些视图的时候了。

真实的视图

我们已经构建了模型和序列化器,现在我们需要为我们的应用程序设置视图和 URL。毕竟,我们无法对没有视图的应用程序做任何事情。我们已经在上面的 HelloWorld 视图中看到了一个示例。然而,这始终是一个人为的、概念验证的示例,并没有真正展示 Django REST Framework 的视图可以做什么。让我们清除 HelloWorld 视图和 URL,以便我们可以从头开始创建视图。

我们将构建的第一个视图是 InfoView。与之前的框架一样,我们只想打包并发送一个包含我们建议的路由的字典。视图本身可以放在 django_todo.views 中,因为它不属于特定的模型(因此在概念上不属于特定的应用)。

# django_todo/views.py
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

class InfoView(APIView):
    """List of routes for this API."""
    def get(self, request):
        output = {
            'info': 'GET /api/v1',
            'register': 'POST /api/v1/accounts',
            'single profile detail': 'GET /api/v1/accounts/<username>',
            'edit profile': 'PUT /api/v1/accounts/<username>',
            'delete profile': 'DELETE /api/v1/accounts/<username>',
            'login': 'POST /api/v1/accounts/login',
            'logout': 'GET /api/v1/accounts/logout',
            "user's tasks": 'GET /api/v1/accounts/<username>/tasks',
            "create task": 'POST /api/v1/accounts/<username>/tasks',
            "task detail": 'GET /api/v1/accounts/<username>/tasks/<id>',
            "task update": 'PUT /api/v1/accounts/<username>/tasks/<id>',
            "delete task": 'DELETE /api/v1/accounts/<username>/tasks/<id>'
        }
        return JsonResponse(output)

这与我们在 Tornado 中的非常相似。让我们将其连接到一个合适的路由,然后继续前进。为了确保万无一失,我们还将删除 admin/ 路由,因为我们在这里不会使用 Django 管理后端。

# in django_todo/urls.py
from django_todo.views import InfoView
from django.urls import path

urlpatterns = [
    path('api/v1', InfoView.as_view(), name="info"),
]

将模型连接到视图

让我们弄清楚下一个 URL,它将是用于创建新 Task 或列出用户现有任务的端点。这应该存在于 todo 应用的 urls.py 中,因为它必须专门处理 Task 对象,而不是作为整个项目的一部分。

# in todo/urls.py
from django.urls import path
from todo.views import TaskListView

urlpatterns = [
    path('', TaskListView.as_view(), name="list_tasks")
]

这个路由是怎么回事?我们没有指定特定的用户或太多的路径。由于将有几个路由需要基本路径 /api/v1/accounts/<username>/tasks,为什么我们要一遍又一遍地编写它,而我们可以只写一次呢?

Django 允许我们获取一整套 URL,并将它们导入到基本的 django_todo/urls.py 文件中。然后,我们可以为每个导入的 URL 提供相同的基本路径,只在变量部分变化时才担心它们。

# in django_todo/urls.py
from django.urls import include, path
from django_todo.views import InfoView

urlpatterns = [
    path('api/v1', InfoView.as_view(), name="info"),
    path('api/v1/accounts/<str:username>/tasks', include('todo.urls'))
]

现在,来自 todo/urls.py 的每个 URL 都将路径 api/v1/accounts/<str:username>/tasks 为前缀。

让我们在 todo/views.py 中构建视图

# todo/views.py
from django.shortcuts import get_object_or_404
from rest_framework.response import JsonResponse
from rest_framework.views import APIView

from owner.models import Owner
from todo.models import Task
from todo.serializers import TaskSerializer


class TaskListView(APIView):
    def get(self, request, username, format=None):
        """Get all of the tasks for a given user."""
        owner = get_object_or_404(Owner, user__username=username)
        tasks = Task.objects.filter(owner=owner).all()
        serialized = TaskSerializer(tasks, many=True)
        return JsonResponse({
            'username': username,
            'tasks': serialized.data
        })

少量的代码中发生了很多事情,所以让我们逐步了解它。

我们从相同的 APIView 继承开始,这是我们一直在使用的,为我们的视图奠定基础。我们覆盖了我们之前覆盖的相同的 get 方法,添加了一个参数,允许我们的视图从传入的请求中接收 username

我们的 get 方法将使用该 username获取与该用户关联的 Owner。这个 get_object_or_404 函数允许我们做到这一点,并添加了一些特殊的东西,以便于使用。

如果找不到指定的用户,那么查找任务就没有意义,这是合乎逻辑的。实际上,我们希望返回 404 错误。get_object_or_404 根据我们传入的任何条件获取单个对象,并返回该对象或引发 Http404 exception。我们可以根据对象的属性设置该条件。Owner 对象都通过其 user 属性附加User。但是,我们没有 User 对象可用于搜索。我们只有一个 username。因此,我们对 get_object_or_404 说“当您查找 Owner 时,检查与其附加User 是否具有我想要的 username”,方法是指定 user__username。那是两个下划线。当通过 QuerySet 进行过滤时,两个下划线表示“此嵌套对象的属性”。这些属性可以根据需要进行深度嵌套。

我们现在有了与给定用户名对应的 Owner。我们使用该 Owner 过滤所有任务,仅检索它拥有的任务,使用 Task.objects.filter。我们可以使用与 get_object_or_404 相同的嵌套属性模式来深入到连接到 OwnerUser,再连接到 Tasks (tasks = Task.objects.filter(owner__user__username=username).all()),但没有必要如此狂野

Task.objects.filter(owner=owner).all() 将为我们提供一个 QuerySet,其中包含与我们的查询匹配的所有 Task 对象。太棒了。然后,TaskSerializer获取QuerySet 及其所有数据,以及 many=True 标志,以通知它是一个项目集合,而不是仅仅一个项目,并返回一组序列化的结果。实际上是一个字典列表。最后,我们提供传出的响应,其中包含 JSON 序列化数据和用于查询的用户名。

处理 POST 请求

post 方法看起来与我们之前看到的有些不同

# still in todo/views.py
# ...other imports...
from rest_framework.parsers import JSONParser
from datetime import datetime

class TaskListView(APIView):
    def get(self, request, username, format=None):
        ...

    def post(self, request, username, format=None):
        """Create a new Task."""
        owner = get_object_or_404(Owner, user__username=username)
        data = JSONParser().parse(request)
        data['owner'] = owner.id
        if data['due_date']:
            data['due_date'] = datetime.strptime(data['due_date'], '%d/%m/%Y %H:%M:%S')

        new_task = TaskSerializer(data=data)
        if new_task.is_valid():
            new_task.save()
            return JsonResponse({'msg': 'posted'}, status=201)

        return JsonResponse(new_task.errors, status=400)

当我们从客户端接收数据时,我们使用 JSONParser().parse(request) 将其解析为字典。我们将所有者添加到数据中,并为任务格式化 due_date(如果存在)。

我们的 TaskSerializer 承担了繁重的工作。它首先接收传入的数据,并将其转换为我们在模型上指定的字段。然后,它验证该数据,以确保其符合指定的字段。如果附加到新 Task 的数据有效,它将使用该数据构建一个新的 Task 对象,并将其提交到数据库。然后,我们发回一个适当的“耶!我们创建了一个新事物!”响应。如果无效,我们收集 TaskSerializer 生成的错误,并将这些错误与 400 Bad Request 状态代码一起发回客户端。

如果我们构建用于更新 Taskput 视图,它将与此非常相似。主要区别在于,当我们实例化 TaskSerializer 时,我们不是仅仅传入新数据,而是传入旧对象和该对象的新数据,例如 TaskSerializer(existing_task, data=data)。我们仍然会进行有效性检查,并发送回我们想要发送回的响应。

总结

Django 作为一个框架是高度可定制的,每个人都有自己拼接 Django 项目的方式。我在这里写出的方式不一定是 Django 项目需要设置的确切方式;这只是 a) 我熟悉的方式,以及 b) 利用 Django 管理系统的方式。当您将概念分离到它们自己的小孤岛中时,Django 项目的复杂性会增加。您这样做是为了让多人更容易地为整个项目做出贡献,而不会互相干扰

然而,Django 项目的庞大文件地图并不会使其性能更高,也不会自然而然地倾向于微服务架构。相反,它很容易变成一个令人困惑的庞然大物。这可能仍然对您的项目有用。但也可能使您的项目更难以管理,尤其是在项目增长时。

仔细考虑您的选择,并为正确的工作使用正确的工具。对于像这样的简单项目,Django 很可能不是正确的工具。

Django 旨在处理涵盖各种不同项目领域的多组模型,这些领域可能共享一些共同点。这个项目是一个小型、双模型项目,只有少数几个路由。如果我们进一步构建它,我们将只有七个路由,并且仍然是相同的两个模型。这几乎不足以证明一个完整的 Django 项目是合理的。

如果我们预期这个项目会扩展,这将是一个很好的选择。这不是其中一个项目。这是选择火焰喷射器来点燃蜡烛。这绝对是杀鸡用牛刀

尽管如此,Web 框架仍然是 Web 框架,无论您为项目使用哪一个。它可以接收请求并像其他任何框架一样做出响应,所以您随意。只需注意您选择的框架带来的开销

就这样!我们已经到达本系列的结尾!我希望这是一次富有启发性的冒险,并且当您考虑如何构建下一个项目时,它将帮助您做出不仅仅是最熟悉的选择。请务必阅读每个框架的文档,以扩展本系列中涵盖的任何内容(因为它甚至不是最全面的)。每个框架都有一个广阔的世界可以涉足。祝您编码愉快!

接下来要读什么

7 条评论

文章写得非常好!!

您还可以包括如何包含 drfdocs 以进行 API 文档记录,这使使用 API 的用户的生活更轻松一些。

哦,当然。关于构建适当的 Django REST API,我还有很多想说的,但这篇文章已经够长了。如果您有一个链接示例可以包含在此处的评论中,那就太好了,这样其他读者就可以获得补充信息。

感谢阅读!

回复 作者 psachin

不错的教程,一些反馈

JsonResponse 在 djangorestframework 3.8.2 中不可用。

另外,您可以分享您的 Github 代码吗?

哦,糟糕,您完全正确!它应该只是普通的 `Response`。我想我最初使用 Django 中的 `JsonResponse` 在本文中编写了代码块,并在将其切换为 rest_framework.response 中的 `Response` 时忘记更新它。谢谢您的指出!

回复 作者 jasonwee

不错的教程... 在 ubuntu 16.04 LTS 下工作有点棘手(那里有一篇文章等待制作)

您可以分享您的 Github 代码吗?

© . All rights reserved.