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_todo 内部的 django_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 项目内加密签名的密钥。在实践中,它用于会话、cookie、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 函数将 ONLY 将视图函数作为第二个参数,而不仅仅是基于类的视图本身。幸运的是,所有有效的 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 项目,因此我们应该尽可能将我们的模型划分为它们自己的小包 The Django Way™。

The 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 列同时设置了 blanknullTrue,以便待办事项列表上的项目可以只是在未来的某个时间点完成的项目,没有定义的日期或时间。

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 表的 owner 字段具有空值,也不会是世界末日;从现在开始创建的任何任务都必然会有一个所有者。如果您的数据库表不允许这种模式,请删除您的迁移,删除表,并重建迁移。

# 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 来获取与该用户关联的 Ownerget_object_or_404 函数允许我们做到这一点,并添加了一些特殊的东西,以便于使用。

如果找不到指定的用户,那么查找任务就没有意义了,这是有道理的。实际上,我们希望返回 404 错误。get_object_or_404 基于我们传入的任何条件获取单个对象,并返回该对象或引发 Http404 异常。我们可以根据对象的属性设置该条件。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 状态代码一起发回客户端。

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

总结

Django 作为一个框架是高度可定制的,每个人都有自己组织 Django 项目的方式。我在这里写出的方式不一定是 Django 项目需要设置的确切方式;它只是 a) 我熟悉的,以及 b) 利用 Django 管理系统的。当您将概念分离到它们自己的小 silo 中时,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.