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 .