使用 Django2 快速开发 Web 服务

微信扫一扫,分享到朋友圈

使用 Django2 快速开发 Web 服务

Django 是一款基于 Python 编写并且采用 MVC 设计模式的开源的 Web 应用框架,早期是作为劳伦斯出版集团新闻网站的 CMS 内容管理系统而开发,后于 2005 年 7 月 在 BSD 许可协议下开源,并于 2017 年 12 月 2 日 发布 2.0 正式版。虽然近几年 Go 语言在 Web 开发领域异军突起,但是在框架成熟度以及语言生态方面与 Python 还存有一定差距,针对于需要快速开发的原型类项目以及性能要求不高的 CMS 和 Admin 类型项目,已经发展了 12 年之久的 Django 依然是非常明智的选择。

本文基于 《Django 官方 Tutorials》 以及 《Django REST framework 官方 Tutorials》 编写,发稿时所使用的 Django 版本为 2.1.4 ,Python 版本为 3.6.6 ,文中涉及的代码都已经由笔者验证运行通过,最终形成了一个简单项目并推送至笔者 Github 上的 jungle 项目当中,需要的朋友可以基于此来逐步步完善成为一个产品化的项目。

新建 Django 项目

下面的命令行展示了在 Windows 操作系统下,基于 venv 虚拟环境搭建一个 Django 项目的步骤:

# 建立虚拟环境
C:Workspacedjango
λ python -m venv venv
# 激活虚拟环境
C:Workspacedjango
λ .venvScriptsactivate.bat
(venv) λ
# 安装Django
C:Workspacedjango
(venv) λ pip install Django
Looking in indexes: https://mirrors.aliyun.com/pypi/simple/
Collecting Django
Using cached https://mirrors.aliyun.com/pypi/packages/fd/9a/0c028ea0fe4f5803dda1a7afabeed958d0c8b79b0fe762ffbf728db3b90d/Django-2.1.4-py3-none-any.whl
Collecting pytz (from Django)
Using cached https://mirrors.aliyun.com/pypi/packages/f8/0e/2365ddc010afb3d79147f1dd544e5ee24bf4ece58ab99b16fbb465ce6dc0/pytz-2018.7-py2.py3-none-any.whl
Installing collected packages: pytz, Django
Successfully installed Django-2.1.4 pytz-2018.7
# 进入虚拟环境目录,新建一个Django项目
C:Workspacedjango
(venv) λ django-admin startproject mysite
C:Workspacedjango
(venv) λ ls
mysite/  venv/
# 进入新建的Django项目,建立一个应用
C:Workspacedjango
(venv) λ cd mysite
C:Workspacedjangomysite
(venv) λ python manage.py startapp demo
C:Workspacedjangomysite
(venv) λ ls
demo/  manage.py*  mysite/
# 同步数据库
C:Workspacedjangomysite
(venv) λ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
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 admin.0003_logentry_add_action_flag_choices... 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
# 启动开发服务
(venv) λ python manage.py runserver 8080
Performing system checks...
System check identified no issues (0 silenced).
January 03, 2019 - 21:31:48
Django version 2.1.4, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8080/
Quit the server with CTRL-BREAK.
# 返回uinika虚拟环境目录,并将当前虚拟环境的依赖导入至requirements.txt
C:Workspacedjangomysite
(venv) λ cd ..
C:Workspacedjango
(venv) λ pip freeze > requirements.txt
C:Workspacedjango
(venv) λ ls
mysite/  requirements.txt  venv/

通过 django-admin startproject 命令创建的外部 mysite/ 目录是 Web 项目的容器,而 manage.py 文件是用于与 Django 项目交互的命令行工具,更多的使用方式可以参阅 django-admin 文档 。。

mysite/
manage.py
mysite/
__init__.py
settings.py
urls.py
wsgi.py

内部嵌套的 mysite/ 目录是用于放置项目中具体的 Python 包,它的名称是您需要用来导入其中任何内容的 Python 包名称,例如 mysite.urls

  • mysite/__init__.py : 空文件,用于提示系统将当前目录识别为一个 Python 包。
  • mysite/settings.py : Django 项目的配置文件,更多配置请查阅 Django settings
  • mysite/urls.py : 当前 Django 项目的 URL 声明,更多内容请参阅 URL dispatcher
  • mysite/wsgi.py : 兼容 WSGI 规范的当前项目入口点,更多细节可以阅读 如果使用 WSGI 进行部署

建立 mysite 项目之后,上面的命令行还通过了 py manage.py startapp 建立了一个 demo/ 应用目录,Django 当中一个项目( mysite )可以拥有多个应用( demo ), demo/ 目录下的文件结构如下:

demo/
__init__.py
admin.py
apps.py
migrations/
__init__.py
models.py
tests.py
views.py

使用命令 python manage.py runserver 启动 Django 服务时,默认会监听 localhost 地址下的 80 端口,如果希望网络里的其它主机能够正常访问服务,必须在 mysite/settings.py 显式的声明当前允许的主机地址:

ALLOWED_HOSTS = ['10.102.16.79']

然后使用 manage.py 启动服务时,指定好主机名和对应的端口号:

C:Workspacecloud-keymysite (master -> origin)
(venv) λ python manage.py runserver 10.102.16.79:8000
Performing system checks...
System check identified no issues (0 silenced).
January 09, 2019 - 14:09:48
Django version 2.1.5, using settings 'mysite.settings'
Starting development server at http://10.102.16.79:8000/
Quit the server with CTRL-BREAK.

请求与响应

首先进入 Python 虚拟环境并进入 mysite 目录后,执行如下命令:

C:Workspacedjangomysite (master -> origin)
(venv) λ python manage.py startapp polls

polls/views.py

新建一个 polls 应用之后,打开该目录下的 polls/views.py 源码文件,输入以下代码:

from django.http import HttpResponse
def index(request):
return HttpResponse("你好,这是一个投票应用!")

polls/urls.py

接下来,我们需要将上面修改的视图文件 views.py 映射到一个 URL,先在 polls/ 目录下新建一个 urls.py 文件,然后键入下面这段代码:

from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]

mysite/urls.py

最后,将上面定义的应用的 URL 声明文件 polls/urls.py 模块包含至项目的 mysite/urls.py 代码当中,

from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]

上面代码中出现的 include() 函数主要用于引入其它 URL 配置文件,这样我们就可以通过 http://localhost:8080/polls/ 路径访问到如下信息了:

模型和管理页面

  • Model 类 :Django 中每一个 Model 都是 django.db.models.Model 的子类,每个 Model 类都映射着一个数据库表,Model 的每个属性则相当于一个数据库字段。
from django.db import models
# Django会在Blog类创建后自动生成`CREATE TABLE`语句。
class Blog(models.Model):
title = models.CharField(max_length=50)
content = models.CharField(max_length=800)
  • Model 实例 :Model 类的实例用来表示数据库表中的一条特定记录,可以通过向 Model() 类传递关键字参数,然后调用 save() 方法保存至数据库,从而创建出一个 Model 类实例。
from blog.models import Blog
blog = Blog(title='Bit by bit', content='一些内容。')
blog.save()  # 该方法没有返回值,在底层会生成并执行一条insert语句
  • QuerySet : QuerySet 表示数据库查询的结果集( SELECT 语句 ),该结果集拥有一个或多个过滤方法( WHERE 或 LIMIT 子句 )。获取结果集可通过 Model 的 Manager 管理器( 用于向 Django 模型提供数据库查询操作的接口 ),而 Manager 则默认由结果集的 objects 属性获得。
Blog.objects # <django.db.models.manager.Manager object at ...>
Blog.objects.all() # 包含Blog对象里的所有记录

Django 当中, QuerySet 用来执行 记录级操作Model 实例则用来进行 表级操作

mysite/settings.py

mysite/settings.py 文件包含了项目的基本配置,该文件通过如下声明默认使用 Django 内置的 SQLite 作为项目数据库。

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}

如果使用其它数据库,则可以将配置书写为下面的格式:

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',     # 数据库引擎名称
'NAME': 'db',                             # 数据库连接名称
'USER': 'uinika',                         # 数据库连接用户名
'PASSWORD': 'test',                       # 数据库连接密码
'HOST': 'localhost',                      # 数据库主机地址
'PORT': '3306',                           # 数据库端口
}
}

其中 ENGINE 属性可以根据项目所使用数据库的不同而选择如下值:

django.db.backends.sqlite3
django.db.backends.mysql
django.db.backends.postgresql
django.db.backends.oracle

接下来继续修改 mysite/settings.py ,设置 TIME_ZONE 属性为项目使用国家的时区。

LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Chongqing'

mysite/settings.py 文件头部的 INSTALLED_APPS 属性定义了当前项目使用的应用程序。

INSTALLED_APPS = [
'django.contrib.admin',          # 管理员站点
'django.contrib.auth',           # 认证授权系统
'django.contrib.contenttypes',   # 内容类型框架
'django.contrib.sessions',       # 会话框架
'django.contrib.messages',       # 消息框架
'django.contrib.staticfiles',    # 静态文件管理
]

在前面命令行中执行的 python manage.py migrate 命令会检查 INSTALLED_APPS 属性的设置,并为其中的每个应用创建所需的数据表,实际上 migrate 命令只会为对 INSTALLED_APPS 里声明了的应用进行数据库迁移

polls/models.py

了解项目配置文件的一些设置之后,现在来编辑 polls/models.py 文件新建 Question(问题)Choice(选项) 两个数据模型:

from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)

每个自定义模型都是 django.db.models.Model 的子类,模型里的类变量都表示一个数据库字段,每个字段实质都是 Field 类的实例。注意在 Choice 使用了 ForeignKey 属性定义了一个与 Question 的外键关联关系,Django 支持所有常用的多对一、多对多和一对一数据库关系。

mysite/settings.py

数据库模型建立完成之后,由于 PollsConfig 类位于 polls/apps.py 文件当中,所以其对应的点式路径为 polls.apps.PollsConfig ,现在我们需要将该路径添加至 mysite/settings.py 文件的 INSTALLED_APPS 属性:

INSTALLED_APPS = [
'polls.apps.PollsConfig', # 添加PollsConfig
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

将数据模型迁移至数据库

通过 manage.py 提供的 makemigrations 命令,可以将模型的更新迁移至 SQLite 数据库当中。

C:Workspacedjangomysite (master -> origin)
(venv) λ python manage.py makemigrations polls
Migrations for 'polls':
pollsmigrations001_initial.py
- Create model Choice
- Create model Question
- Add field question to choice

我们还可以通过 manage.py 提供的 sqlmigrate 命令,查看数据迁移过程中执行了哪些 SQL 语句,该命令并不会实质性执行 Django 模型到数据库的迁移任务。

C:Workspacedjangomysite (master -> origin)
(venv) λ python manage.py sqlmigrate polls 0001
BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "v
otes" integer NOT NULL);
--
-- Create model Question
--
CREATE TABLE "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL
, "pub_date" datetime NOT NULL);
--
-- Add field question to choice
--
ALTER TABLE "polls_choice" RENAME TO "polls_choice__old";
CREATE TABLE "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "v
otes" integer NOT NULL, "question_id" integer NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERR
ED);
INSERT INTO "polls_choice" ("id", "choice_text", "votes", "question_id") SELECT "id", "choice_text", "votes", NULL FR
OM "polls_choice__old";
DROP TABLE "polls_choice__old";
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
COMMIT;

Django 模型的数据库主键 ID 会被自动创建, 并会在外键字段名称后追加 _id 字符串作为后缀。

接下来运行 manage.py 提供的 migrate 命令,在根据新定义的模型创建相应的数据库表。

C:Workspacedjangomysite (master -> origin)
(venv) λ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
Applying polls.0001_initial... OK

为了便于在版本管理系统提交迁移数据,Django 将模型的修改分别独立为 生成应用 两个命令,因此修改 Django 模型会涉及如下 3 个步骤:

models.py
python manage.py makemigrations
python manage.py migrate

完成上述 Django 模型与数据库的同步之后,接下来可以通过 manage.py 提供的 shell 命令,在命令行工具内运行 Django 提供的交互式 API。

C:Workspacedjangomysite (master -> origin)
(venv) λ python manage.py shell
Python 3.6.6 (v3.6.6:4cf1f54eb7, Jun 27 2018, 03:37:03) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from polls.models import Choice, Question
>>> Question.objects.all()
<QuerySet []>
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())
>>> q.save()
>>> q.id
1
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2019, 1, 4, 9, 10, 1, 955820, tzinfo=<UTC>)
>>> q.question_text = "What's up?"
>>> q.save()
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

上面命令行执行结果中的 <Question: Question object (1)> 对于实际开发没有意义,因此可以考虑为上面建立的 Django 模型增加 __str__() 方法直接打印模型对象的属性数据。为了便于进一步测试,这里还为 Question 类添加一个自定义的 was_published_recently() 方法:

import datetime
from django.db import models
from django.utils import timezone
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
# 自定义was_published_recently()方法
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
# 添加__str__()方法
def __str__(self):
return self.question_text
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
# 添加__str__()方法
def __str__(self):
return self.choice_text

完成修改工作之后,再一次运行 python manage.py shell 命令:

C:Workspacedjangomysite (master -> origin)
(venv) λ python manage.py shell
Python 3.6.6 (v3.6.6:4cf1f54eb7, Jun 27 2018, 03:37:03) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from polls.models import Choice, Question
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>
>>> Question.objects.get(id=2)
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "C:Workspacedjangovenvlibsite-packagesdjangodbmodelsmanager.py", line 82, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "C:Workspacedjangovenvlibsite-packagesdjangodbmodelsquery.py", line 399, in get
self.model._meta.object_name
polls.models.Question.DoesNotExist: Question matching query does not exist.
>>> Question.objects.get(pk=1)
<Question: What's up?>
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True
>>> q = Question.objects.get(pk=1)
>>> q.choice_set.all()
<QuerySet []>
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
>>> c.question
<Question: What's up?>
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()
(1, {'polls.Choice': 1})

管理站点

Django 能够根据模型自动创建后台管理界面, 这里我们执行 manage.py 提供的 createsuperuser 命令创建一个管理用户:

C:Workspacedjangomysite (master -> origin)
(venv) λ python manage.py createsuperuser
Username (leave blank to use 'zhenghang'): hank
Email address: uinika@outlook.com
Password: ********
Password (again): ********
Superuser created successfully.

启动 Django 服务之后,就可以通过 URL 地址 http://localhost:8080/admin/login 并使用上面新建的用户名和密码进行登陆管理操作:

登陆后默认只能对权限相关的 UserGroup 进行管理,如果我们需要将 Question 数据模型纳入管理,那么必须要在 polls/admin.py 文件对其进行注册。

from django.contrib import admin
from .models import Question
admin.site.register(Question)

完成注册之后,刷新管理站点页面即可查看到 Question 管理选项:

视图与模板

polls/views.py

Django 使用 URLconfs 配置将 URL 与视图关联,即将 URL 映射至视图,下面我们将向 polls/views.py 文件添加一些能够接收参数的视图:

from django.http import HttpResponse
def index(request):
return HttpResponse("你好,这是一个投票应用!")
def detail(request, question_id):
return HttpResponse("你正在查看问题 %s 。" % question_id)
def results(request, question_id):
response = "你看到的是问题 %s 的结果。"
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("你正在对问题 %s 进行投票。" % question_id)

polls.urls

然后将这些新的视图添加至 polls.urls 模块:

from django.urls import path
from . import views
urlpatterns = [
# 访问 http://localhost:8080/polls/
path('', views.index, name='index'),
# 访问 http://localhost:8080/polls/5/
path('<int:question_id>/', views.detail, name='detail'),
# 访问 http://localhost:8080/polls/5/results/
path('<int:question_id>/results/', views.results, name='results'),
# 访问 http://localhost:8080/polls/5/vote/
path('<int:question_id>/vote/', views.vote, name='vote'),
]

修改 polls/views.py

Django 的每个视图只会完成两个任务:返回一个包含被请求页面内容的 HttpResponse 对象,或是抛出一个 Http404 这样的异常。这里为了展示数据库里按照发布日期排序的最近五个投票问题,我们再向 polls/views.py 代码文件的 index() 函数添加如下内容:

from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
output = ', '.join([q.question_text for q in latest_question_list])
return HttpResponse(output)

添加 templates 模板目录

这样直接将数据库查询结果输出到页面的方式并不优雅,实际开发环境当中我们通常会使用模板页面来展示数据,首先在 polls 应用目录下创建一个用来存放模板文件的 templates 目录。由于站点配置文件 mysite/settings.pyTEMPLATES 属性的默认设置,能够让 Django 在每个 INSTALLED_APPS 文件夹中自动寻找 templates 子目录,从而正确定位出模板的位置。

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

接下来继续在 templates 下面新建一个 polls 目录,然后在里边放置一个 index.html 文件,此时通过 URL 地址 http://localhost:8080/polls/ 就可以访问到这个模板文件,模板文件会将按照发布日期排序了的 Question 列表 latest_question_list 放置到 HttpResponse 上下文,并在 polls/index.html 模板当中完成数据绑定。

from django.http import HttpResponse
from django.template import loader
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template('polls/index.html')
context = {
'latest_question_list': latest_question_list,
}
return HttpResponse(template.render(context, request))

render()快捷方法

事实上,通过使用 render() 方法,Django 能够以更加简化的方式完成载入模板、填充上下文、返回 HttpResponse 对象这一系列步骤:

from django.shortcuts import render
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)

Http404 处理

接下来处理投票详情页面,这里会有一个新原则,即如果指定 ID 所对应的 Question 不存在,那么视图就会抛出一个 Http404 异常。在 polls/views.py 添加如下代码,

from django.http import Http404
from django.shortcuts import render
from .models import Question
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("问题不存在!")
return render(request, 'polls/detail.html', {'question': qu

然后暂时向 polls/templates/polls/detail.html 添加一行简单的 代码便于测试上面的代码。

Django 提供了诸如 get_object_or_404()get_list_or_404() 这样的快捷函数语法糖来解决 Http404 判断的问题,因而上一步的代码依然可以进一步简化为下面这样:

from django.shortcuts import get_object_or_404, render
from .models import Question
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})

templates/polls/detail.html

让我们进一步完善 polls/templates/polls/detail.html ,填充完整的视图代码:

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

通过在模板代码中使用 . 符号来访问变量属性,例如对于上面代码中的 , Django 首先会尝试对 question 对象使用 字典查找 (既 obj.get(str) ),如果失败再尝试 属性查找 (既 obj.str ),如果依然失败就会尝试 列表查找 (即 obj[int] )。另外循环 for 中的 question.choice_set.all 语句会被解析为 question.choice_set.all() 的 Python 的函数调用,完成后将返回一个可迭代的 Choice 对象,该对象仅限于 for 循环标签内部使用。

polls/templates/polls/index.html 编写的投票链接里使用了诸如 <a href="https://www.tuicool.com/polls//"></a> 这样的硬编码,但是这样容易造成视图与后端业务的耦合,因此 Django 提供了 url 标签来解决这个问题。

<a href="https://www.tuicool.com/articles/n2EJRzM/{% url 'detail' question.id %}">{{ question.question_text }}</a>

事实上,在 mysite/polls/urls.py 里的 path('<int:question_id>/', views.detail, name='detail') 函数调用当中, path()name 属性就是作用于 url 标签中的这个特性的。

URL 命名空间

为了避免项目当中各个应用的 URL 重名,导致 url 标签在使用时产生歧义,需要在 polls/urls.py 上添加应用的命名空间作为区分。

from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
path('<int:question_id>/results/', views.results, name='results'),
path('<int:question_id>/vote/', views.vote, name='vote'),
]

然后编辑 polls/templates/polls/index.html 文件,为每个 url 标签添加上面声明的 polls: 命名空间。

<li>
<a href="https://www.tuicool.com/articles/n2EJRzM/{% url 'polls:detail' question.id %}"
>{{ question.question_text }}</a
>
</li>

表单和通用视图

首先为投票详细页面 polls/detail.html 添加一个 <form> 表单。

<h1>{{ question.question_text }}</h1>
{% if error_message %}
<p><strong>{{ error_message }}</strong></p>
{% endif %}
<form action="https://www.tuicool.com/articles/n2EJRzM/{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %} {% for choice in question.choice_set.all %}
<input
type="radio"
name="choice"
id="choice{{ forloop.counter }}"
value="{{ choice.id }}"
/>
<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label
><br />
{% endfor %} <input type="submit" value="Vote" />
</form>

上面代码中,除了前端模板常用的 for 循环标签,还使用了 csrf_token 标签来防止跨站点请求伪造,建议所有针对内部 URL 的 POST 表单都应该使用它。另外,代码中的表达式 forloop.counter 用来指示 for 标签的执行次数。

接下来,创建一个 Django 视图来处理上面表单提交的数据,在上一步当中,我们在 polls/urls.py 里创建的 URLconf 如下:

from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
# ... ...
path('<int:question_id>/vote/', views.vote, name='vote'),
# ... ...
]

这里我们需要处理对应的 vote() 的函数,下面代码中通过 request.POST['choice'] 以字符串形式返回选择的 ChoiceID request.POST 的值永远是字符串 )。如果 request.POST['choice'] 中不存在 choice ,将会引发一个 KeyError , 下面代码通过异常检查机制来处理 KeyError ,如果 choice 不存在将会重新显示 Question 表单以及一个错误提示信息。选择并且投票成功之后, Choice 的得票数会自增 1 ,同时通过返回 HttpResponseRedirect 重定向到指定的 URL

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import Choice, Question
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# 重新显示问题的投票表单。
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "你没有进行选择!",
})
else:
selected_choice.votes += 1
selected_choice.save()
# 请求成功后总是返回一个HttpResponseRedirect,这样可以防止用户点击返回按钮后表单被再次发送。
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

注意: HttpResponseRedirect() 函数中使用了一个 reverse() 函数,该函数可以根据 URLconf 生成相应的 URL ,从而避免了在视图函数中硬编码,这里 reverse() 函数的返回结果为 '/polls/1/results/'

当完成对 Question 的投票操作之后, vote() 视图会将请求重定向到一个结果视图 results() ,继续修改 polls/views.py 中的 results() 函数:

from django.shortcuts import get_object_or_404, render
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})

然后添加视图对应的模板页面 polls/templates/polls/results.html

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>
{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes |
pluralize }}
</li>
{% endfor %}
</ul>
<a href="https://www.tuicool.com/articles/n2EJRzM/{% url 'polls:detail' question.id %}">再次投票?</a>

通用视图

正如前面一系列代码所展示的那样,根据 URL 中的参数从数据库中获取数据、载入模板文件然后返回渲染后的模板,这是 Web 开发当中常见的情况,Django 将这个过程抽象为一套 通用视图 的快捷方法,下面我们将基于 通用视图 来进行修改。

改进前的 URLconf:

from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
path('<int:question_id>/results/', views.results, name='results'),
path('<int:question_id>/vote/', views.vote, name='vote'),
]

改进后的 URLconf:

from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.IndexView.as_view(), name='index'),
# 注意:路径字符串中匹配模式的名称由<question_id>修改为<pk>。
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
path('<int:question_id>/vote/', views.vote, name='vote'),
]

改进前的 polls/views.py

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from .models import Choice, Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'latest_question_list': latest_question_list}
return render(request, 'polls/index.html', context)
def detail(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html', {'question': question})
def vote(request, question_id):
question = get_object_or_404(Question, pk=question_id)
try:
selected_choice = question.choice_set.get(pk=request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
# 重新显示问题的投票表单。
return render(request, 'polls/detail.html', {
'question': question,
'error_message': "你没有进行选择!",
})
else:
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

改进后的 polls/views.py

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Choice, Question
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
""" 返回最近发布的5个问题。 """
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
def vote(request, question_id):
''' vote()函数定义同上 '''

这份代码中,使用了 ListView显示一个对象的列表 )和 DetailView显示一个指定对象的详细信息 )两个通用视图。

通用视图 DetailView 期望从 URL 获取名为 pk 的主键值,所以上述代码将 question_id 改为 pk 。默认情况下 DetailView 会使用路径为 <app name>/<model name>_detail.html 的模板,通过 template_name 属性可以指定自定义的模板来进行渲染。

通用视图 ListView 同样使用 <app name>/<model name>_list.html 作为默认模板,因此代码中也通过 template_name 属性告诉 ListView 使用已经创建好了的 polls/index.html 作为模板。在前面章节当中,与视图模板一起使用的,还有一个包含有 questionlatest_question_list 变量的 context ,而 ListView 当中,提供了一个 context_object_name 快捷属性,可以显式指定当前需要使用的 Context 上下文变量是 latest_question_list

好了,到目前为止 问题投票应用 polls 的功能已经大功告成,接下来的章节会讲解一些附加功能以及相关的第三方扩展的使用。

测试

静态文件

Web 应用中的 CSS、图片、JavaScript 通常需要放置在一个静态目录当中,Django 通过 mysite/settings.py 中的 'django.contrib.staticfiles' 应用提供了相关支持,默认情况下,该应用会自动在应用目录( 比如 polls )下的 static 目录查询静态文件。在进行下一步操作之前,请先按照如下的目录结构来建立文件。

C:Workspacejunglemysitepollsstatic (master -> origin)
(venv) λ tree /f
C:.
└─polls
│ style.css
└─images
└─background.jpg

static/polls/style.css

li a {
color: red;
}
body {
background: url("images/background.jpg");
}

template/polls/index.html

<html>
<head>
{% load static %}
<link
rel="stylesheet"
type="text/css"
href="https://www.tuicool.com/articles/n2EJRzM/{% static 'polls/style.css' %}"
/>
</head>
<body>
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li>
<a href="https://www.tuicool.com/articles/n2EJRzM/{% url 'polls:detail' question.id %}"
>{{ question.question_text }}</a
>
</li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %} {% load static %}
</body>
</html>

自定义管理站点

如何编写可复用的 Web 应用

执行原生 SQL

Django 提供了 2 种执行原生查询的方式:一种是使用 Manager.raw() 执行查询并返回一个 Model 实例,另一种是完全避开 Model 层,直接执行自定义的 SQL 语句。

执行原生查询

Django 提供了 raw() 管理器方法来执行一个原生 SQL 语句,并且返回一个 django.db.models.query.RawQuerySet 实例,该实例可以像普通 QuerySet 一样进行迭代,从而提供 Model 对象的实例。

Manager.raw(raw_query, params=None, translations=None)

首先,编写一名称为 Person 的 Model:

class Person(models.Model):
first_name = models.CharField()
last_name = models.CharField()

然后,在该 Model 上调用 row() 方法执行 SQL 语句:

for person in Person.objects.raw('SELECT * FROM myapp_person'):
print(person)

也可以在 row() 方法中指定需要查询的列:

Person.objects.raw('SELECT id, first_name, last_name FROM myapp_person')

遇到 Model 属性名与列名不匹配的情况,可以使用 AS 子句手动进行映射:

Person.objects.raw('''SELECT pk    AS id,
first AS first_name,
last  AS last_name,
FROM myapp_person''')

也可以使用 raw() 方法的 translations 参数来完成映射工作,该参数是一个包含了数据库字段到 Model 属性映射的字典:

name_map = {'pk': 'id', 'first': 'first_name', 'last': 'last_name'}
Person.objects.raw('SELECT * FROM myapp_person', translations=name_map)

raw() 方法支持索引,如果只需要第 1 条结果,可以像下面这样编写代码:

first_person = Person.objects.raw('SELECT * FROM myapp_person')[0]

然而,索引和切片并不在数据库级别上执行,如果数据量较大,直接在 SQL 级别上进行限制查询将会获得更佳的性能:

first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1')[0]

row() 方法查询返回的 Person 对象将是 deferred 模型实例,这意味着查询中省略的字段将会按需加载,例如:

for person in Person.objects.raw('SELECT id, first_name FROM myapp_person'):
print(person.first_name,  # 由原始查询检索
person.last_name)   # 按需检索

由于 Django 使用主键来标识模型实例,原始查询中必须包含主键字段,忽略主键字段将会引发 InvalidQuery 异常。

假如在定义 Model 时添加了一个 birth_dat 属性来保存生日数据,那么就可以借用 PostgreSQL 提供的 age() 方法来根据出生日期计算年龄:

persons = Person.objects.raw('SELECT *, age(birth_date) AS age FROM myapp_person')
for person in persons:
print("%s 已经 %s 岁了!" % (person.first_name, person.age)) # Hank 已经 18 岁了!

实际开发环境下,可以通过 Func()表达式 来避免使用原始 SQL 计算注解。

如果需要使用带参数的查询,那么可以向 raw() 方法传递 params 参数:

name = 'Hank'
Person.objects.raw('SELECT * FROM myapp_person WHERE last_name = %s', [name])

params 参数可以是一个列表或者字典,相应的查询参数在 SQL 中的占位符可以是 %s 或者 %(key)s但是需要注意 SQLite 不支持字典参数,因此须将参数作为列表传递

不要在原始查询或占位符上使用 格式化字符串 ,因为这样会引发 SQL 注入漏洞。

直接运行自定义 SQL

如果认为 Manager.raw() 还不够灵活,或者不需要将查询结果映射到 Model ,那么可以通过 django.db.connection 去使用默认的数据库连接,例如调用 connection.cursor() 去获取指针对象,调用 cursor.execute(sql, [params]) 去执行 SQL,调用 cursor.fetchone()cursor.fetchall() 去返回结果集。

from django.db import connection
def my_custom_sql(self):
with connection.cursor() as cursor:
cursor.execute("UPDATE bar SET foo = 1 WHERE baz = %s", [self.baz])
cursor.execute("SELECT foo FROM bar WHERE baz = %s", [self.baz])
row = cursor.fetchone()
return row

为了避免数据库注入攻击,必须禁止在 %s 周围使用引号。

注意,如果需要在查询中包含文字百分比符号,那么传递参数时必须将重复书写它们为 %% :

cursor.execute("SELECT foo FROM bar WHERE baz = '30%'")
cursor.execute("SELECT foo FROM bar WHERE baz = '30%%' AND id = %s", [self.id])

在使用多个数据库的时候,可以使用 django.db.connections 去获得数据库的连接和游标, django.db.connections 是一个允许使用别名查询指定数据库连接的字典对象。

from django.db import connections
with connections['my_db_alias'].cursor() as cursor:
# 自定义代码

默认情况下,Python 数据库 API 会返回没有字段名称的列表,而非一个字典。

def dictfetchall(cursor):
"以字典方式从一个游标返回所有行"
columns = [col[0] for col in cursor.description]
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]

另一个选择是使用 Python 标准库中的 collections.namedtuple() ,一个 namedtuple 是一个类似元组的对象,可以通过属性去访问字段,同样支持索引与迭代,其结果是不可变的。

from collections import namedtuple
def namedtuplefetchall(cursor):
"以元组方式从一个游标返回所有行"
desc = cursor.description
nt_result = namedtuple('Result', [col[0] for col in desc])
return [nt_result(*row) for row in cursor.fetchall()]
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> cursor.fetchall()
((54360982, None), (54360880, None))
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> dictfetchall(cursor)
[{'parent_id': None, 'id': 54360982}, {'parent_id': None, 'id': 54360880}]
>>> cursor.execute("SELECT id, parent_id FROM test LIMIT 2");
>>> results = namedtuplefetchall(cursor)
>>> results
[Result(id=54360982, parent_id=None), Result(id=54360880, parent_id=None)]
>>> results[0].id
54360982
>>> results[0][0]
54360982

数据库连接与游标

Django 的 connectioncursor 实现了除事务处理外的大部份 Python 数据库 API。如果不熟悉 Python 数据库 API,需要注意 cursor.execute() 中的 SQL 语句使用了占位符 %s ,而非直接在 SQL 语句中添加参数,这种技术会在底层按需转译相应参数。

需要注意 Django 使用的占位符是 %s ,而非 Python 自带 SQLite 绑定的占位符 ?

使用游标作 cursor 为上下文管理器:

with connection.cursor() as c:
c.execute(...)

等同于:

c = connection.cursor()
try:
c.execute(...)
finally:
c.close()

存储过程调用

可以通过输入 params 序列或者 kparams 字典参数去调用具有特定名称的数据库存储过程( 仅有 Oracle 支持 kparams )。

CursorWrapper.callproc(procname, params=None, kparams=None)¶

例如对于 Oracle 中给定的存储过程:

CREATE PROCEDURE "TEST_PROCEDURE"(v_i INTEGER, v_text NVARCHAR2(10)) AS
p_i INTEGER;
p_text NVARCHAR2(10);
BEGIN
p_i := v_i;
p_text := v_text;
...
END;

那么在 Django 中可以这样调用它:

with connection.cursor() as cursor:
cursor.callproc('test_procedure', [1, 'test'])

Django REST framework

Django REST framework 是一款用来构建强大灵活 API 的工具包,支持 OAuth1a 和 OAuth2 身份验证策略,并且生成可以通过 Web 浏览器进行可视化访问的 API,以及同时支持 ORM 和非 ORM 两种数据源的序列化。

本文所使用的 Django REST framework 版本为 3.9.0,支持当前最新版的 Python3.7 以及 Django 2.1,并同时兼容 Python2.x 和 Django1.x;除此之外,Django REST framework 还支持如下可选的扩展包:

  • coreapi (1.32.0+) – Schema 生成支持。
  • Markdown (2.1.0+) – 为浏览器 API 添加 Markdown 支持。
  • django-filter (1.0.1+) – 过滤器支持。
  • django-crispy-forms – 针对过滤器的增强 HTML 显示。
  • django-guardian (1.1.1+) – Object 级别的权限支持。

接下下来,可以像下面这样安装可选的支持包以及 djangorestframework

pip install djangorestframework
pip install markdown       # 可选
pip install django-filter  # 可选

然后在站点设置里添加如下支持:

INSTALLED_APPS = (
...
'rest_framework',
)

如果尝试使用可浏览的 API,开发人员可能也需要添加 REST 框架的登入登出页面,添加如下代码到你的 urls.py 文件。

urlpatterns = [
...
url(r'^api-auth/', include('rest_framework.urls'))
]

快速开始

接下来我们建立一个简单的 API,用来查看和编辑 Django 管理站点当中默认的 usersgroups 数据模型。

项目设置

复用前面章节内容中建立的 jungle 项目脚手架,激活虚拟环境后,建立名为 tutorial 的 Django 项目,然后启动一个 quickstart 应用。

# 安装
> pip install djangorestframework
# 建立一个Django项目
> django-admin startproject tutorial
# 建立应用
> cd tutorial
> django-admin startapp quickstart
# 将模型同步至数据库
> python manage.py migrate
# 建立管理站点用户
> python manage.py createsuperuser --email admin@example.com --username admin

串行器

首先定义一个串行器( Serializer, /’siəriəlaiz/ ),然后建立一个名为 tutorial/quickstart/serializers.py 的模块用于接下来的数据展示。

from django.contrib.auth.models import User, Group
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ('url', 'name')

注意这段代码中,我们通过 HyperlinkedModelSerializer 建立了超链接关联,当然也可以选择主键或其它关联方式,但是对于 RESTful 设计而言超链接模式是一种较好的选择。

视图

这里我们最好是编写一些视图,打开 tutorial/quickstart/views.py 文件进行如下编辑:

from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from quickstart.serializers import UserSerializer, GroupSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
允许users被查看和编辑的API终点
"""
queryset = User.objects.all().order_by('-date_joined')
serializer_class = UserSerializer
class GroupViewSet(viewsets.ModelViewSet):
"""
允许groups被查看和编辑的API终点
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer

与其整合多个视图,不如将它们相似的行为整合到一个 ViewSets 类,当然如果需要也可以方便的将它们分离为多个单独的视图,但使用 ViewSets 可以保持视图逻辑更加清晰有条理。

URL

好了,这里让我们在 tutorial/urls.py 写下 API 的 URL:

from django.conf.urls import url, include
from rest_framework import routers
from quickstart import views
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
# 使用自动URL路由连接我们的API,并将登陆url包含至可浏览的API。
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

由于在前面代码中,我们使用了 ViewSets 代替 Views ,通过简单的将 viewSets 注册到 router 类,所以我们能够方便的为 API 生成 URL 配置。当然如果你觉得有必要,也可以继续使用传统的 Views 类并且显式注册 URL 配置。上面代码里,我们还将默认的登入登出页面整合为可浏览的 API,这是可选的,但是对于需要进行权限校验的场景又是必要的。

配置 settings.py

tutorial/settings.py 添加分页配置可以控制 API 返回对象的数量:

REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}

然后注册 'rest_framework'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至Django应用
]

测试 API

激动人心的时刻到来,我们可以通过 python manage.py runserver 启动服务然后访问 http://127.0.0.1:8000/ 测试 API:

C:Workspacejungletutorial (master -> origin)
(venv) λ python manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
January 08, 2019 - 17:34:15
Django version 2.1.5, using settings 'tutorial.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

微信扫一扫,分享到朋友圈

使用 Django2 快速开发 Web 服务

22日水星“合”金星 天宇上演“星空私语”

上一篇

车市,从来没有报复性消费

下一篇

你也可能喜欢

使用 Django2 快速开发 Web 服务

长按储存图像,分享给朋友