在这个由四部分组成的系列文章的前三篇中,我们比较了不同的 Python Web 框架,涵盖了 Pyramid、Flask 和 Tornado 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.7
和 djangorestframework-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 也必须公开一个。这是在这里完成的。然后可以使用诸如 Gunicorn、Waitress 或 uWSGI 之类的东西来服务它。
设置设置
查看 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', ''))
。
- 在开发模式下,当出现错误时,Django 将显示导致该错误的完整堆栈跟踪,以及运行项目所涉及的所有设置和配置。如果在生产环境中将
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
数据库。我们可以按名称设置其他数据库,只要我们提供HOST
、USER
、PASSWORD
、PORT
、数据库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 个字符)- 如果设置为
True
,USE_L10N
(L10N = 本地化 [“l”和“n”之间有 10 个字符])将使用常见的本地数据格式。一个很好的例子是日期:在美国是 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
是数据库的主机。localhost
或127.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 基于类的视图都直接或间接地继承自 APIView
。APIView
处理大量内容,但对于我们的目的,它执行以下特定操作
- 设置基于 HTTP 方法(例如 GET、POST、PUT、DELETE)定向流量所需的方法
- 使用我们将需要的所有数据和属性填充
request
对象,以解析和处理任何传入的请求 - 获取每个调度方法(即,名为
get
、post
、put
、delete
的方法)返回的Response
或JsonResponse
,并构造格式正确的 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,通常是因为人们想检查它看起来是否正确,或者了解 JSON 响应在他们设计 API 的一些消费者时会是什么样子。这很像您从 Postman 等服务中获得的东西。
无论如何,我们知道我们的视图正在工作!太棒了!让我们回顾一下我们所做的事情
- 使用
django-admin startproject <项目名称>
启动项目 - 更新
django_todo/settings.py
以使用环境变量来设置DEBUG
、SECRET_KEY
和DATABASES
字典中的值 - 安装了
Django REST Framework
并将其添加到INSTALLED_APPS
列表中 - 创建了
django_todo/views.py
以包含我们的第一个视图类来向世界问好 - 使用我们新的主页路由的路径更新了
django_todo/urls.py
- 更新了
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,从而减少麻烦。
要为我们的 To-Do List 项目创建 Django 应用程序,我们将需要使用带有 manage.py
的 startapp
命令。
(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.Model
的 class
。将应用于该模型实例的所有字段都应作为类属性出现。与过去一样从 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.CharField
或 models.TextField
。CharField
用于特定最大长度的小文本字段,而 TextField
用于任何数量的文本。
TextField
应该能够为空,我们以两种方式指定这一点。blank=True
表示当构造此模型的实例时,并且正在验证附加到此字段的数据时,该数据可以为空。这与 null=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
列的 blank
和 null
都设置为 True
,这样 To-Do List 上的项目就可以只是在未来的某个时间完成的项目,而没有定义的日期或时间。
BooleanField
最终成为一个可以取两个值之一的字段:True
或 False
。这里,默认值设置为 False
。
管理数据库
如前所述,Django 有自己管理数据库的方式。我们无需编写… 实际上任何关于我们数据库的代码,我们只需利用 Django 在构建时提供的 manage.py
脚本。它不仅会管理数据库表的构建,还会管理我们希望对这些表进行的任何更新,而无需必须完全清除它!
因为我们构建了一个新的模型,所以我们需要让我们的数据库知道它。首先,我们需要将与此模型对应的模式放入代码中。manage.py
的 makemigrations
命令将获取我们构建的模型类及其所有字段的快照。它将获取该信息并将其打包到一个 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
实例也将被删除。 让我们运行 makemigrations
和 migrate
将这个新模型烘焙到我们的数据库中。
(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
对象上粘贴一个 ForeignKey
字段,指向 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)
每个待办事项列表任务都恰好有一个所有者,该所有者可以拥有多个任务。 当该所有者被删除时,他们拥有的任何任务都会随之消失。
现在让我们运行 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 也会对我们的迁移进行预检查,以确保它们是兼容的,而我们提出的这个新迁移是不兼容的。
有几种方法可以处理这类问题
- 删除当前的迁移,并构建一个新的迁移,其中包含当前的模型配置
- 为
Task
对象上的owner
字段添加默认值 - 允许任务的
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
设置为该 Owner
的 user
字段。” 在实践中,它看起来像这样
# 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
对象。 这将取代我们熟悉的 session
或 dbsession
。 让我们打开 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 异常。 我们可以根据对象的属性设置该条件。 Owner
对象都通过它们的 user
属性附加到 User
。 但是,我们没有 User
对象可以用来搜索。 我们只有一个 username
。 因此,我们告诉 get_object_or_404
“当您查找 Owner
时,请检查与其关联的 User
是否具有我想要的 username
”,方法是指定 user__username
。 这是两个下划线。 当通过 QuerySet 筛选时,两个下划线表示“这个嵌套对象的属性”。 这些属性可以根据需要进行深度嵌套。
我们现在拥有与给定用户名对应的 Owner
。 我们使用该 Owner
筛选所有任务,仅检索它拥有的任务,使用 Task.objects.filter
。 我们可以使用与 get_object_or_404
相同的嵌套属性模式来深入了解连接到 Owner
的 User
,再连接到 Tasks
的 User
(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 管理系统的方式。当您将概念分离到各自的小型信息孤岛中时,Django 项目的复杂性会增加。您这样做是为了让多人更容易地为整个项目做出贡献,而不会互相干扰。
然而,构成 Django 项目的庞大文件地图并不会使其性能更高,也不会自然而然地倾向于微服务架构。相反,它很容易变成一个令人困惑的庞然大物。这可能仍然对您的项目有用。但也可能使您的项目更难管理,尤其是随着它的增长。
仔细考虑您的选择,并为正确的工作使用正确的工具。对于像这样的简单项目,Django 可能不是正确的工具。
Django 旨在处理多组模型,这些模型涵盖各种不同的项目领域,这些领域可能共享一些共同点。这个项目是一个小型、双模型项目,只有少量路由。如果我们要进一步构建它,我们将只有七个路由,并且仍然是相同的两个模型。这不足以证明一个完整的 Django 项目是合理的。
如果我们期望这个项目扩展,这将是一个很好的选择。但这并不是其中一个项目。这就像选择火焰喷射器来点燃蜡烛。这绝对是杀鸡用牛刀。
尽管如此,Web 框架就是一个 Web 框架,无论您为项目使用哪一个。它可以接收请求并像其他任何框架一样响应,所以您可以随心所欲。只是要注意您选择的框架带来的开销。
就这样!我们已经到达本系列的结尾!我希望这是一次富有启发性的冒险,并且当您考虑如何构建下一个项目时,它将帮助您做出不仅仅是最熟悉的选择。请务必阅读每个框架的文档,以扩展本系列中涵盖的任何内容(因为它甚至不是最全面的)。每个框架都有一个广阔的世界可以深入了解。祝您编码愉快!
7 条评论