Django入门

2,843 阅读12分钟

Django是一个高级的Python Web框架,它支持快速开发和简洁实用的设计。这篇文章是看了Django官方文档并进行练习之后总结的笔记,主要总结入门需要了解的几个知识点:

  • 使用Django创建项目
  • 路径匹配,一个请求路径是如何映射到对应的回调函数。
  • Model,用于以面向对象的方式来操作数据库。
  • View,接收一个Web请求,然后返回一个Web响应。
  • 单元测试,单元测试单独写了一篇文:Django单元测试

比较常用的一些链接放在这里:字段查询

使用Django创建项目

1.准备工作

安装Python和使用MySQL数据库。

这部分可以查看之前写的文章Python入门MySQL入门。在这里不再赘述。

不同Django版本可以使用的对应的Python版本

数据库安装(包含除MySQL外的其他数据库)

2.安装Django

先创建一个虚拟环境并切换到该虚拟环境中,这样保证将Django安装在该虚拟环境中。

mkvirtualenv demo_env

安装正式发布的版本:

pip3 install Django

使用以下指令可以看到下载的Django的版本:

python3 -m django --version

(这里下载的是3.2.7版本的Django)

3.创建项目

cd到你想存放代码的位置,执行以下指令:

django-admin startproject demo

就会自动生成一些Django项目需要的代码。

django-admin是一个Django的管理任务的命名行应用程序。

manage.py是每一个Django项目自动创建的文件,它和django-admin一样也是管理任务用的,但是manage.py还会设置 DJANGO_SETTINGS_MODULE 环境变量,这个环境变量指向项目的settings.py文件,告诉Django你需要使用哪些设置。

官网详情: django-admin and manage.pysettings

4.设置数据库

(1)安装数据库API驱动mysqlclient

pip3 install mysqlclient

(2)在数据库服务中创建名为demo的数据库

create database demo;

(3)通过demo.settings.py文件中的DATABASES设置数据库:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'demo',
        'USER': 'root',
        'PASSWORD': 'secret',
        'HOST': '127.0.0.1',
        'PORT': '3306'
    }
}

官网详情:database setup

5.运行开发服务器

cd demo到创建的项目目录下,执行以下指令运行开发服务器:

python3 manage.py runserver

在浏览器中打开http://127.0.0.1:8000/能看到一个界面。

6.创建App

刚才我们执行django-admin startproject demo创建了一个名为demo的项目。一个项目中可能包含多个应用(App),一个应用可能在多个项目中。

manage.py文件路径下执行:

python3 manage.py startapp todo

来创建一个名为todo的应用。创建时完成后,在demo.settings文件的 INSTALLED_APPS 中,添加todo应用的信息,表明demo项目中包含todo应用:

INSTALLED_APPS = [
    ...
    'todo.apps.TodoConfig',
]

这个todo应用将会实现以下这些接口:

(1)创建/更新 待办事项

(2)获取待办事项详情

(3)获取待办事项列表

路径匹配

到目前为止,项目的文件目录是这样的:

.
├── db.sqlite3
├── demo
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-37.pyc
│   │   ├── settings.cpython-37.pyc
│   │   ├── urls.cpython-37.pyc
│   │   └── wsgi.cpython-37.pyc
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── todo
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    └── views.py

demo/settings.py文件中放的是项目的相关设置,settings.py文件中设置了ROOT_URLCONF

ROOT_URLCONF = 'demo.urls'

当网站的一个接口被请求的时候,Django会找到ROOT_URLCONF设置的模块中名为urlpatterns的变量,并且按顺序转化模式。也就是说找到某种模式的接口要执行的Python代码(一个Python函数或者一个基于类的视图(View))。

1.不含参数的url

创建的todo应用文件夹下,已经有views.py文件,在views文件夹下添加以下内容:

from django.http import HttpResponse

def temp(request):
    return HttpResponse('<h1>演示url匹配用的临时代码</h1>')

这是一个简单返回一个 HttpResponse 对象的函数。在demo.urlsurlpatterns列表中,添加如下内容:

from django.urls import path
from todo import views as todo_views

urlpatterns = [
    path('temp/', todo_views.temp),
]

在浏览器中访问http://127.0.0.1:8000/temp/,就能看到返回的html字符。

当匹配到temp/这样的接口的时候,会调用todo_views.temp(request)request是一个HttpRequest对象,包含请求相关的信息。

2.含参数的url

修改temp函数如下:

def temp(request, **kwargs):
    return HttpResponse(f'<h1>演示url匹配用的临时代码</h1><p>传递的参数{str(kwargs)}</p>')

修改demo.urls中的urlpatterns如下:

from django.urls import path
from todo import views as todo_views

urlpatterns = [
    path('temp/<int:temp_id>/', todo_views.temp),
]

在浏览器中访问http://127.0.0.1:8000/temp/123/,能看到:

图-1

<int:temp_id>中,int是转换器类型,表明匹配的这部分是整型。Django自带的转换器有str(没有设置转换器时的默认类型)、intsluguuidpath。也可以自定义转换器。

3. 命名URL模式

path() 或者 re_path() 中使用name参数能够命名URL模式,这样在单元测试的时候,就能方便地使用 reverse() 拿到对应的url。比如:

urlpatterns = [
    path('temp/<int:temp_id>/', todo_views.temp, name='index'),
]

在单元测试时,使用如下的方式测试该接口是否返回状态码200。

from django.urls import reverse

response = client.get(reverse('index'))
self.assertEqual(response.status_code, 200)

4. 调整结构

目前为止我们的todo应用的url模式都是写在项目的urls文件中的,如果有多个应用,都挤在这个文件中就不是很清晰。在todo文件夹下新建一个urls.py文件,添加如下内容:

from django.urls import path

from . import views

urlpatterns = [
    path('temp/<int:temp_id>/', views.temp, name='index'),
]

修改demo项目的urls文件如下:

from django.urls import path, include

urlpatterns = [
    path('todo/',include('todo.urls')),
]

在浏览器中访问http://127.0.0.1:8000/todo/temp/123/能看到 图-1 相同的页面。

5.URL名称命名空间

上文的path('temp/<int:temp_id>/', views.temp, name='index'),中,该URL模式的名称为index,如果我们在别的应用中也需要使用index作为名称,要么使用another-index命名避免冲突,要么使用命名空间:

...
app_name = 'todo'

urlpatterns = [
    path('temp/<int:temp_id>/', views.temp, name='index'),
]

使用reverse('todo:index')获取该匹配模式对应的路径。

官网详情:URL dispatcher

模型 (Model)

模型包含存储的数据的基本字段和行为,通常,一个模型对应一个数据库表。

1.创建Model

在创建模型之前,先要想好数据库的表结构。

from django.db import models

class Todo(models.Model):
    content = models.CharField('待办事项内容', max_length=200)
    remark = models.CharField('备注', max_length=500)
    PRIORITY_CHOICES = [
        (1, '低优先级'),
        (2, '中低优先级'),
        (3, '中优先级'),
        (4, '中高优先级'),
        (5, '高优先级'),
    ]
    priority = models.IntegerField('优先级', choices=PRIORITY_CHOICES, default=1)
    completed = models.BooleanField('是否已完成', default=False)
    created_time = models.DateTimeField('创建的时间', auto_now_add=True)
    modified_time = models.DateTimeField('最后一次更新的时间', auto_now=True)

字段类型

choices中,元组的第一个值是实际要赋给某字段的值,第二个值是便于阅读的内容。

(1) 执行以下指令,把对model的修改存储为migration(迁移)。

python3 manage.py makemigrations todo

会发现在应用todomigrations文件夹下面多出了一个0001_initial.py文件,这就是存储的迁移文件。

└── todo
    ├── __init__.py
    ...
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   ├── 0001_initial.py
    │   ├── __init__.py
    │   └── __pycache__
    ├── models.py

(2) 执行以下指令:

python3 manage.py sqlmigrate todo 0001

查看对model的修改对应的SQL语句(0001_initial.py迁移文件对应的SQL):

python3 manage.py sqlmigrate todo 0001
--
-- Create model Todo
--
CREATE TABLE `todo_todo` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `content` varchar(200) NOT NULL, `remark` varchar(500) NOT NULL, `priority` integer NOT NULL, `completed` bool NOT NULL, `created_time` datetime(6) NOT NULL, `modified_time` datetime(6) NOT NULL);

可以看到在models.py文件中我们并没有声明id字段,但是对应的SQL创建了id字段作为自增主键。也可以自己通过设置 primary_key=True 来设置某个字段作为主键。

CREATE TABLE todo_todo中可以看出,Django将应用名todomodel名称的小写结合起来作为表的名称,如果要自定义对应的表的名称,需要使用Meta选项中的db_table属性:

class Todo(models.Model):
    content = models.CharField('待办事项内容', max_length=200)
    ...
    modified_time = models.DateTimeField('最后一次更新的时间', auto_now=True)

    class Meta:
        db_table = 'todo'

这样修改后再执行一遍python3 manage.py makemigrations todo,新的修改被存储为了0002_alter_todo_table.py文件,执行python3 manage.py sqlmigrate todo 0002,就能看到如下内容:

--
-- Rename table for todo to todo
--
RENAME TABLE `todo_todo` TO `todo`;

(3) 执行migrate进行迁移,也就是将代码中对数据库的表述,应用到实际的数据库上:

python3 manage.py migrate

到数据库中查看,就会发现已经新创建了一张todo表。

2. 添加数据

当创建了数据模型之后,Django会自动给到一个数据库抽象的API,用于进行数据的增删改查。一个模型类表示一个数据库表,一个模型类实例代表一个数据库表中的记录。

使用指令python3 manage.py shell调起Python命令行,在Python命令行执行代码查看效果。

(1) 可以通过实例化一个模型类,然后调用 save() 将数据保存到数据库中:

>>> from todo.models import Todo
>>> todo = Todo(content='第一件事就是写文', remark='写文使人快乐', priority=3)
>>> todo.save()

在MySQL中查找数据:

use demo;
select * from todo limit 20;

发现已经出现了一条数据:

图-2

(2) 使用objectscreate() 方法。

这里先简单了解一下:为了从数据库中获取对象,需要使用模型类的一个 Manager 构造 QuerySet ,一个 QuerySet 代表从数据库的得到的对象的集合。每一个模型至少有一个Manager,并且默认情况下,它被称为objects

>>> from todo.models import Todo
>>> Todo.objects.create(content='第二件事就是打游戏', remark='打游戏同样使人快乐', priority=3)
<Todo: Todo object (2)>

在MySQL中使用select * from todo limit 20;查找数据,得到:

图-3

3.查找数据

(1) 使用 all() 方法查找所有的数据:

>>> Todo.objects.all()
<QuerySet [<Todo: Todo object (1)>, <Todo: Todo object (2)>]>

这里发现打印出来的内容不大直观,给模型类添加__str__方法:

class Todo(models.Model):
    content = models.CharField('待办事项内容', max_length=200)
    ...
    def __str__(self):
        return self.content

Ctrl + D退出Python命令行,再执行python3 manage.py shell重新调起,重新执行代码:

>>> from todo.models import Todo
>>> Todo.objects.all()
<QuerySet [<Todo: 第一件事就是写文>, <Todo: 第二件事就是打游戏>]>

(2) 使用filter返回包含匹配查询参数的对象的结果集:

>>> Todo.objects.filter(created_time__year=2021)
<QuerySet [<Todo: 第一件事就是写文>, <Todo: 第二件事就是打游戏>]>

查找创建时间为2021年的数据。

(3) 使用exclude返回一个不包含给定查询参数的结果集:

>>> Todo.objects.exclude(created_time__year=2021)
<QuerySet []>

查找创建时间不是2021年的数据。

(4) 使用 get() 方法返回匹配到的唯一的一个对象。

>>> Todo.objects.get(pk=1)
<Todo: 第一件事就是写文>

查找主键为1的数据。

(5) 限制查询结果集

>>> Todo.objects.all()[1:5]
<QuerySet [<Todo: 第二件事就是打游戏>]>

等同于OFFSET 1 LIMIT 5,返回从偏移位置1开始的前5条数据。

官网详情:字段查找

4.更新数据

(1) 更新一个对象,使用save()

>>> todo = Todo.objects.get(pk=1)
>>> todo.completed = True
>>> todo.save()
图-4

(2) 更新一个或者多个对象,使用update()

updateQuerySet的方法,只有结果集有update方法,因为get()拿到的是一个对象,所以是没有update方法的。

>>> Todo.objects.filter(pk=2).update(completed=True)
1

update返回的是匹配的行的数量。

图-5

5.删除数据

使用delete()删除QuerySet的所有行,返回的是删除的行的数量,以及包含删除的每个对象类型的数量信息的一个字典。

Todo.objects.filter(pk=2).delete()
(1, {'todo.Todo': 1})
图-6

官网详情:字段类型Model实例QuerySet API查询数据模型

视图 (View)

视图是一个Python函数,它接收一个Web请求,然后返回一个Web响应。这个响应可能是一个Web页面的HTML内容、一个重定向、一个404错误、或者一个XML文档,一个图片等。请求和响应的细节,可以看官网文档:Request and Response objects

可以使用templates(模板)动态生成HTML作为响应返回,但因为实际工作中前后端分离,基本上不会用到模板,所以这个练习中只是实现接口,在Postman中观察效果,不实现界面交互。

View 函数

上文的练习中我们已经写过如下代码:

from django.http import HttpResponse

def temp(request, **kwargs):
    return HttpResponse(f'<h1>演示url匹配用的临时代码</h1><p>传递的参数{str(kwargs)}</p>')

url模式是这样写的:

urlpatterns = [
    path('temp/<int:temp_id>/', views.temp, name='index'),
]

views.temp是url匹配上之后要调用的函数。

不论是什么方法的请求(GET、POST、PUT...),只要匹配到模式temp/<int:temp_id>/,都会调用views.temp函数,所以需要在方法内部进行一些处理,比如:

def temp(request, **kwargs):
    if request.method == 'GET':
        # GET 请求的处理
    elif request.method == 'POST':
        # POST 请求的处理

我们可以使用基于类的视图,基于类的视图会做好不同方法的组织,而不用我们手动写if ... elif

基于类的View

所有的基于类的View都继承自 View

class Temp(View):
    def get(self, request, **kwargs):
        return HttpResponse(f'<p>get请求,{str(kwargs)}</p>')
    def post(self, request, **kwargs):
        return HttpResponse(f'post请求,{str(kwargs)}')

这里的POST请求不能直接返回HttpResponse,否则会报错CSRF cookie not set。使用csrf_exempt装饰器将接口设置为不受CSRF保护(仅为了简化练习这样设置)。

todo.urls中的内容修改为:

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from .views import Temp

app_name = 'todo'

urlpatterns = [
    path('temp/<int:temp_id>/', csrf_exempt(Temp.as_view()), name='index'),
]

基于类的View包含一个as_view() 方法,返回匹配到url的时候要调用的回调函数。

在Postman中用GET请求访问http://127.0.0.1:8000/todo/temp/123/,得到:

图-7

用POST请求访问http://127.0.0.1:8000/todo/temp/123/,得到:

图-8

实现接口

用基于类的View实现以下接口:

(1)创建/更新 待办事项

(2)获取待办事项详情

(3)获取待办事项列表

from django.core import serializers
from django.http import HttpResponse, JsonResponse, QueryDict
from django.views import View
from .models import Todo
import json

class TodoView(View):
    '''清单
    
    post:创建清单;put:更新清单;get:获取清单详情
    '''
    def format_request_data(self, request_data):
        '''整理请求传递的数据'''
        todo_data = {}
        todo_data['content'] = request_data.get('content', '')
        todo_data['remark'] = request_data.get('remark', '')
        todo_data['priority'] = int(request_data.get('priority', 1))
        return todo_data
    
    def get(self, request, pk):
        try:
            todo = Todo.objects.filter(pk=pk)
            todo_list_str = serializers.serialize('json', todo) # 将QuerySet序列化为JSON数据
            todo_list = json.loads(todo_list_str)
            todo_data = todo_list[0]['fields']
            return JsonResponse(todo_data, status=200)
        except Exception as e:
            return HttpResponse(f'获取清单失败,{e.__str__()}', status=400)

    def post(self, request):
        try:
            data = QueryDict(request.body)
            todo_data = self.format_request_data(data)
            todo = Todo(**todo_data)
            todo.save()
            return HttpResponse('创建清单成功', status=201)
        except Exception as e:
            return HttpResponse(f'创建清单失败,{e.__str__()}', status=400)

    def put(self, request, pk):
        try:
            data = QueryDict(request.body)
            todo_data = self.format_request_data(data)
            Todo.objects.filter(pk=pk).update(**todo_data)
            return HttpResponse('更新清单成功', status=200)
        except Exception as e:
            return HttpResponse(f'更新清单失败,{e.__str__()}', status=400) 

class TodoListView(View):
    '''清单列表

    get:获取清单列表
    '''
    def get(self, request):
        try:
            todo_list = Todo.objects.all()
            todo_list_str = serializers.serialize('json', todo_list) # 将QuerySet序列化为JSON数据
            todo_list_data = json.loads(todo_list_str)
            res_data = []
            for todo in todo_list_data:
                todo_data = { **todo.get('fields') }
                todo_data.update(id=todo.get('pk'))
                res_data.append(todo_data)
            return JsonResponse(res_data, safe=False, status=200) # 设置safe为False是为了返回非字典类型的数据
        except Exception as e:
            return HttpResponse(f'获取清单列表失败,{e.__str__()}', status=400)

修改todo/urls.py文件如下:

from django.urls import path
from django.views.decorators.csrf import csrf_exempt

from .views import TodoView, TodoListView

app_name = 'todo'

urlpatterns = [
    path('create/', csrf_exempt(TodoView.as_view()), name='create'),
    path('edit/<int:pk>/', csrf_exempt(TodoView.as_view()), name='edit'),
    path('<int:pk>/', csrf_exempt(TodoView.as_view()), name='detail'),
    path('list/', csrf_exempt(TodoListView.as_view()), name='list'),
]

测试接口:http://127.0.0.1:8000/todo/create/http://127.0.0.1:8000/todo/edit/1/http://127.0.0.1:8000/todo/1/http://127.0.0.1:8000/todo/list/

在Postman中请求接口,在MySQL数据库中查看数据是否正确。这里的练习实现的是最简化的内容,参考即可,可以自行对代码进行完善。

简单地使用generic.ListView实现列表接口(和使用django.views.View实现的区别不大):

from django.core import serializers
from django.views import generic
from django.http import JsonResponse
from .models import Todo
import json

class TodoListView(generic.ListView):
    model = Todo

    def render_to_response(self, context):
            todo_list = context.get('object_list')
            todo_list_str = serializers.serialize('json', todo_list) # 将QuerySet序列化为JSON数据
            todo_list_data = json.loads(todo_list_str)
            res_data = []
            for todo in todo_list_data:
                todo_data = { **todo.get('fields') }
                todo_data.update(id=todo.get('pk'))
                res_data.append(todo_data)
            return JsonResponse(res_data, safe=False, status=200) # 设置safe为False是为了返回非字典类型的数据

官网详情:Built-in class-based views APICSRF便捷函数序列化Django对象

源码地址