在本系列四篇比较不同 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 个字符)USE_L10N
(L10N = 本地化 [“l”和“n”之间有 10 个字符]) 如果设置为True
,将使用常见的数据本地格式。一个很好的例子是日期:在美国,它是 MM-DD-YYYY。在欧洲,日期倾向于写成 DD-MM-YYYYSTATIC_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,因此我们必须通过在 settings.py
的 INSTALLED_APPS
末尾添加 rest_framework
来确保我们可以使用它。
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,通常是因为人们想检查它看起来是否正确,或者在设计 API 的某些使用者时了解 JSON 响应的外观。这很像您从 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,而无需太多麻烦。
要为我们的待办事项列表项创建 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.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
不同,后者表示,当构造此模型类的表时,与 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
,这样待办事项列表中的项目可以只是将来某个时候要完成的项目,而无需定义日期或时间。
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
对象上粘贴一个指向 Owner
的 ForeignKey
字段。
# 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
表的所有者具有空值,那不会是世界末日;从现在开始创建的任何任务都必然会有一个所有者。如果您的情况是数据库表不允许这种模式,请清除您的迁移,删除表,然后重建迁移。
# 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
。我们只希望在这种情况下执行某些操作。如果是新实例,我们创建一个新的 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
(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 条评论