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 项目的步骤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 # 建立虚拟环境 C:\Workspace \django λ python -m venv venv # 激活虚拟环境 C :\Workspace \django λ .\venv \Scripts \activate.bat (venv ) λ # 安装Django C :\Workspace \django (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 :\Workspace \django (venv ) λ django -admin startproject mysite C :\Workspace \django (venv ) λ ls mysite / venv /# 进入新建的Django 项目,建立一个应用 C :\Workspace \django (venv ) λ cd mysite \ C :\Workspace \django \mysite (venv ) λ python manage.py startapp demo C :\Workspace \django \mysite (venv ) λ ls demo / manage.py * mysite /# 同步数据库 C :\Workspace \django \mysite (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:48Django 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 :\Workspace \django \mysite (venv ) λ cd .. C :\Workspace \django (venv ) λ pip freeze > requirements.txt C :\Workspace \django (venv ) λ ls mysite / requirements.txt venv /
通过django-admin startproject
命令创建的外部mysite/
目录是
Web 项目的容器,而manage.py
文件是用于与 Django
项目交互的命令行工具,更多的使用方式可以参阅django-admin
文档 。。
1 2 3 4 5 6 7 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/
目录下的文件结构如下:
1 2 3 4 5 6 7 8 9 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
显式的声明当前允许的主机地址:
1 ALLOWED_HOSTS = ['10.102.16.79' ]
然后使用manage.py
启动服务时,指定好主机名和对应的端口号:
1 2 3 4 5 6 7 8 9 C:\Workspace \cloud -key \mysite (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:48Django 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
目录后,执行如下命令:
1 2 C:\Workspace \django \mysite (master -> origin ) (venv ) λ python manage.py startapp polls
polls/views.py
新建一个polls
应用之后,打开该目录下的polls/views.py
源码文件,输入以下代码:
1 2 3 4 from django.http import HttpResponsedef index (request ): return HttpResponse("你好,这是一个投票应用!" )
polls/urls.py
接下来,我们需要将上面修改的视图文件views.py
映射到一个
URL,先在polls/
目录下新建一个urls.py
文件,然后键入下面这段代码:
1 2 3 4 5 6 7 from django.urls import pathfrom . import viewsurlpatterns = [ path('' , views.index, name='index' ), ]
mysite/urls.py
最后,将上面定义的应用的 URL
声明文件polls/urls.py
模块包含至项目的mysite/urls.py
代码当中,
1 2 3 4 5 6 7 from django.contrib import adminfrom django.urls import include, pathurlpatterns = [ 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 的每个属性则相当于一个数据库字段。
1 2 3 4 5 6 from django.db import modelsclass Blog (models.Model): title = models.CharField(max_length=50 ) content = models.CharField(max_length=800 )
Model 实例 :Model
类的实例用来表示数据库表中的一条特定记录,可以通过向 Model()
类传递关键字参数,然后调用save()
方法保存至数据库,从而创建出一个
Model 类实例。
1 2 3 4 from blog.models import Blogblog = Blog(title='Bit by bit' , content='一些内容。' ) blog.save()
QuerySet : QuerySet
表示数据库查询的结果集(SELECT
语句 ),该结果集拥有一个或多个过滤方法(WHERE 或 LIMIT
子句 )。获取结果集可通过 Model 的 Manager 管理器(用于向 Django
模型提供数据库查询操作的接口 ),而Manager
则默认由结果集的objects
属性获得。
1 2 Blog.objects Blog.objects.all ()
Django
当中,QuerySet
用来执行记录级操作 ,Model
实例则用来进行表级操作 。
mysite/settings.py
mysite/settings.py
文件包含了项目的基本配置,该文件通过如下声明默认使用
Django 内置的 SQLite 作为项目数据库。
1 2 3 4 5 6 DATABASES = { 'default' : { 'ENGINE' : 'django.db.backends.sqlite3' , 'NAME' : os.path.join(BASE_DIR, 'db.sqlite3' ), } }
如果使用其它数据库,则可以将配置书写为下面的格式:
1 2 3 4 5 6 7 8 9 10 DATABASES = { 'default' : { 'ENGINE' : 'django.db.backends.mysql' , 'NAME' : 'db' , 'USER' : 'uinika' , 'PASSWORD' : 'test' , 'HOST' : 'localhost' , 'PORT' : '3306' , } }
其中ENGINE
属性可以根据项目所使用数据库的不同而选择如下值:
SQLite:django.db.backends.sqlite3
MySQL:django.db.backends.mysql
PostgreSQL:django.db.backends.postgresql
Oracle:django.db.backends.oracle
接下来继续修改mysite/settings.py
,设置TIME_ZONE
属性为项目使用国家的时区。
1 2 LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Chongqing'
mysite/settings.py
文件头部的INSTALLED_APPS
属性定义了当前项目使用的应用程序。
1 2 3 4 5 6 7 8 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(选项)
两个数据模型:
1 2 3 4 5 6 7 8 9 10 from django.db import modelsclass 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
属性:
1 2 3 4 5 6 7 8 9 INSTALLED_APPS = [ 'polls.apps.PollsConfig' , 'django.contrib.admin' , 'django.contrib.auth' , 'django.contrib.contenttypes' , 'django.contrib.sessions' , 'django.contrib.messages' , 'django.contrib.staticfiles' , ]
将数据模型迁移至数据库
通过manage.py
提供的makemigrations
命令,可以将模型的更新迁移至
SQLite 数据库当中。
1 2 3 4 5 6 7 C:\Workspace \django \mysite (master -> origin ) (venv ) λ python manage.py makemigrations polls Migrations for 'polls ': polls \migrations \0001_initial.py - Create model Choice - Create model Question - Add field question to choice
我们还可以通过manage.py
提供的sqlmigrate
命令,查看数据迁移过程中执行了哪些
SQL 语句,该命令并不会实质性执行 Django 模型到数据库的迁移任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 C:\Workspace \django \mysite (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
命令,在根据新定义的模型创建相应的数据库表。
1 2 3 4 5 6 C:\Workspace\django\mysite (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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 C:\Workspace \django \mysite (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()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import datetimefrom django.db import modelsfrom django.utils import timezoneclass Question (models.Model): question_text = models.CharField(max_length=200 ) pub_date = models.DateTimeField('date published' ) def was_published_recently (self ): return self .pub_date >= timezone.now() - datetime.timedelta(days=1 ) 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 ) def __str__ (self ): return self .choice_text
完成修改工作之后,再一次运行python manage.py shell
命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 C:\Workspace \django \mysite (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 :\Workspace \django \venv \lib \site -packages \django \db \models \manager.py ", line 82, in manager_method return getattr (self.get_queryset (), name )(*args , **kwargs ) File "C :\Workspace \django \venv \lib \site -packages \django \db \models \query.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
命令创建一个管理用户:
1 2 3 4 5 6 7 C:\Workspace \django \mysite (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
并使用上面新建的用户名和密码进行登陆管理操作:
登陆后默认只能对权限相关的User
和Group
进行管理,如果我们需要将Question
数据模型纳入管理,那么必须要在polls/admin.py
文件对其进行注册。
1 2 3 4 from django.contrib import adminfrom .models import Questionadmin.site.register(Question)
完成注册之后,刷新管理站点页面即可查看到Question
管理选项:
视图与模板
polls/views.py
Django 使用URLconfs
配置将 URL 与视图关联,即将 URL
映射至视图,下面我们将向polls/views.py
文件添加一些能够接收参数的视图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from django.http import HttpResponsedef 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
模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 from django.urls import pathfrom . import viewsurlpatterns = [ 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/views.py
Django
的每个视图只会完成两个任务:返回一个包含被请求页面内容的HttpResponse
对象,或是抛出一个Http404
这样的异常。这里为了展示数据库里按照发布日期排序的最近五个投票问题,我们再向polls/views.py
代码文件的index()
函数添加如下内容:
1 2 3 4 5 6 from .models import Questiondef 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.py
里TEMPLATES
属性的默认设置,能够让
Django
在每个INSTALLED_APPS
文件夹中自动寻找templates
子目录,从而正确定位出模板的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
模板当中完成数据绑定。
1 2 3 4 5 6 7 8 9 10 11 from django.http import HttpResponsefrom django.template import loaderfrom .models import Questiondef 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
对象这一系列步骤:
1 2 3 4 5 6 7 from django.shortcuts import renderfrom .models import Questiondef 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
添加如下代码,
1 2 3 4 5 6 7 8 9 10 from django.http import Http404from django.shortcuts import renderfrom .models import Questiondef 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
添加一行简单的{{ question }}
代码便于测试上面的代码。
Django
提供了诸如get_object_or_404()
、get_list_or_404()
这样的快捷函数语法糖来解决Http404
判断的问题,因而上一步的代码依然可以进一步简化为下面这样:
1 2 3 4 5 6 from django.shortcuts import get_object_or_404, renderfrom .models import Questiondef 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
,填充完整的视图代码:
1 2 3 4 5 6 <h1 > {{ question.question_text }}</h1 > <ul > {% for choice in question.choice_set.all %} <li > {{ choice.choice_text }}</li > {% endfor %} </ul >
通过在模板代码中使用.
符号来访问变量属性,例如对于上面代码中的{{ question.question_text }}
,
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="/polls/{{ question.id }}/">{{ question.question_text }}</a>
这样的硬编码,但是这样容易造成视图与后端业务的耦合,因此
Django 提供了url
标签来解决这个问题。
1 <a href ="{% 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
上添加应用的命名空间作为区分。
1 2 3 4 5 6 7 8 9 10 from django.urls import pathfrom . import viewsapp_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:
命名空间。
1 2 3 4 5 <li > <a href ="{% url 'polls:detail' question.id %}" > {{ question.question_text }}</a > </li >
表单和通用视图
首先为投票详细页面polls/detail.html
添加一个<form>
表单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <h1 > {{ question.question_text }}</h1 > {% if error_message %} <p > <strong > {{ error_message }}</strong > </p > {% endif %} <form action ="{% 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
如下:
1 2 3 4 5 6 7 8 9 10 from django.urls import pathfrom . import viewsapp_name = 'polls' urlpatterns = [ path('<int:question_id>/vote/' , views.vote, name='vote' ), ]
这里我们需要处理对应的vote()
的函数,下面代码中通过request.POST['choice']
以字符串形式返回选择的Choice
的ID
(request.POST
的值永远是字符串 )。如果request.POST['choice']
中不存在choice
,将会引发一个KeyError
,
下面代码通过异常检查机制来处理KeyError
,如果choice
不存在将会重新显示Question
表单以及一个错误提示信息。选择并且投票成功之后,Choice
的得票数会自增1
,同时通过返回HttpResponseRedirect
重定向到指定的URL
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from django.shortcuts import get_object_or_404, renderfrom django.http import HttpResponse, HttpResponseRedirectfrom django.urls import reversefrom .models import Choice, Questiondef 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 ,)))
注意:HttpResponseRedirect()
函数中使用了一个reverse()
函数,该函数可以根据URLconf
生成相应的URL
,从而避免了在视图函数中硬编码,这里reverse()
函数的返回结果为'/polls/1/results/'
。
当完成对Question
的投票操作之后,vote()
视图会将请求重定向到一个结果视图results()
,继续修改polls/views.py
中的results()
函数:
1 2 3 4 5 from django.shortcuts import get_object_or_404, renderdef 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
:
1 2 3 4 5 6 7 8 9 10 11 12 <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 ="{% url 'polls:detail' question.id %}" > 再次投票?</a >
通用视图
正如前面一系列代码所展示的那样,根据 URL
中的参数从数据库中获取数据、载入模板文件然后返回渲染后的模板,这是 Web
开发当中常见的情况,Django
将这个过程抽象为一套通用视图 的快捷方法,下面我们将基于通用视图 来进行修改。
改进前的 URLconf:
1 2 3 4 5 6 7 8 9 10 11 from django.urls import pathfrom . import viewsapp_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:
1 2 3 4 5 6 7 8 9 10 11 12 from django.urls import pathfrom . import viewsapp_name = 'polls' urlpatterns = [ path('' , views.IndexView.as_view(), name='index' ), 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 from django.shortcuts import get_object_or_404, renderfrom django.http import HttpResponse, HttpResponseRedirectfrom django.urls import reversefrom .models import Choice, Questiondef 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from django.http import HttpResponseRedirectfrom django.shortcuts import get_object_or_404, renderfrom django.urls import reversefrom django.views import genericfrom .models import Choice, Questionclass 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
作为模板。在前面章节当中,与视图模板一起使用的,还有一个包含有question
和latest_question_list
变量的context
,而ListView
当中,提供了一个context_object_name
快捷属性,可以显式指定当前需要使用的
Context 上下文变量是latest_question_list
。
好了,到目前为止问题投票应用
polls 的功能已经大功告成,接下来的章节会讲解一些附加功能以及相关的第三方扩展的使用。
测试
静态文件
Web 应用中的 CSS、图片、JavaScript
通常需要放置在一个静态目录当中,Django
通过mysite/settings.py
中的'django.contrib.staticfiles'
应用提供了相关支持,默认情况下,该应用会自动在应用目录(比如polls
)下的static
目录查询静态文件。在进行下一步操作之前,请先按照如下的目录结构来建立文件。
1 2 3 4 5 6 7 8 C:\Workspace \jungle \mysite \polls \static (master -> origin ) (venv ) λ tree /f C :.└─polls │ style.css └─images └─background.jpg
static/polls/style.css
1 2 3 4 5 6 7 li a { color : red; } body { background : url ("images/background.jpg" ); }
template/polls/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <html > <head > {% load static %} <link rel ="stylesheet" type ="text/css" href ="{% static 'polls/style.css' %}" /> </head > <body > {% if latest_question_list %} <ul > {% for question in latest_question_list %} <li > <a href ="{% 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
对象的实例。
1 Manager.raw(raw_query, params=None , translations=None )
首先,编写一名称为Person
的 Model:
1 2 3 class Person (models.Model): first_name = models.CharField() last_name = models.CharField()
然后,在该 Model 上调用row()
方法执行 SQL 语句:
1 2 for person in Person.objects.raw('SELECT * FROM myapp_person' ): print (person)
也可以在row()
方法中指定需要查询的列:
1 Person.objects.raw('SELECT id, first_name, last_name FROM myapp_person' )
遇到 Model
属性名与列名不匹配的情况,可以使用AS
子句手动进行映射:
1 2 3 4 Person.objects.raw('''SELECT pk AS id, first AS first_name, last AS last_name, FROM myapp_person''' )
也可以使用raw()
方法的translations
参数来完成映射工作,该参数是一个包含了数据库字段到
Model 属性映射的字典:
1 2 name_map = {'pk' : 'id' , 'first' : 'first_name' , 'last' : 'last_name' } Person.objects.raw('SELECT * FROM myapp_person' , translations=name_map)
raw()
方法支持索引,如果只需要第 1
条结果,可以像下面这样编写代码:
1 first_person = Person.objects.raw('SELECT * FROM myapp_person' )[0 ]
然而,索引和切片并不在数据库级别上执行,如果数据量较大,直接在 SQL
级别上进行限制查询将会获得更佳的性能:
1 first_person = Person.objects.raw('SELECT * FROM myapp_person LIMIT 1' )[0 ]
row()
方法查询返回的Person
对象将是deferred
模型实例,这意味着查询中省略的字段将会按需加载,例如:
1 2 3 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()
方法来根据出生日期计算年龄:
1 2 3 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))
实际开发环境下,可以通过Func()表达式
来避免使用原始
SQL 计算注解。
如果需要使用带参数的查询,那么可以向raw()
方法传递params
参数:
1 2 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()
去返回结果集。
1 2 3 4 5 6 7 8 9 from django.db import connectiondef 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
周围使用引号。
注意,如果需要在查询中包含文字百分比符号,那么传递参数时必须将重复书写它们为%%
:
1 2 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
是一个允许使用别名查询指定数据库连接的字典对象。
1 2 3 from django.db import connectionswith connections['my_db_alias' ].cursor() as cursor:
默认情况下,Python 数据库 API
会返回没有字段名称的列表,而非一个字典。
1 2 3 4 5 6 7 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
是一个类似元组的对象,可以通过属性去访问字段,同样支持索引与迭代,其结果是不可变的。
1 2 3 4 5 6 7 from collections import namedtupledef namedtuplefetchall (cursor ): "以元组方式从一个游标返回所有行" desc = cursor.description nt_result = namedtuple('Result' , [col[0 ] for col in desc]) return [nt_result(*row) for row in cursor.fetchall()]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 >>> 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
的connection
和cursor
实现了除事务处理外的大部份
Python 数据库 API。如果不熟悉 Python 数据库
API,需要注意cursor.execute()
中的 SQL
语句使用了占位符%s
,而非直接在 SQL
语句中添加参数,这种技术会在底层按需转译相应参数。
需要注意 Django 使用的占位符是%s
,而非 Python 自带
SQLite 绑定的占位符?
。
使用游标作cursor
为上下文管理器:
1 2 with connection.cursor() as c: c.execute(...)
等同于:
1 2 3 4 5 c = connection.cursor () try : c.execute (...) finally : c.close ()
存储过程调用
可以通过输入params
序列或者kparams
字典参数去调用具有特定名称的数据库存储过程(仅有
Oracle 支持kparams
)。
1 CursorWrapper.callproc(procname, params=None , kparams=None )¶
例如对于 Oracle 中给定的存储过程:
1 2 3 4 5 6 7 8 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 中可以这样调用它:
1 2 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
:
1 2 3 pip install djangorestframework pip install markdown pip install django-filter
然后在站点设置里添加如下支持:
1 2 3 4 INSTALLED_APPS = ( ... 'rest_framework' , )
如果尝试使用可浏览的 API,开发人员可能也需要添加 REST
框架的登入登出页面,添加如下代码到你的urls.py
文件。
1 2 3 4 urlpatterns = [ ... url(r'^api-auth/' , include('rest_framework.urls' )) ]
快速开始
接下来我们建立一个简单的 API,用来查看和编辑 Django
管理站点当中默认的users
和groups
数据模型。
项目设置
复用前面章节内容中建立的jungle 项目脚手架,激活虚拟环境后,建立名为tutorial
的
Django 项目,然后启动一个quickstart
应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 安装 > 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
的模块用于接下来的数据展示。
1 2 3 4 5 6 7 8 9 10 11 12 from django.contrib.auth.models import User, Groupfrom rest_framework import serializersclass 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
文件进行如下编辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from django.contrib.auth.models import User, Groupfrom rest_framework import viewsetsfrom quickstart.serializers import UserSerializer, GroupSerializerclass 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 from django.conf.urls import url, includefrom rest_framework import routersfrom quickstart import viewsrouter = routers.DefaultRouter() router.register(r'users' , views.UserViewSet) router.register(r'groups' , views.GroupViewSet) 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
返回对象的数量:
1 2 3 4 REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS' : 'rest_framework.pagination.PageNumberPagination' , 'PAGE_SIZE' : 10 }
然后注册'rest_framework'
到INSTALLED_APPS
属性当中:
1 2 3 4 5 6 7 8 9 INSTALLED_APPS = [ 'django.contrib.admin' , 'django.contrib.auth' , 'django.contrib.contenttypes' , 'django.contrib.sessions' , 'django.contrib.messages' , 'django.contrib.staticfiles' , 'rest_framework' , ]
测试 API
激动人心的时刻到来,我们可以通过python manage.py runserver
启动服务然后访问http://127.0.0.1:8000/
测试
API:
1 2 3 4 5 6 7 8 9 C:\Workspace \jungle \tutorial (master -> origin ) (venv ) λ python manage.py runserver Performing system checks ...System check identified no issues (0 silenced ).January 08, 2019 - 17:34:15Django version 2.1.5, using settings 'tutorial.settings 'Starting development server at http ://127.0.0.1:8000/Quit the server with CTRL -BREAK .