Django详细笔记

django 学习

特点

  1. 快速开发
  2. 安全性高
  3. 可伸缩性强

URL 组成部分

URL: 同意资源定位符

一个URL由以下几部分组成

scheme://host:port/path/?query-string=xxx#anchor
  • scheme: 代表的是访问的协议,一般为http或https协议
  • host: 主机名,域名
  • port: 端口 http 默认:80端口 https 默认端口:443
  • path: 查找路径
  • query-string:查询字符串
  • anchor:锚点,后台一般不用管,前端用来做页面定位的

注意:url中的所有字符串都是ASCII字符集,如果非ASCII字符,比如中文,浏览器会进行编码再进行传输

1. Django开篇

MVT 与 MVC

Django采用了MVT的设计模式,即模型(Model),视图(View)和模板(Template)

MVT模型的工作流程

2. 使用

安装django

pip install django

2.1 创建第一个项目

django-admin startproject 项目名

2.2 启动django项目

  1. 进入项目目录

    cd 项目名

  2. 启动项目

    python3 manage.py runserver 0.0.0.0:8000
    或
    python manage.py runserver 0.0.0.0:8000
    

    0.0.0.0 让其他电脑可以链接到开发服务器,8000为端口号,如果不说明,默认端口为8000

  3. 成功页面

2.3 创建app

python   manage.py   startapp   appname

3. 路由控制器

在django中所有的路由最终都被保存到一个变量urlpatterns,urlpatterns必须声明在主应用下的urls.py总路由中,这些配置是由settings设置的。

在给urlpatterns路由列表添加路由的过程中,django提供了两个函数给开发者注册路由。

from django.urls import path      # 字符串路由
from django.urls import re_path   # 正则路由,会把url地址看成一个正则模式与客户端的请求url地址进行正则匹配
  • path:用于普通路径,不需要自己手动添加正则首位限制符号,底层已经添加。
  • re_path:用于正则路径,需要自己手动添加正则首位限制符号。
  1. 基本使用

    path(r'^articles/2003/$', views.special_case_2003),
    re_path(r'^articles/([0-9]{4})/$', views.year_archive),
    re_path(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
    re_path(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive2)
    

    对于从路径中获取参数,view视图函数中除了request要记得加上相应的位置/关键字参数用来接收。

    一些参数可以从路径中获取,一些参数可以在get post等请求中get出来。

  2. path函数

    path() 可以接收四个参数,两个必选参数:routeview 两个可选参数:kwargs name

    • route: 字符串,定义 URL 的路径部分。可以包含变量,例如 <int:my_variable>,以从 URL 中捕获参数并将其传递给视图函数。
    • view: 视图函数,处理与给定路由匹配的请求。可以是一个函数或一个基于类的视图。
    • kwargs(可选): 一个字典,包含传递给视图函数的额外关键字参数。
    • name(可选): 为 URL 路由指定一个唯一的名称,以便在代码的其他地方引用它。这对于在模板中生成 URL 或在代码中进行重定向等操作非常有用。 (即是说 可以根据名字 反向获得路由)
  3. 路由分发

    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
    
  4. 路由转发器

    有时候上面的内置配置的url转换器不能满足需求,因此django提供了一个接口定义了自己的url转发器

    from django.urls import register_converter
    from django.shortcuts import HttpResponse
    # 自定义路由转换器
    class MobileConverter(object):
        regex = "1[3-9]\d{9}"
        def to_python(self,value):
            print(type(value))
            # 将匹配结果传递到视图内部时使用
            # 返回str还是int主要看需求,纯数字的可以返回int
            return value
     
        def to_url(self,value):
            # 将匹配结果用于反向解析传值时使用
            return value
        
    # register_converter(路由转换器的类名,调用别名)
    register_converter(MobileConverter,"mobile")
    
    path("index/<mobile:mobile>",index)
    
    def index(request,mobile):
        print(":::",type(mobile))
        return HttpResponse(f"hi,{mobile}用户")
    
  5. url的命名与namespace

    • name

      假如我们url里的一条路径为publisher_list/,后续我们想将publisher_list改为publisher,因为假如我们在视图函数的重定向、前端模板的一些路径如a标签、form表单里的active等无论是以相对路径还是绝对路径的方式写出的url都是写死的状态,都需要再重新修改,而假如我们给路径起了个名字,每次通过名字去寻找路径,这样即使路径改变,如publisher_list改为publisher,其名字不变,各种跳转依旧可以执行(因为它是每次通过名字去寻找路径的)。

      (当然,该图片只是一个例子,像这种增删改查路径一样可以优化)

      (虽然前后端分离项目中我们大多数用ajax发送请求,但路径的命名依然重要)

    • namespace

      假如我们多个人开发一个项目,在这个项目中我写app01,你写app02,难免出现些起名字重名的意外,这种情况下再进行反向解析,只会根据settings.py里的app注册的顺序去开始寻找,找到第一个就返回了。

      因此,我们给每个app在进行路由分发时起一个‘大名字’,namespace,这样以后进行反向解析时,会先去找大名字再去找小名字。(小知识:manage.py里可以修改配置文件)

      (这里我只注册了一个app,下面图片会有飘黄,不用管,当作我创建了)

      这样,假如students和children里都有name='xx'的url时,可以根据需要,进行'namespace:name'

  6. 反向解析

    在需要URL 的地方,对于不同层级,Django 提供不同的工具用于URL 反查:

    • 在模板中:使用url模板标签
    • 在Python 代码中:使用from django.urls import reverse 函数。

    urls.py中为url设置别名参数:

    from django.conf.urls import url
    from . import views
     
    urlpatterns = [
        #...
        url(r'^articles/([0-9]{4})/$', views.year_archive, name='news-year-archive'),
        #...
    ]
    

    应用之在模板中反向解析:

    2012 Archive
    {#相当于#}
    2012 Archive
     
    2012 Archive
     
    {#也可以这种方式传参#}
    2012 Archive
    

    对于第四种因为a标签发的是get请求,其请求里的url可以通过request.GET.get('year')的形式获取出来,对于第一种,是/articles/2012/这样的,urls.py中记得要加一些如正则表达式的接收。

    在python中反向解析

    即根据 url 名字 反向获取url路径

    from  django.urls  import  reverse
    
    def  index(request):
        # 根据理由名称  得到url
        print(reverse("new-years-archie"))
        # 路由反转获取理由参数
        print(reverse("new-years-archie",kwargs={"book_id":1}))
        # 如果是查询字符串的方式传参只能通过拼接
        print(reverse("book_slug")+"?id=1")
    	# 反转获得app中的url
        print(reverse("movie:movie_list"))
        return  HttpResponse("ieo;fdjwqoief")
    

    args/kwargs传参,也相当于上面的路径拼接。

4. 视图

django的视图有两种:分别是函数视图(FBV)类视图(CBV)

  1. 请求对象

    • 请求方式

      print(request.method)

    • 请求数据

      1.HttpRequest.GET:一个类似于字典的对象,包含 HTTP GET 的所有参数。详情请参考 QueryDict 对象。

      2.HttpRequest.POST:一个类似于字典的对象,如果请求中包含表单数据,则将这些数据封装成 QueryDict 对象。

      注意:键值对的值是多个的时候,比如checkbox类型的input标签,select标签,需要用: request.POST.getlist("hobby")

      3.HttpRequest.body:一个字符串,代表请求报文的请求体的原数据。

    • 请求路径

      HttpRequest.path:表示请求的路径组件(不含get参数) HttpRequest.get_full_path():含参数路径
      
    • 请求头

      HttpRequest.META:一个标准的Python 字典,包含所有的HTTP 首部。具体的头部信息取决于客户端和服务器
      

      简单访问下,在地址栏输入127.0.0.1:8000/students/index (get请求)

      request.path_info 不包含域名端口 不包含路径参数 request.get_full_path 会携带参数

      当我们发送post请求时,如果form表单发送的默认数据的编码方式是URLencode,那么才可以被request.POST得到,否则POST理由但请求体里会有(byte类型) 。

  2. 响应对象

    响应对象的三种形式

    • HttpResponse()
    • render()
    • redirect()

    (1) HttpResponse()

        Django服务器接收到客户端发送过来的请求后,会将提交上来的这些数据封装成一个 HttpRequest 对象传给视图函数。那么视图函数在处理完相关的逻辑后,也需要返回一个响应给浏览器。而这个响应,我们必须返回 HttpResponseBase 或者他的子类的对象。而 HttpResponse 则是 HttpResponseBase 用得最多的子类。
    

    常用属性

    content:返回的内容。实例化HttpResponse('content')

    status:返回的HTTP响应状态码。200 404

    content_type:返回的数据的MIME类型,默认为 text/html 。浏览器会根据这个属性,来显示数据。如果是 text/html ,那么就会解析这个字符串,如果 text/plain ,那么就会显示一个纯文本。

    设置响应头: response['X-Access-Token'] = 'xxxx' 。

    def index(request):
     res =HttpResponse('<h1>ok</h1>',content_type='text/plain')
     res['add_head'] = 'xxxx'
     return res
    

    JsonResponse类:

    用来对象 dump 成 json 字符串,然后返回将 json 字符串封装成 Response 对象返回给浏览器。并且他的 Content-Type 是 application/json 。示例代码如下:

    from django.http import JsonResponse
    def index(request):
     return JsonResponse({"title":"三国演义","price":199})
    

    默认情况下 JsonResponse 只能对字典进行 dump ,如果想要对非字典的数据进行 dump ,那么需要给 JsonResponse 传递一个 safe=False 参数。示例代码如下:

      一般用来响应ajax请求,相比于HttpResponse(json.dumps(dict,ensure_ascii=Fasle)),这个会自动序列化并且设置content_type=application/json,ajax会自动反序列化为JS的object类型。
    

    (2) render()

    def index(request):
     return render(request,'index.html',{'xx':xx})#locals()
    

    参数:

    /*
    request: 用于生成响应的请求对象。
    template_name:要使用的模板的完整名称,可选的参数
    context:添加到模板上下文的一个字典,
           默认是一个空字典。如果字典中的某个值是可调用的,视图将在渲染模板之前调用它。
           */ 
    

    render方法就是将一个模板页面中的模板语法进行渲染,最终渲染成一个html页面作为响应体。

    注意templates里的html路径是在settings.py里设置的,要改templates名或者选择html时要注意补全。
    

    render函数内部本质

    from django.shortcuts import render
    from django.template.loader import get_template
    from django.http.response import HttpResponse
    def index(request):
    name = "hello world!"
    # 1. 初始化模板,读取模板内容,实例化模板对象
     # get_template会从项目配置中找到模板目录,我们需要填写的参数就是补全模板文件的路径
    template = get_template("index.html")
    # 2. 识别context内容, 和模板内容里面的标记[标签]替换,针对复杂的内容,进行正则的替换
    context = {"name": name}
    content = template.render(context, request) # render中完成了变量替换成变量值的过程,这个过程使用了正则。
    print(content)
    # 3. 通过response响应对象,把替换了数据的模板内容返回给客户端
    return HttpResponse(content)
    # 上面代码的简写,直接使用 django.shortcuts.render
    # return render(request, "index.html",context={"name":name})
     # return render(request,"index3.html", locals())
     # data = {}
     # data["name"] = "xiaoming"
     # data["message"] = "你好!"
     # return render(request,"index3.html", data)
    

    (3) redirect()方法

    您在某些时候必须将用户从一个URL重定向到另一个URL。

    通过redirect方法实现重定向。

    参数可以是:

    • 一个绝对的或相对的URL, 将原封不动的作为重定向的位置.
    • 一个url的别名: 可以使用reverse来反向解析url
    #传递要重定向到的一个具体的网址
    def my_view(request):
     ...
     return redirect("/some/url/")
    #当然也可以是一个完整的网址
    def my_view(request):
     ...
     return redirect("http://www.baidu.com")
    #传递一个视图的名称
    def my_view(request):
     ...
     return redirect(reverse("namespace:name")) 
    

    浏览器发起请求->服务器响应重定向一个新地址->浏览器发送响应到新地址->服务器返回响应。

    响应头里有Location:'地址'

    再往上去他俩父亲继承 HttpResponse

    用HttpResponse简单实现

    def red(request):
     ret = HttpResponse('', status=301)
     ret['Location'] = '/students/index/'
    

5. 模板

忽略 (前后端分离项目 忽略)

3、静态文件
开发中在开启了debug模式时,django可以通过配置,允许用户通过对应的url地址访问django的静态文件。

setting.py,代码:

STATIC_ROOT = BASE_DIR / 'static'

STATIC_URL = '/static/' # django模板中,可以引用{{STATIC_URL}}变量避免把路径写死。
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
os.path.join(BASE_DIR, 'static2'),
os.path.join(BASE_DIR, 'static3'),
]
从上往下依次查找

像前端后面写的这些路径

这里的/static/虽然写的时候就会提示static文件名,但实际上它的名字要和settings.py里的STATIC_URL一致。同样STATICFILES_DIRS对应的名字则为文件夹名。

6. 模型层 (ORM)

django中内嵌了ORM框架,不需要直接编写SQL语句进行数据库操作,二十通过定义模型类,操作模型类来完成数据库中表的增删改查和创建等操作。其内部是通过pymysql完成的,发送一些mysql语句

通过以下步骤来使用django的数据库操作

1. 配置数据库链接信息
2. 在model.py中定义模型类
3. 生成数据库迁移文件并执行迁移文件
4. 通过模型类对象提供的方法或属性完成数据表的增删改查操作

6.1 配置数据库链接

在settings.py中保存了数据库的链接配置信息,Django默认初始化配置使用sqlite数据库。

  1. 使用msql数据库首先需要安装驱动

    pip install PyMySQL

  2. 在Django的工程同名的子目录的__init__.py文件中添加如下语句

    from pymysql import install_as_MySQLdb
    install_as_MySQLdb() # 让pymysql以MySQLDB的运行模式和Django的ORM对接运行

    作用是让django的ORM能以mysqldb的方式来调用pymysql

  3. 修改databases配置信息

    DATABASES = {
    'default': {
    'ENGINE': 'django.db.backends.mysql',
    'HOST': '127.0.0.1', # 数据库主机
    'PORT': 3306, # 数据库端口
    'USER': 'root', # 数据库用户名
    'PASSWORD': '123', # 数据库用户密码
    'NAME': 'student' # 数据库名字
    }
    }

  4. 在mysql中创建数据库

    create database student; # mysql8.0默认就是utf8mb4;
    create database student default charset=utf8mb4; # mysql8.0之前的版本

  5. 注意 如果想打印orm转换过程中的sql,需要在settings中进行如下配置:

    LOGGING = {
        'version': 1,
        'disable_existing_loggers': False,
        'handlers': {
            'console':{
                'level':'DEBUG',
                'class':'logging.StreamHandler',
            },
        },
        'loggers': {
            'django.db.backends': {
                'handlers': ['console'],
                'propagate': True,
                'level':'DEBUG',
            },
        }
    }  
    

6.2 定义模型类

  • 模型类被定义在“子应用/models.py”文件中
  • 模型必须直接或者间接继承自django.db.models.Model类。

接下来以学生管理为例进行演示。[系统大概3-4表,学生信息,课程信息,老师信息]

在models.py 文件中定义模型类。

from django.db import models
from datetime import datetime

# 模型类必须要直接或者间接继承于 models.Model
class BaseModel(models.Model):
 """公共模型[公共方法和公共字段]"""
 # created_time = models.IntegerField(default=0, verbose_name="创建时间")
 created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
 # auto_now_add 当数据添加时设置当前时间为默认值
 # auto_now= 当数据添加/更新时, 设置当前时间为默认值
 updated_time = models.DateTimeField(auto_now=True)
 class Meta(object):
 	abstract = True # 设置当前模型为抽象模型, 当系统运行时, 不会认为这是一个数据表对应的模型.

class Student(BaseModel):
 """Student模型类"""
 #1. 字段[数据库表字段对应]
 SEX_CHOICES = (
 	(0,"女"),
 	(1,"男"),
 	(2,"保密"),
 )

 # 字段名 = models.数据类型(约束选项1,约束选项2, verbose_name="注释")
 # SQL: id bigint primary_key auto_increment not null comment="主键",
 # id = models.AutoField(primary_key=True, null=False, verbose_name="主键") # django会自动在创建数据表的时候生成id主键/还设置了一个调用别名 pk

 # SQL: name varchar(20) not null comment="姓名"
 # SQL: key(name),
 name = models.CharField(max_length=20, db_index=True, verbose_name="姓名" )

 # SQL: age smallint not null comment="年龄"
 age = models.SmallIntegerField(verbose_name="年龄")

 # SQL: sex tinyint not null comment="性别"
 # sex = models.BooleanField(verbose_name="性别")
 sex = models.SmallIntegerField(choices=SEX_CHOICES, default=2)

 # SQL: class varchar(5) not null comment="班级"
 # SQL: key(class)
 classmate = models.CharField(db_column="class", max_length=5, db_index=True, verbose_name="班级")
 # SQL: description longtext default "" not null comment="个性签名"
 description = models.TextField(default="", verbose_name="个性签名")

 #2. 数据表结构信息
 class Meta:
 	db_table = 'tb_student'  # 指明数据库表名,如果没有指定表明,则默认为子应用目录名_模型名称,例如: users_student
 	verbose_name = '学生信息表'  # 在admin站点中显示的名称
 	verbose_name_plural = verbose_name  # 显示的复数名称
     # 除此之外还可以设置联合索引 联合唯一索引等(注意可能会与unique=True发生冲突)
      #3. 自定义数据库操作方法
     def __str__(self):
         """定义每个数据对象的显示信息"""
         return "<User %s>" % self.name

真正的项目,一些登陆注册等的功能,对于用户表,后面会讲到用户认证组件Auth模块,我们一般在models.py里自定义一个class User()继承自带的Auth表。

(1) 数据库表名

模型类如果未指明表名db_table,Django默认以 小写app应用名_小写模型类名 为数据库表名

可通过db_table 指明数据库表名。

(2) 关于主键

django会为表创建自动增长的主键列,每个模型只能有一个主键列。

如果使用选项设置某个字段的约束属性为主键列(primary_key)后,django不会再创建自动增长的主键列。

class Student(models.Model):
# django会自动在创建数据表的时候生成id主键/还设置了一个调用别名 pk
id = models.AutoField(primary_key=True, null=False, verbose_name="主键") # 设置主键
默认创建的主键列属性为id,可以使用pk代替,pk全拼为primary key。

(3) 属性命名限制

不能是python的保留关键字。

不允许使用连续的2个下划线,这是由django的查询方式决定的。__ 是关键字来的,不能使用!!!

定义属性时需要指定字段类型,通过字段类型的参数指定选项,语法如下:

属性名 = models.字段类型(约束选项, verbose_name="注释")
(4)字段类型

类型 说明
AutoField 自动增长的IntegerField,通常不用指定,不指定时Django会自动创建属性名为id的自动增长属性
BooleanField 布尔字段,值为True或False
NullBooleanField 支持Null、True、False三种值
CharField 字符串,参数max_length表示最大字符个数,对应mysql中的varchar
TextField 大文本字段,一般大段文本(超过4000个字符)才使用。
IntegerField 整数
DecimalField 十进制浮点数, 参数max_digits表示总位数, 参数decimal_places表示小数位数,常用于表示分数和价格 Decimal(max_digits=7, decimal_places=2) ==> 99999.99~ 0.00
FloatField 浮点数
DateField 日期 参数auto_now表示每次保存对象时,自动设置该字段为当前时间。 参数auto_now_add表示当对象第一次被创建时自动设置当前。 参数auto_now_add和auto_now是相互排斥的,一起使用会发生错误。
TimeField 时间,参数同DateField
DateTimeField 日期时间,参数同DateField
FileField 上传文件字段,django在文件字段中内置了文件上传保存类, django可以通过模型的字段存储自动保存上传文件, 但是, 在数据库中本质上保存的仅仅是文件在项目中的存储路径!!
ImageField 继承于FileField,对上传的内容进行校验,确保是有效的图片

注意:手机号不要用IntegerField,超过范围了

(5)约束选项

选项 说明
null 如果为True,表示允许为空,默认值是False。相当于python的None
blank 如果为True,则该字段允许为空白,默认值是False。 相当于python的空字符串,“”
db_column 字段的名称,如果未指定,则使用属性的名称。(展示名称)
db_index 若值为True, 则在表中会为此字段创建索引,默认值是False。 相当于SQL语句中的key
default 默认值,当不填写数据时,使用该选项的值作为数据的默认值。
primary_key 如果为True,则该字段会成为模型的主键,默认值是False,一般不用设置,系统默认设置。
unique 如果为True,则该字段在表中必须有唯一值,默认值是False。相当于SQL语句中的unique

verbose_name admin里显示的名字

注意:null是数据库范畴的概念,blank是表单验证范畴的(同理default也是)。更改一些class属性参数的时候可以不用重新数据库迁移 ,而修改数据库结构的需要重新迁移。

数据可范畴出错会发生报错,用户输入范畴不符合规定会提示不能为空,或者自增、默认值。

后加东西要是设置默认值
(6)外键

在设置外键时,需要通过on_delete选项指明主表删除数据时,对于外键引用表数据如何处理,在django.db.models中包含了可选常量:

CASCADE 级联,删除主表数据时连通一起删除外键表中数据

PROTECT 保护,通过抛出ProtectedError异常,来阻止删除主表中被外键应用的数据

SET_NULL 设置为NULL,仅在该字段null=True允许为null时可用

SET_DEFAULT 设置为默认值,仅在该字段设置了默认值时可用

SET() 设置为特定值或者调用特定方法,例如:

from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models

def get_sentinel_user():
 return get_user_model().objects.get_or_create(username='deleted')[0]

class UserModel(models.Model):
 user = models.ForeignKey(
     settings.AUTH_USER_MODEL,
     on_delete=models.SET(get_sentinel_user),
 )
  • DO_NOTHING 不做任何操作,如果数据库前置指明级联性,此选项会抛出IntegrityError异常

商品分类表

id category
1 蔬菜
2 电脑

商品信息表

id goods_name cid
1 冬瓜 1
2 华为笔记本 2
3 茄子 1

当模型字段的on_delete=CASCADE, 删除蔬菜(id=1),则在外键cid=1的商品id1和3就被删除。

当模型字段的on_delete=PROTECT,删除蔬菜,mysql自动检查商品信息表,有没有cid=1的记录,有则提示必须先移除掉商品信息表中,id=1的所有记录以后才能删除蔬菜。

当模型字段的on_delete=SET_NULL,删除蔬菜以后,对应商品信息表,cid=1的数据的cid全部被改成cid=null

当模型字段的on_delete=SET_DEFAULT,删除蔬菜以后,对应商品信息表,cid=1的数据记录的cid被被设置默认值。

6.2 数据库迁移

将模型类定义表架构的代码转换成SQL同步到数据库中,这个过程就是数据迁移。django中的数据迁移,就是一个类,这个类提供了一系列的终端命令,帮我们完成数据迁移的工作。

(1)生成迁移文件

所谓的迁移文件, 是类似模型类的迁移类,主要是描述了数据表结构的类文件.

python manage.py makemigrations

(2)同步到数据库中

python manage.py migrate
补充:在django内部提供了一系列的功能,这些功能也会使用到数据库,所以在项目搭建以后第一次数据迁移的时候,会看到django项目中其他的数据表被创建了。其中就有一个django内置的admin站点管理。(这个后面会写)

admin站点默认是开启状态的,我们可以通过http://127.0.0.1:8000/admin

这个站点必须有个管理员账号登录,所以我们可以在第一次数据迁移,有了数据表以后,就可以通过以下终端命令来创建一个超级管理员账号。

python manage.py createsuperuser

(后台超级管理员看到的表需要注册,且一些自增字段不会显示)

这里我注册用户名root 密码root123 进入admin

这是因为我之前用auth组件设置过一个普通用户,浏览器中还保存着sessionid,这个普通用户显然不是管理员,无权登录。

我们进入自己的root账户

在admin.py里注册表

记得在models.py里加上def__str__ 这样后台也就能显示相应的中文表示的对象了。

6.3 数据库基本操作

6.3.1 脚本orm操作数据库

注意导入顺序,不然会报错

(父级models类里有__str____repr__方法,也可以把__repr__重写)

import os
 
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'homework06.settings')
import django
 
django.setup()
 
from students.models import *
 
ret = Course.objects.all()# <QuerySet [<Course: 课程1>, <Course: 课程2>, <Course: 课程3>]>
ret = Course.objects.get(pk=1) # 课程1
ret = Course.objects.filter(pk=1)# <QuerySet [<Course: 课程1>]> 
  1. save()方法

    通过创建模型类对象,执行对象的save( )方法保存到数据库中

    student = Student(
        name="刘德华",
        age=17,
        sex=True,
        clasmate=301,
        description="一手忘情水"
    )
    student.save()
    print(student.id) # 判断是否新增有ID
    
  2. create() 方法

    通过模型类.objects.create()保存,返回生成的模型类对象。

    student = Student.objects.create(
        name="赵本山",
        age=50,
        sex=True,
        class_number=301,
        description="一段小品"
    )
    print(student.id)
    

6.3.2 基础查询

ORM中针对查询结果的限制,提供了一个查询集[QuerySet].这个QuerySet,是ORM中针对查询结果进行保存数据的一个类型,我们可以通过了解这个QuerySet进行使用,达到查询优化,或者限制查询结果数量的作用。

后面会详细讲QuerySet,orm进阶及优化搜索,到这里要先了解一些基本操作。

def index(request):
    print(Students.objects, type(Students.objects))#app01.Students.objects <class 'django.db.models.manager.Manager'>
    return HttpResponse('ok')
  1. all ( )

    查询所有对象,返回queryset对象。查询集,也称查询结果集,QuerySet,表示从数据库中获取的对象集合。

    students = Student.objects.all()
    print("students:",students)
    
  2. filter()

    筛选条件相匹配的多个对象,返回queryset对象

    # 查询所有的女生
    students = Student.objects.filter(sex=0)
    print(students)
    
  3. get() 获取单个对象 (登录操作中使用较多)

    返回与所给筛选条件相匹配的对象,返回结果有且只有一个, 如果符合筛选条件的对象超过一个或者没有都会抛出错误。

    student = Student.objects.get(pk=1)
    print(student)
    print(student.description)
    get使用过程中的注意点:get是根据条件返回多个结果或者没有结果,都会报错
    try:
        student = Student.objects.get(name="刘德华")
        print(student)
        print(student.description)
    except Student.MultipleObjectsReturned:
        print("查询得到多个结果!")
    except Student.DoesNotExist:
        print("查询结果不存在!")
    
  4. first()、last()

    分别为查询集的第一条记录和最后一条记录

    # 没有结果返回none,如果有多个结果,则返回模型对象
    students = Student.objects.all()
    # print(students.name)
    print(students[0].name)
    stu01 = Student.objects.first()
    stu02 = Student.objects.last()
    
    print(stu01.name)
    print(stu02.name)
    
  5. exclude()

    筛选条件不匹配的对象,返回queryset对象。

    # 查询张三以外的所有的学生
    students = Student.objects.exclude(name="张三")
    print(students)
    
  6. order_by( )

    对查询结果排序

    # order_by("字段")  # 按指定字段正序显示,相当于 asc  从小到大
    # order_by("-字段") # 按字段倒序排列,相当于 desc 从大到小
    # order_by("第一排序","第二排序",...)
    
    # 查询所有的男学生按年龄从高到低展示
    # students = Student.objects.all().order_by("-age","-id")
    students = Student.objects.filter(sex=1).order_by("-age", "-id")
    print(students)
    
  7. count()

    查询集中对象的个数

    结果其实与len(Student.objects.all())一样

    # 查询所有男生的个数
    count = Student.objects.filter(sex=1).count()
    print(count)
    
  8. exists()

    判断查询集中是否有数据,如果有则返回True,没有则返回False

    # 查询Student表中是否存在学生
    print(Student.objects.exists())
    
  9. values()、values_list()

    • value()把结果集中的模型对象转换成字典,并可以设置转换的字段列表,达到减少内存损耗,提高性能

    • values_list(): 把结果集中的模型对象转换成元组(即没有键只有值),并可以设置转换的字段列表,达到减少内存损耗,提高性能

    • 二者都返回QuerySet 只是里面的类型不同

    # values 把查询结果中模型对象转换成字典
    student_list = Student.objects.filter(classmate="301")
    student_list = student_list.order_by("-age")
    student_list = student_list.filter(sex=1)
    ret1 = student_list.values() # 默认把所有字段全部转换并返回
    ret2 = student_list.values("id","name","age") # 可以通过参数设置要转换的字段并返回
    ret3 = student_list.values_list() # 默认把所有字段全部转换并返回
    ret4 = student_list.values_list("id","name","age") # 可以通过参数设置要转换的字段并返回
    print(ret4)
    return JsonResponse({},safe=False)
    
    def index(request):
        print(Students.objects.values('name'))
        return HttpResponse('ok')
    #<QuerySet [{'name': '张三'}, {'name': '李四'}, {'name': '王五'}]>
    
  10. distinct()

    从返回结果中剔除重复记录,返回queryset

    # 查询所有学生出现过的年龄
    print(Student.objects.values("age").distinct())
    
  11. reverse()

    对已经排序了的queryset进行反转 相当于order_by(’加个负号‘)

    students = Student.objects.all().order_by("-age").reverse()
    

小总结

  • 返回QuerySet

    all()、filter()、exclude()、order_by()、values()、values_list()、reverse、distinct()

    直接 Students.objects相当于.all() 可以在后面根对QuerySet的操作

  • 返回对象

    get()、first()、last()

  • 返回数字

    count()

  • 返回布尔

    exist()

6.3.3 模糊查询

  1. contains

    说明:如果要包含%无需转义,直接写即可。

    例:查询姓名包含'华'的学生

    Student.objects.filter(name__contains='华')
    
  2. startswith、endswith

    例:查询姓名以'文'结尾的学生

    Student.objects.filter(name__endswith='文')
    

    以上运算符都区分大小写,在这些运算符前加上i表示不区分大小写,如iexact、icontains、istartswith、iendswith.

  3. isnull

    例:查询个性签名不为空的学生。

    # 修改Student模型description属性允许设置为null,然后数据迁移
    description = models.TextField(default=None, null=True, verbose_name="个性签名")
    # 添加测试数据
    NSERT INTO student.db_student (name, age, sex, class, description, created_time, updated_time) VALUES ('刘德华', 17, 1, '407', null, '2020-11-20 10:00:00.000000', '2020-11-20 10:00:00.000000');
    # 代码操作
    tudent_list = Student.objects.filter(description__isnull=True)
    
  4. in

    例:查询编号为1或3或5的学生

    Student.objects.filter(id__in=[1, 3, 5])
    
  5. 比较查询

    • gt 大于 (greater then)
    • gte 大于等于 (greater then equal)
    • lt 小于 (less then)
    • lte 小于等于 (less then equal)

    例:查询编号大于3的学生

    Student.objects.filter(id__gt=3)
    
  6. 日期查询

    year、month、day、week_day、hour、minute、second:对日期时间类型的属性进行运算。

    例:查询2010年被添加到数据中的学生。

    Student.objects.filter(born_date__year=1980)
    例:查询2016年6月20日后添加的学生信息。

    from django.utils import timezone as datetime
    student_list = Student.objects.filter(created_time__gte=datetime.datetime(2016,6,20),created_time__lt=datetime.datetime(2016,6,21)).all()
    print(student_list)
    

6.3.4 进阶查询

  1. F 查询

    之前的查询都是对象的属性与常量值比较,两个属性怎么比较呢? 答:使用F对象,被定义在django.db.models中。

    语法如下:

    """F对象:2个字段的值比较"""
    # 获取从添加数据以后被改动过数据的学生
    from django.db.models import F
    # SQL: select * from db_student where created_time=updated_time;
    student_list = Student.objects.exclude(created_time=F("updated_time"))
    print(student_list)
    
  2. Q查询

    多个过滤器逐个调用表示逻辑与关系,同sql语句中where部分的and关键字。

    例:查询年龄大于20,并且编号小于30的学生。

    Student.objects.filter(age__gt=20,id__lt=30)
    #或
    Student.filter(age__gt=20).filter(id__lt=30)
    

    如果需要实现逻辑或or的查询,需要使用Q()对象结合|运算符,Q对象被义在django.db.models中。

    语法如下:

    Q(属性名__运算符=值)
    Q(属性名__运算符=值) | Q(属性名__运算符=值)
    

    例:查询年龄小于19或者大于20的学生,使用Q对象如下。

    from django.db.models import Q
    student_list = Student.objects.filter( Q(age__lt=19) | Q(age__gt=20) ).all()
    

    Q对象可以使用&、|连接,&表示逻辑与,|表示逻辑或**

    例:查询年龄大于20,或编号小于30的学生,只能使用Q对象实现

    Student.objects.filter(Q(age__gt=20) | Q(pk__lt=30))
    
    Q对象左边可以使用~操作符,表示非not。但是工作中,我们只会使用Q对象进行或者的操作,只有多种嵌套复杂的查询条件才会使用&和~进行与和非得操作。
    

    例:查询编号不等于30的学生。

    Student.objects.filter(~Q(pk=30))
    
  3. 聚合查询

    使用aggregate()过滤器调用聚合函数。聚合函数包括:Avg 平均,Count 数量,Max 最大,Min 最小,Sum 求和,被定义在django.db.models中。

    例:查询学生的平均年龄。

    from django.db.models import Sum,Count,Avg,Max,Min
    
    Student.objects.aggregate(Avg('age'))
    

    注意:aggregate的返回值是一个字典类型,格式如下:

      {'属性名__聚合类小写':值} 如{'age__avg': 13.2}
    

    使用count时一般不使用aggregate()过滤器。

    例:查询学生总数。

    Student.objects.count() # count函数的返回值是一个数字。
    
  4. 分组查询

    annotate() 返回值依然是 queryset对象,增加了分组统计后的键值对

    QuerySet对象.annotate()
    # annotate() 进行分组统计,按前面select 的字段进行 group by
    模型对象.objects.values("id").annotate(course=Count('course__sid')).values('id','course')
    # 查询指定模型, 按id分组 , 将course下的sid字段计数,返回结果是 name字段 和 course计数结果 
    
    # SQL原生语句中分组之后可以使用having过滤,在django中并没有提供having对应的方法,但是可以使用filter对分组结果进行过滤
    # 所以filter在annotate之前,表示where,在annotate之后代表having
    # 同理,values在annotate之前,代表分组的字段,在annotate之后代表数据查询结果返回的字段
    
  5. 原生查询

    执行原生sql语句,也可以直接跳过模型,使用原生pymysql

     ret = Student.objects.raw("SELECT id,name,age FROM db_student")  # student 可以是任意一个模型
     # 这样执行获取的结果无法通过QuerySet进行操作读取,只能循环提取
     print(ret,type(ret))
     for item in ret:
        print(item,type(item))
    

6.3.5 修改记录

  1. 使用save更新数据

    student = Student.objects.filter(name='刘德华').first()
    print(student)
    student.age = 19
    student.classmate = "303"
    # save之所以能提供给我们添加数据的同时,还可以更新数据的原因?
    # save会找到模型的字段的主键id的值,
    # 主键id的值如果是none,则表示当前数据没有被数据库,所以save会自动变成添加操作
    # 主键id有值,则表示当前数据在数据库中已经存在,所以save会自动变成更新数据操作
    student.save()
    
  2. update更新 (推荐)

    使用模型类.objects.filter().update(),会返回受影响的行数

    # update是全局更新,只要符合更新的条件,则全部更新,因此强烈建议加上条件!!!
    student = Student.objects.filter(name="赵华",age=22).update(name="刘芙蓉",sex=True)
    print(student)
    

6.3.6删除记录

删除有两种方法

(1)模型类对象.delete

student = Student.objects.get(id=13)
student.delete()

(2)模型类.objects.filter().delete()

Student.objects.filter(id=14).delete() 

代码:

# 1. 先查询到数据模型对象。通过模型对象进行删除
student = Student.objects.filter(pk=13).first()
student.delete()

# 2. 直接删除
ret = Student.objects.filter(pk=100).delete()
print(ret)
# 务必写上条件,否则变成了清空表了。ret = Student.objects.filter().delete()

6.4 创建关联模型

实例:我们来假定下面这些概念,字段和关系

  • 班级模型: 班级名称、导员。

  • 课程模型:课程名称、讲师等。

  • 学生模型: 学生有姓名,年龄,只有一个班级,所以和班级表是一对多的关系(one-to-many);选修了多个课程,所以和课程表是多对多的关系(many-to-many)

  • 学生详情:学生的家庭地址,手机号,邮箱等详细信息,和学生模型应该是一对一的关系(one-to-one)

from django.db import models
 
 
# Create your models here.
 
 
class Clas(models.Model):
    name = models.CharField(max_length=32, unique=True, verbose_name="班级名称")
 
    class Meta:
        db_table = "db_class"
 
 
class Course(models.Model):
    name = models.CharField(max_length=32, unique=True, verbose_name="课程名称")
 
    class Meta:
        db_table = "db_course"
 
 
class Student(models.Model):
    sex_choices = (
        (0, "女"),
        (1, "男"),
        (2, "保密"),
    )
 
    name = models.CharField(max_length=32, unique=True, verbose_name="姓名")
    age = models.SmallIntegerField(verbose_name="年龄", default=18)  # 年龄
    sex = models.SmallIntegerField(choices=sex_choices)
    birthday = models.DateField()
 
    # 一对多
    # on_delete= 关联关系的设置
    # models.CASCADE    删除主键以后, 对应的外键所在数据也被删除
    # models.DO_NOTHING 删除主键以后, 对应的外键不做任何修改
    # 反向查找字段 related_name
    clas = models.ForeignKey("Clas", on_delete=models.CASCADE)
 
    # 多对多
    # 建立多对多的关系,ManyToManyField可以建在两个模型中的任意一个,自动创建第三张表
    courses = models.ManyToManyField("Course", db_table="db_student2course")
 
    # 一对一,使用同一对多
    stu_detail = models.OneToOneField("StudentDetail", on_delete=models.CASCADE)
 
    class Meta:
        db_table = "db_student"
 
    def __str__(self):
        return self.name
 
 
class StudentDetail(models.Model):
    tel = models.CharField(max_length=32)
    email = models.EmailField()
    description = models.TextField(null=True, verbose_name="个性签名")
 
    class Meta:
        db_table = "db_student_detail"

6.4.1 关联添加

  1. 一对多 与 一对一

     stu = Student.objects.create(name="王五", age=23, sex=1, birthday="1991-11-12", clas_id=9, stu_detail_id=6)
      print(stu.name)
      print(stu.age)
      print(stu.sex)
      print(stu.clas_id)  # 6
      print(stu.stu_detail_id)  # 5
      print(stu.clas)  # 模型类对象
      print(stu.stu_detail)  # 模型类对象
    
      # 查询stu这个学生的班级名称
      print(stu.clas.name)
      # 查询stu这个学生的手机号
      print(stu.stu_detail.tel)
    
  2. 多对多

     # stu = Student.objects.create(name="rain", age=33, sex=1, birthday="1996-11-12", clas_id=9, stu_detail_id=7)
        # (1) 添加多对多的数据
    
        # 添加多对多方式1
        c1 = Course.objects.get(title="思修")
        c2 = Course.objects.get(title="逻辑学")
        stu.courses.add(c1,c2)
    
        # 添加多对多方式2
        stu = Student.objects.get(name="张三")
        stu.courses.add(5,7)
    
        # 添加多对多方式3
        stu = Student.objects.get(name="李四")
        stu.courses.add(*[6,7])
    
        # (2) 删除多对多记录
    
       stu = Student.objects.get(name="李四")
       stu.courses.remove(7)
    
        # (3) 清空多对多记录:clear方法
    
        stu = Student.objects.get(name="rain")
        stu.courses.clear()
    
        # (4) 重置多对多记录:set方法
    
        stu = Student.objects.get(name="李四")
        stu.courses.set([5,8])
    
        # (5) 多对多记录查询: all
        # 查询李四所报课程的名称
        stu = Student.objects.get(name="李四")
        courses = stu.courses.all()
        courses = stu.courses.all().values("title")
        print(courses)  # <QuerySet [<Course: Course object (5)>, <Course: Course object (8)>]>
    

6.4.2 关联查询

  1. 基于对象查询(子查询)

        # **********************************  一对多查询
        # 查询张三所在班级的名称
        stu = Student.objects.get(name="张三")
        print(stu.clas.name)
    
        # 查询计算机科学与技术2班有哪些学生
    
        clas = Clas.objects.get(name="计算机科学与技术2班")
        # 反向查询方式1:
        ret = clas.student_set.all()  # 反向查询按表名小写_set
        print(ret) # <QuerySet [<Student: 张三>, <Student: 李四>]>
        # 反向查询方2:
    
        print(clas.student_list.all())  # <QuerySet [<Student: 张三>, <Student: 李四>]>
    
        # **********************************  一对一查询
    
        # 查询李四的手机号
        stu = Student.objects.get(name="李四")
        print(stu.stu_detail.tel)
    
        # 查询110手机号的学生姓名和年龄
    
        stu_detail = StudentDetail.objects.get(tel="110")
        # 反向查询方式1: 表名小写
        print(stu_detail.student.name)
        print(stu_detail.student.age)
        # 反向查询方式2: related_name
        print(stu_detail.stu.name)
        print(stu_detail.stu.age)
    
        # **********************************  多对多查询
    
        # 查询张三所报课程的名称
    
        stu = Student.objects.get(name="张三")
        print(stu.courses.all())  # QuerySet [<Course: 近代史>, <Course: 篮球>]>
    
        # 查询选修了近代史这门课程学生的姓名和年龄
        course = Course.objects.get(title="近代史")
        # 反向查询方式1: 表名小写_set
        print(course.student_set.all()) # <QuerySet [<Student: 张三>, <Student: 李四>]>
    
        # 反向查询方式2:related_name
       print(course.students.all())
       print(course.students.all().values("name","age")) # <QuerySet [{'name': '张三', 'age': 22}, {'name': '李四', 'age': 24}]>
    
    

    (1)正向查询按字段

    (2)反向查询按表名小写或者related_name

  2. 基于双下划线查询(join查询)

        # 查询张三的年龄
        ret = Student.objects.filter(name="张三").values("age")
        print(ret) # <QuerySet [{'age': 22}]>
    
        # (1) 查询年龄大于22的学生的姓名以及所在名称班级
        # select db_student.name,db_class.name from db_student inner join db_class on db_student.clas_id = db_class.id where db_student.age>22;
    
        # 方式1 : Student作为基表
        ret = Student.objects.filter(age__gt=22).values("name","clas__name")
        print(ret)
        # 方式2 :Clas表作为基表
        ret = Clas.objects.filter(student_list__age__gt=22).values("student_list__name","name")
        print(ret)
    
        # (2) 查询计算机科学与技术2班有哪些学生
    
        ret = Clas.objects.filter(name="计算机科学与技术2班").values("student_list__name")
        print(ret)  #<QuerySet [{'student_list__name': '张三'}, {'student_list__name': '李四'}]>
    
        # (3) 查询张三所报课程的名称
    
        ret = Student.objects.filter(name="张三").values("courses__title")
        print(ret) # <QuerySet [{'courses__title': '近代史'}, {'courses__title': '篮球'}]>
    
        # (4) 查询选修了近代史这门课程学生的姓名和年龄
    
        ret = Course.objects.filter(title="近代史").values("students__name","students__age")
        print(ret) # <QuerySet [{'students__name': '张三', 'students__age': 22}, {'students__name': '李四', 'students__age': 24}]>
    
        # (5) 查询李四的手机号
    
        ret = Student.objects.filter(name='李四').values("stu_detail__tel")
        print(ret)  # <QuerySet [{'stu_detail__tel': '911'}]>
    
    
        # (6) 查询手机号是110的学生的姓名和所在班级名称
    
        # 方式1
        ret = StudentDetail.objects.filter(tel="110").values("stu__name","stu__clas__name")
        print(ret) # <QuerySet [{'stu__name': '张三', 'stu__clas__name': '计算机科学与技术2班'}]>
    
        # 方式2:
        ret = Student.objects.filter(stu_detail__tel="110").values("name","clas__name")
        print(ret) # <QuerySet [{'name': '张三', 'clas__name': '计算机科学与技术2班'}]>
    

    (1)正向关联按关联字段

    (2)反向按表名小写或related_name

  3. 关联分组查询

    # from django.db.models import Avg, Count, Max, Min
    ret = Student.objects.values("sex").annotate(c = Count("name"))
    print(ret) # <QuerySet [{'sex': 0, 'c': 1}, {'sex': 1, 'c': 3}]>
    
    # (1)查询每一个班级的名称以及学生个数
    
    ret = Clas.objects.values("name").annotate(c = Count("student_list__name"))
    print(ret) # <QuerySet [{'name': '网络工程1班', 'c': 0}, {'name': '网络工程2班', 'c': 0}, {'name': '计算机科学与技术1班', 'c': 0}, {'name': '计算机科学与技术2班', 'c': 1}, {'name': '软件1班', 'c': 3}]>
    
    # (2)查询每一个学生的姓名,年龄以及选修课程的个数
    
    ret = Student.objects.values("name","age").annotate(c=Count("courses__title"))
    print(ret) # <QuerySet [{'name': 'rain', 'c': 0}, {'name': '张三', 'c': 2}, {'name': '李四', 'c': 2}, {'name': '王五', 'c': 0}]>
    
    ret = Student.objects.all().annotate(c=Count("courses__title")).values("name","age","sex","c")
    # print(ret)
    
    # (3) 每一个课程名称以及选修学生的个数
    ret = Course.objects.all().annotate(c = Count("students__name")).values("title","c")
    print(ret) # <QuerySet [{'title': '近代史', 'c': 2}, {'title': '思修', 'c': 0}, {'title': '篮球', 'c': 1}, {'title': '逻辑学', 'c': 1}, {'title': '轮滑', 'c': 0}]>
    
    # (4)  查询选修课程个数大于1的学生姓名以及选修课程个数
    ret = Student.objects.all().annotate(c=Count("courses__title")).filter(c__gt=1).values("name","c")
    print(ret) # <QuerySet [{'name': '张三', 'c': 2}, {'name': '李四', 'c': 2}]>
    
    # (5) 查询每一个学生的姓名以及选修课程个数并按着选修的课程个数进行从低到高排序
    ret = Student.objects.all().annotate(c=Count("courses__title")).order_by("c").values("name","c")
    print(ret)
    

7.ORM进阶

7.1 queryset特性

  1. 可切片

    使用Python 的切片语法来限制查询集记录的数目 。它等同于SQL 的LIMITOFFSET 子句。

    >>> Article.objects.all()[:5]   # (LIMIT 5)
    >>> Article.objects.all()[5:10]    # (OFFSET 5 LIMIT 5)
    

    不支持负的索引(例如Article.objects.all()[-1])。通常,查询集 的切片返回一个新的查询集 —— 它不会执行查询。

  2. 可迭代

    articleList=models.Article.objects.all()
    
    for article in articleList:
        print(article.title)
    
  3. 惰性查询

    查询集 是惰性执行的 —— 创建查询集不会带来任何数据库的访问。你可以将过滤器保持一整天,直到查询集 需要求值时,Django 才会真正运行这个查询。

    queryResult=models.Article.objects.all() # not hits database
    print(queryResult) # hits database
    for article in queryResult:
        print(article.title)    # hits database
    

    一般来说,只有在“请求”查询集 的结果时才会到数据库中去获取它们。当你确实需要结果时,查询集 通过访问数据库来求值。 关于求值发生的准确时间。

  4. 缓存机制

    每个查询集都包含一个缓存来最小化对数据库的访问。理解它是如何工作的将让你编写最高效的代码。

    在一个新创建的查询集中,缓存为空。首次对查询集进行求值 —— 同时发生数据库查询 ——Django 将保存查询的结果到查询集的缓存中并返回明确请求的结果(例如,如果正在迭代查询集,则返回下一个结果)。接下来对该查询集 的求值将重用缓存的结果。

    请牢记这个缓存行为,因为对查询集使用不当的话,它会坑你的。例如,下面的语句创建两个查询集,对它们求值,然后扔掉它们:

    queryset = Book.objects.all()
    print(queryset) # hit database
    print(queryset) # hit database
    

    注:简单地打印查询集不会填充缓存

    这意味着相同的数据库查询将执行两次,显然倍增了你的数据库负载。同时,还有可能两个结果列表并不包含相同的数据库记录,因为在两次请求期间有可能有Article被添加进来或删除掉。为了避免这个问题,只需保存查询集并重新使用它:

    queryset = Book.objects.all()
    ret = [i for i in queryset] # hit database
    print(queryset) # 使用缓存
    print(queryset) # 使用缓存
    

    何时查询集会被缓存?

    1. 遍历queryset时
    2. if语句(为了避免这个,可以用exists()方法来检查是否有数据)

    所以单独queryset的索引和切片都不会缓存

    queryset = Book.objects.all()
    one = queryset[0] # hit database
    two = queryset[1] # hit database
    print(one)
    print(two)
    
  5. exists()与iterator()方法

    exists

    简单的使用if语句进行判断也会完全执行整个queryset并且把数据放入cache,虽然你并不需要这些 数据!为了避免这个,可以用exists()方法来检查是否有数据:

     if queryResult.exists():
        #SELECT (1) AS "a" FROM "blog_article" LIMIT 1; args=()
            print("exists...")
    

    iterator

    当queryset非常巨大时,cache会成为问题。

    处理成千上万的记录时,将它们一次装入内存是很浪费的。更糟糕的是,巨大的queryset可能会锁住系统 进程,让你的程序濒临崩溃。要避免在遍历数据的同时产生queryset cache,可以使用iterator()方法 来获取数据,处理完数据就将其丢弃。

    objs = Book.objects.all().iterator()
    
    # iterator()可以一次只从数据库获取少量数据,这样可以节省内存
    
    for obj in objs:
        print(obj.title)
    #BUT,再次遍历没有打印,因为迭代器已经在上一次遍历(next)到最后一次了,没得遍历了
    for obj in objs:
        print(obj.title)
    

    当然,使用iterator()方法来防止生成cache,意味着遍历同一个queryset时会重复执行查询。所以使 #用iterator()的时候要当心,确保你的代码在操作一个大的queryset时没有重复执行查询。

    queryset的cache是用于减少程序对数据库的查询,在通常的使用下会保证只有在需要的时候才会查询数据库。 使用exists()和iterator()方法可以优化程序对内存的使用。不过,由于它们并不会生成queryset cache,可能 会造成额外的数据库查询。 

    总结:在使用缓存机制还是生成器机制的选择上如果是,数据量大情况主要使用生成器;数据少使用次数多的情况使用缓存机制。

7.2 中介模型

处理类似搭配 pizza 和 topping 这样简单的多对多关系时,使用标准的ManyToManyField 就可以了。但是,有时你可能需要关联数据到两个模型之间的关系上。

例如,有这样一个应用,它记录音乐家所属的音乐小组。我们可以用一个ManyToManyField 表示小组和成员之间的多对多关系。但是,有时你可能想知道更多成员关系的细节,比如成员是何时加入小组的。

对于这些情况,Django 允许你指定一个中介模型来定义多对多关系。 你可以将其他字段放在中介模型里面。源模型的ManyToManyField 字段将使用through 参数指向中介模型。对于上面的音乐小组的例子,代码如下:

from django.db import models
 
class Person(models.Model):
    name = models.CharField(max_length=128)
 
    def __str__(self):              # __unicode__ on Python 2
        return self.name
 
class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')
 
    def __str__(self):              # __unicode__ on Python 2
        return self.name
 
class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

既然你已经设置好ManyToManyField 来使用中介模型(在这个例子中就是Membership),接下来你要开始创建多对多关系。你要做的就是创建中介模型的实例:

>>> ringo = Person.objects.create(name="Ringo Starr")
>>> paul = Person.objects.create(name="Paul McCartney")
>>> beatles = Group.objects.create(name="The Beatles")
>>> m1 = Membership(person=ringo, group=beatles,
...     date_joined=date(1962, 8, 16),
...     invite_reason="Needed a new drummer.")
>>> m1.save()
>>> beatles.members.all()
[<Person: Ringo Starr>]
>>> ringo.group_set.all()
[<Group: The Beatles>]
>>> m2 = Membership.objects.create(person=paul, group=beatles,
...     date_joined=date(1960, 8, 1),
...     invite_reason="Wanted to form a band.")
>>> beatles.members.all()
[<Person: Ringo Starr>, <Person: Paul McCartney>]

与普通的多对多字段不同,你不能使用addcreate和赋值语句(比如,beatles.members = [...])来创建关系:

# THIS WILL NOT WORK
>>> beatles.members.add(john)
# NEITHER WILL THIS
>>> beatles.members.create(name="George Harrison")
# AND NEITHER WILL THIS
>>> beatles.members = [john, paul, ringo, george]

为什么不能这样做? 这是因为你不能只创建 Person和 Group之间的关联关系,你还要指定 Membership模型中所需要的所有信息;而简单的add、create 和赋值语句是做不到这一点的。所以它们不能在使用中介模型的多对多关系中使用。此时,唯一的办法就是创建中介模型的实例。

remove()方法被禁用也是出于同样的原因。但是clear() 方法却是可用的。它可以清空某个实例所有的多对多关系:

>>> # Beatles have broken up
>>> beatles.members.clear()
>>> # Note that this deletes the intermediate model instances
>>> Membership.objects.all()

7.3 数据库表反向生成模型类

众所周知,Django较为适合原生开发,即通过该框架搭建一个全新的项目,通过在修改models.py来创建新的数据库表。但是往往有时候,我们需要利用到之前的已经设计好的数据库,数据库中提供了设计好的多种表单。那么这时如果我们再通过models.py再来设计就会浪费很多的时间。所幸Django为我们提供了inspecdb的方法。他的作用即使根据已经存在对的mysql数据库表来反向映射结构到models.py中.

我们在展示django ORM反向生成之前,我们先说一下怎么样正向生成代码。

正向生成,指的是先创建model.py文件,然后通过django内置的编译器,在数据库如mysql中创建出符合model.py的表。

反向生成,指的是先在数据库中create table,然后通过django内置的编译器,生成model代码。

python manage.py inspectdb > models文件名

7.4 查询优化

  1. select_related()

    对于一对一字段(OneToOneField)和外键字段(ForeignKey),可以使用select_related 来对QuerySet进行优化。

    select_related 返回一个QuerySet,当执行它的查询时它沿着外键关系查询关联的对象的数据。它会生成一个复杂的查询并引起性能的损耗,但是在以后使用外键关系时将不需要数据库查询。

    简单说,在对QuerySet使用select_related()函数后,Django会获取相应外键对应的对象,从而在之后需要的时候不必再查询数据库了。

    下面的例子解释了普通查询和select_related() 查询的区别。

    查询id=2的的书籍的出版社名称,下面是一个标准的查询:

    # Hits the database.
    book= models.Book.objects.get(nid=2)
    # Hits the database again to get the related Blog object.
    print(book.publish.name)
    

    如果我们使用select_related()函数:

    books=models.Book.objects.select_related("publish").all()
    for book in books:
         #  Doesn't hit the database, because book.publish
         #  has been prepopulated in the previous query.
         print(book.publish.name)
    

    多外键查询

    这是针对publish的外键查询,如果是另外一个外键呢?让我们一起看下:

    book=models.Book.objects.select_related("publish").get(nid=1)
    print(book.authors.all())
    

    观察logging结果,发现依然需要查询两次,所以需要改为:

    book=models.Book.objects.select_related("publish","").get(nid=1)
    print(book.publish)
    或者:
    
    book=models.Article.objects
                 .select_related("publish")
                 .select_related("")
                 .get(nid=1)  # django 1.7 支持链式操作
    print(book.publish)
    
  2. prefetch_related()

    对于多对多字段(ManyToManyField)和一对多字段,可以使用prefetch_related()来进行优化。

    prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。若有n个对象,每个对象的多对多字段对应Mi条,就会生成Σ(n)Mi 行的结果表。

    prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系。

    # 查询所有文章关联的所有标签
    books=models.Book.objects.all()
    for book in books:
        print(book.authors.all())  #4篇文章: hits database 5
    

    改为prefetch_related:

    # 查询所有文章关联的所有标签
    books=models.Book.objects.prefetch_related("authors").all()
    for book in books:
        print(book.authors.all())  #4篇文章: hits database 2
    
  3. extra()

    extra(select=None, where=None, params=None, 
          tables=None, order_by=None, select_params=None)
    

    有些情况下,Django的查询语法难以简单的表达复杂的 WHERE 子句,对于这种情况, Django 提供了 extra() QuerySet修改机制 — 它能在 QuerySet生成的SQL从句中注入新子句

    extra可以指定一个或多个 参数,例如 select, where or tables. 这些参数都不是必须的,但是你至少要使用一个!要注意这些额外的方式对不同的数据库引擎可能存在移植性问题.(因为你在显式的书写SQL语句),除非万不得已,尽量避免这样。

    参数之select

    The select 参数可以让你在 SELECT 从句中添加其他字段信息,它应该是一个字典,存放着属性名到 SQL 从句的映射。

    queryResult=models.Article.objects.extra(select={'is_recent': "create_time > '2017-09-05'"})
    

    结果集中每个 Entry 对象都有一个额外的属性is_recent, 它是一个布尔值,表示 Article对象的create_time 是否晚于2017-09-05.

    参数之where / tables

    您可以使用where定义显式SQL WHERE子句 - 也许执行非显式连接。您可以使用tables手动将表添加到SQL FROM子句。

    where和tables都接受字符串列表。所有where参数均为“与”任何其他搜索条件。

    举例来讲:

    queryResult=models.Article.objects.extra(where=['nid in (3,4) OR title like "py%" ','nid>2'])
    

8. Ajax请求

客户端(浏览器)向服务端发起请求的形式:

  1. 地址栏:GET
  2. 超链接标签:GET
  3. form表单:GET或POST
  4. Ajax(重要):GET或POST或PUT或DELETE

AJAX(Asynchronous Javascript And XML)翻译成中文就是“异步Javascript和XML”。即使用Javascript语言与服务器进行异步交互,传输的数据为XML(当然,传输的数据不只是XML,现在更多使用json数据)。

AJAX的特点和优点:

  • 异步

  • 局部刷新

发送ajax有很多种方法,如用xhr(XMLHttpRequest),jQuery,axios(当前最火),fetch,这里主要讲Jquery(人人都会)。

8.1 Ajax请求案例

8.1.1 视图

# Create your views here.
def reg(request):
    return render(request, "reg.html")

def reg_user(request):
    data = {"msg": "", "state": "success"}
    user = request.POST.get("user")
    if user == "yuan":
        data["state"] = "error"
        data["msg"] = "该用户已存在!"

    return JsonResponse(data)

模板:reg.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<p>用户名:<input type="text"><span class="error" style="color: red"></span></p >

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script>

    $(":text").blur(function () {
        // 发送Ajax请求
        $.ajax({
            url: "http://127.0.0.1:8008/reg_user/",
            type: "post",
            data: {
                user: $(":text").val(),
            },
            success: function (response) {
                console.log(response);
                $(".error").html(response.msg);
            }
        })
    })

</script>

</body>
</html>

流程图

常用参数

返回结果

缓存问题:

对于一些浏览器如IE(新版本不知道还会不会有),内部的缓存会影响到ajax的结果,但是像谷歌等的不会。意思是当你第二次发起ajax请求时,浏览器不会向服务器发起,而走的是缓存,这样你只会显示第一次的数据。

可以自己写个例子试一下,如第一次发送请求返回字符串‘123’,这时你修改响应代码为‘456’再次发生ajax,不同浏览器返回结果不一样。

解决办法:

可以在每次请求后面加个时间戳意思一下,这样每次都不得不重新发请求了:

url: 'http://127.0.0.1:8000/students/xx?t='+Date.now(),

8.1.2同源策略

同源策略和跨域

现在我们将reg.html单独放在客户端,用浏览器打开,再触发事件,会发现报错:

这是因为浏览器的同源策略导致的。

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可

同源策略,它是由Netscape提出的一个著名的安全策略。现在所有支持JavaScript 的浏览器都会使用这个策略。所谓同源是指,域名,协议,端口相同。当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。

那么如何解决这种跨域问题呢,我们主要由三个思路:

  1. jsonp
  2. cors
  3. 前端代理

这里主要给大家介绍第二个:cors

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

所以,服务器方面只要添加一个响应头,同意跨域请求,浏览器也就不再拦截:

response = JsonResponse(data)
response["Access-Control-Allow-Origin"] = "*"

在抓包工具中在这里(翻译下英文):

cors
cors有两种请求:简单请求和非简单请求

只要同时满足以下两大条件,就属于简单的请求

(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。浏览器对这两种请求的处理,是不一样的。

简单请求:一次请求

非简单请求:两次请求,在发送数据之前会先发一次请求用于做“预检”,只有“预检”通过后才再发送一次请求用于数据传输。

- 请求方式:OPTIONS
- “预检”其实做检查,检查如果通过则允许传输数据,检查不通过则不再发送真正想要发送的消息
- 如何“预检”
     => 如果复杂请求是PUT等请求,则服务端需要设置允许某请求,否则“预检”不通过
        Access-Control-Request-Method
     => 如果复杂请求设置了请求头,则服务端需要设置允许某请求头,否则“预检”不通过
        Access-Control-Request-Headers

支持跨域,简单请求:

服务器设置响应头:Access-Control-Allow-Origin = '域名' 或 '*'

支持跨域复杂请求:

由于复杂请求时,首先会发送“预检”请求,如果“预检”成功,则发送真实数据。
“预检”请求时,允许请求方式则需服务器设置响应头:Access-Control-Request-Method
“预检”请求时,允许请求头则需服务器设置响应头:Access-Control-Request-Headers

cors在Django中的实现:

在返回结果中加入允许信息(简单请求):

def test(request):
    import json
    obj=HttpResponse(json.dumps({'name':'yuan'}))
    # obj['Access-Control-Allow-Origin']='*'
    obj['Access-Control-Allow-Origin']='http://127.0.0.1:8004'
    return obj

放到中间件中处理复杂和简单请求:

from django.utils.deprecation import MiddlewareMixin
class MyCorsMiddle(MiddlewareMixin):
    def process_response(self, request, response):
        # 简单请求:
        # 允许http://127.0.0.1:8001域向我发请求
        # ret['Access-Control-Allow-Origin']='http://127.0.0.1:8001'
        # 允许所有人向我发请求
        response['Access-Control-Allow-Origin'] = '*'
        if request.method == 'OPTIONS':
            # 所有的头信息都允许
            response['Access-Control-Allow-Headers'] = '*'
        return response

在settings中配置即可,在中间件中的位置可以随意放置.

也可以通过第三方组件:pip install django-cors-headers

# (1)
pip install django-cors-headers

# (2)
 INSTALLED_APPS = (
 'corsheaders',
)
    
# (3)
 MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'corsheaders.middleware.CorsMiddleware',  # 按顺序
    'django.middleware.common.CommonMiddleware',  # 按顺序
 ]
 
 # 配置白名单
 1 CORS_ALLOW_CREDENTIALS = True#允许携带cookie
 2 CORS_ORIGIN_ALLOW_ALL = True
 3 CORS_ORIGIN_WHITELIST = ( '*')#跨域增加忽略
 4 CORS_ALLOW_METHODS = ( 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', 'VIEW', )

#允许的请求头
 CORS_ALLOW_HEADERS = (
     'XMLHttpRequest',
     'X_FILENAME',
     'accept-encoding',
     'authorization',
     'content-type',
     'dnt',
     'origin',
     'user-agent',
     'x-csrftoken',
     'x-requested-with',
     'Pragma',
)

9. 中间件

介于request与response处理之间的一道处理过程,相对比较轻量级,并且在全局上改变Django的输入与输出。因为改变的是全局,所以谨慎使用。

如果你想修改请求,例如被传送到view中的HttpRequest对象。或者你想修改view放回的HttpResponse对象,这些都可以通过中间件实现。

django框架内部声明了很多的中间件,这些中间件有着各种各种的用途,有些没有被使用,有些被默认开启使用了。而被开启使用的中间件,都是在settngs.py的MIDDLEWARE中注册使用的。

Django默认的Middleware:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

9.1 自定义中间件

  1. 定义中间件

    创建存放在自定义中间件的文件,这里选择在app01中创建mdws.py文件:

    from django.utils.deprecation import MiddlewareMixin
    
    
    class Md1(MiddlewareMixin):
        def process_request(self, request):
            print("Md1请求")
            # return HttpResponse("Md1中断")    # 拦截
        def process_response(self, request, response):
            print("Md1返回")
            return response
    
        
    class Md2(MiddlewareMixin):
        def process_request(self, request):
            print("Md2请求")
            # return HttpResponse("Md2中断")
    
        def process_response(self, request, response):
            print("Md2返回")
            return response
    

    process_request:默认返回None,返回None,则继续执行下一个中间件的

    process_request:一旦返回响应体对象,则会拦截返回。

    process_response必须有一个形参response,并return response;这是view函数返回的响应体,像接力棒一样传承给最后的客户端。

  1. 注册中间件

    MIDDLEWARE = [
        ...
        'app01.mdws.Md1',
        'app01.mdws.Md2'
    ]
    
  2. 构建index路由

    # path('index/', views.index),
    
    
    def index(request):
        print("index 视图函数执行...")
        return HttpResponse("hello yuan")
    
  3. 启动项目,访问index路径:

9.2 中间件应用

  1. 对ip访问频率限制

    某些IP访问服务器的频率过高,进行拦截,比如限制每分钟不能超过20次。

9.3 Cookie与Session

  • Cookie

    在HTTP中它表示服务器送给客户端浏览器的cookie。其实Cookie是key-value结构。客户端浏览器会把cookie保存起来,当下一次再访问服务器时把cookie再发送给服务器。

    cookie流程图

第一次访问设置了cookie键值返回给浏览器,浏览器进行了保存,下次访问时会带着这组键值(cookie)去访问浏览器。信息保存在浏览器并不怎么安全。

cookie语法

# (1) 设置cookie:
res = HttpResponse(...) 或 rep = render(request, ...) 或 rep = redirect() 
res.set_cookie(key,value,max_age...)
res.set_signed_cookie(key,value,salt='加密盐',...) 
# (2) 获取cookie:
request.COOKIES     request.get_signed_cookie('xx',salt='xxxxxx')  
# (3) 删除cookie
response.delete_cookie("cookie_key",path="/",domain=name)

当我们访问不设置cookie的视图函数(含有csrf是因为我没有将中间件注释掉,后面会单独讲csrf)

  • session

    Django 提供对匿名会话(session)的完全支持。这个会话框架让你可以存储和取回每个站点访客任意数据。它在服务器端存储数据, 并以cookies的形式进行发送和接受数据。

    ’第一次‘访问服务器,服务器会去请求中所携带的cookie里去get一个sessionid(名字可以去配置里改)的键去获取值,如果没有,则会在服务器创建一个键session_key,对应的值为session_data,(session_data里是与用户相关的一组组数据),将session_key返回给浏览器,此时浏览器里的cookie便多了一组{sessionid:'wfwafa'}的键值。而服务器那遍也可以根据用户的session_key找到对应的session_data从里面获取数据了。

    等到下次访问的时候,服务器根据浏览器请求所携带的cookie找到对应的session_data,再去根据客户要求对里面的东西进行增删改查。

# 1、设置Sessions值
       request.session['session_name'] ="admin"
# 2、获取Sessions值
       session_name = request.session["session_name"]
# 3、删除Sessions值
       del request.session["session_name"]
# 4、flush()
  # 删除当前的会话数据并删除会话的Cookie。这用于确保前面的会话数据不可以再次被用户的浏览器访问
  def s_login(request):
      if request.method == "GET":
          return render(request, "login.html")
      else:
          user = request.POST.get("user")
          pwd = request.POST.get("pwd")
          try:
              # user_obj = User.objects.get(user=user,pwd=pwd)
              # 写session
              # request.session["is_login"] = True
              # request.session["username"] = user_obj.user
              return redirect("/s_index/")
          except:
              return redirect("/s_login/")
  
            
  def s_index(request):
      # 读session
      is_login = request.session.get("is_login")
      if is_login:
          username = request.session.get("username")
          return render(request, "index.html", {"user": username})
      else:
          return redirect("/s_login/")
  '''
  shop.html:
  <p>
  客户端最后一次访问时间:{{ last_time|default:"第一次访问" }}
  </p >
  <h3>商品页面</h3>
  '''
  
  def shop(request):
      last_time = request.session.get("last_time")
      now = datetime.datetime.now().strftime("%Y-%m-%d %X")
      request.session["last_time"] = now
      return render(request, "shop.html", {"last_time": last_time})
  
  
  def s_logout(request):
      # request.session.flush()
      del request.session["username"]
      del request.session["is_login"]
      return redirect("/s_login/")
  

image-20240623232312215

session 在服务器端,cookie 在客户端(浏览器)

session 默认被存在在服务器的一个文件里(不是内存)

session 的运行依赖 session id,而 session id 是存在 cookie 中的.

session 可以放在 文件、数据库、或内存中都可以。

用户验证这种场合一般会用 session

Session配置

Django默认支持Session,并且默认是将Session数据存储在数据库中,即:django_session 表中。

# 配置 settings.py
 
  SESSION_ENGINE = 'django.contrib.sessions.backends.db'   # 引擎(默认)
  
  
  SESSION_COOKIE_NAME = "sessionid"               # Session的cookie保存在浏览器上时的key,即:sessionid=随机字符串(默认)
  SESSION_COOKIE_PATH = "/"                               # Session的cookie保存的路径(默认)
  SESSION_COOKIE_DOMAIN = None                             # Session的cookie保存的域名(默认)
  SESSION_COOKIE_SECURE = False                            # 是否Https传输cookie(默认)
  SESSION_COOKIE_HTTPONLY = True                           # 是否Session的cookie只支持http传输(默认)
  SESSION_COOKIE_AGE = 1209600                             # Session的cookie失效日期(2周)(默认)
  SESSION_EXPIRE_AT_BROWSER_CLOSE = False                  # 是否关闭浏览器使得Session过期(默认)
  SESSION_SAVE_EVERY_REQUEST = False                       # 是否每次请求都保存Session,默认修改之后才保存(默认)

9.4 用户认证组件

Django默认已经提供了认证系统Auth模块,我们认证的时候,会使用auth模块里面给我们提供的表。(基于session进一步封装,帮助我们登录认证)认证系统包含:

  • 用户管理

  • 权限

  • 用户组

  • 密码哈希系统

  • 用户登录或内容显示的表单和视图

  • 一个可插拔的后台系统 admin

  1. Django用户模型类

    Django认证系统中提供了用户模型类User保存用户的数据,默认的User包含以下常见的基本字段:

    字段名 字段描述
    username 必选。150个字符以内。 用户名可能包含字母数字,_@+ .-个字符。
    first_name 可选(blank=True)。 少于等于30个字符。
    last_name 可选(blank=True)。 少于等于30个字符。
    email 可选(blank=True)。 邮箱地址。
    password 必选。 密码的哈希加密串。 (Django 不保存原始密码)。 原始密码可以无限长而且可以包含任意字符。
    groups Group 之间的多对多关系。
    user_permissions Permission 之间的多对多关系。
    is_staff 布尔值。 设置用户是否可以访问Admin 站点。
    is_active 布尔值。 指示用户的账号是否激活。 它不是用来控制用户是否能够登录,而是描述一种帐号的使用状态。
    is_superuser 是否是超级用户。超级用户具有所有权限。
    last_login 用户最后一次登录的时间。
    date_joined 账户创建的时间。 当账号创建时,默认设置为当前的date/time。

    上面缺少一些字段,所以后面我们会对当前内置的用户模型进行改造,比如说它里面没有手机号字段,后面我们需要加上。

  2. 重要方法

    Django 用户认证(Auth)组件需要导入 auth 模块

    # 认证模块
    from django.contrib import auth
    # 对应数据库用户表,可以继承扩展
    from django.contrib.auth.models import User #这张表便是auth_user表
    

    但是记得注册(新增)用户的时候不要用create而是create_user,因为这样保存的密码依旧是铭文的。同理用User.object.get()一样会用铭文去和密文匹配,除非拿到Django的加密算法不然会匹配失败。

    • 用户对象

      create() # 创建一个普通用户,密码是明文的。
      create_user() # 创建一个普通用户,密码是密文的。
      create_superuser() # 与create_user() 相同,但是设置is_staff 和is_superuser 为True。
      
      set_password(*raw_password*)
      # 设置用户的密码为给定的原始字符串,并负责密码的。 不会保存User对象。当None为raw_password时,密码将设置为一个不可用的密码。
      check_password(*raw_password*)
      # 如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。
      
    • 认证方法

      auth.authenticate(username,password) 
      # 将输入的密码转为密文去认证,认证成功返回用户对象,失败则返回None
      
    • 登录和注销方法

      from django.contrib import auth
      # 该函数接受一个HttpRequest对象,以及一个认证了的User对象。此函数使用django的session框架给某个已认证的用户附加上session id等信息。
      auth.login() 
      # 该函数接受一个HttpRequest对象,无返回值。当调用该函数时,当前请求的session信息会全部清除。该用户即使没有登录,使用该函数也不会报错。
      auth.logout() 
      
    • request.user

      Django有一个默认中间件,叫做AuthenticationMiddleware,每次请求进来都会去session中去一个userid,取不到的话,赋值request.user = AnonymousUser() , 一个匿名用户对象。
      当用户组件auth.login一旦执行,将userid到session中后,再有请求进入Django,将注册的userid对应的user对象赋值给request.user,即再后面的任何视图函数中都可以从request.user中取到该客户端的登录对象。

    • 自定义用户表

      from django.contrib.auth.models import AbstractUser

      设置Auth认证模块使用的用户模型为我们自己定义的用户模型

      格式:“子应用目录名.模型类名”

      AUTH_USER_MODEL = 'users.User'

      Django其实有两个配置文件,我们平时用的经常改的在settings.py中,其实还有一个:

      from django.conf import global_settings

    这个全局settings点进去看一下就行了 一般要改的直接在项目目录下的settings里直接覆盖就行

    (app名字.app的model里面的表名)

    from django.contrib.auth.models import AbstractUser
     
    class UserInfo(AbstractUser):
        phone_num = models.IntegerField(verbose_name='电话号')
    

    然后进行数据库迁移

    示例

    我们这里不做注册添加用户和继承续写auth表了,我们在python console自己创建几个用户,记得用create_user()

    (先导入这张表望截图了from django.contrib.auth.models import User)

views.py

def index(request):
    # request.user:当前登录对象
    '''
        1. 在AuthenticationMiddleware中间件的process_request方法中取user_id
            user_id = request.session.get('user_id')
        2.  导入User表 from django.contrib.auth.models import User
            user = User.object.get(pk=user_id)
            if user:
                request.user = user
            else:
                # 取不到返回一个匿名用户 所有属性都为零值(空或者None)
                request.user = AnonymousUser()
        因为是在中间件中完成因此在任何视图函数中都可以使用request.user
        只要完成了登录即auth.login(request, user) request.user就是登录对象
        如果没有登录过或者失败,那就是匿名对象
    '''
    if request.user.id:  # 随便取request.user.name
        return HttpResponse('登陆成功')
    else:
        return HttpResponse('请先去登录')
 
 
def login(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    else:
        account = request.POST.get('account')
        pwd = request.POST.get('pwd')
        # 找到返回对象 没找到返回None
        user = auth.authenticate(username=account, password=pwd)
        if user:
            # request.session['user_id'] = user.pk
            auth.login(request, user)  # 登陆成功后如果是第一次访问session表里的session就新增了
            return redirect(reverse('students:index'))
        else:
            return HttpResponse('登陆失败')
 
def logout(request):
    # request.session.flush()
    auth.logout(request)
    return redirect(reverse('students:login'))

登录前

登录中

登陆后(记得注掉csrf中间件或者form表单里加上{%csrf_token%}

session表
![](https://img2024.cnblogs.com/blog/3088220/202407/3088220-20240707163828759-606627540.png)

9.5 Django分页器

批量插入数据

from django.core.paginator import Paginator

  1. index视图

     
    def index(request):
        '''
        批量导入数据:
        Booklist=[]
        for i in range(100):
            Booklist.append(Book(title="book"+str(i),price=30+i*i))
        Book.objects.bulk_create(Booklist)
        分页器的使用:
        book_list=Book.objects.all()
        paginator = Paginator(book_list, 10)
        print("count:",paginator.count)           #数据总数
        print("num_pages",paginator.num_pages)    #总页数
        print("page_range",paginator.page_range)  #页码的列表
        page1=paginator.page(1) # 第1页的page对象
        for i in page1:         # 遍历第1页的所有数据对象
            print(i)
        print(page1.object_list) #第1页的所有数据
        page2=paginator.page(2)
        print(page2.has_next())            #是否有下一页
        print(page2.next_page_number())    #下一页的页码
        print(page2.has_previous())        #是否有上一页
        print(page2.previous_page_number()) #上一页的页码
        # 抛错
        #page=paginator.page(12)   # error:EmptyPage
        #page=paginator.page("z")   # error:PageNotAnInteger
        '''
     
        book_list = Book.objects.all()
     
        paginator = Paginator(book_list, 10)
        page = request.GET.get('page', 1)
        current_page = int(page)
     
        try:
            print(page)
            book_list = paginator.page(page)
        except PageNotAnInteger:
            book_list = paginator.page(1)
        except EmptyPage:
            book_list = paginator.page(paginator.num_pages)
     
        return render(request, "index.html", {"book_list": book_list, "paginator": paginator, "currentPage": current_page})
    
  2. index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" 
        integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    </head>
    <body>
    
    <div class="container">
    
        <h4>分页器</h4>
        <ul>
    
            {% for book in book_list %}
                 <li>{{ book.title }} -----{{ book.price }}</li>
            {% endfor %}
    
         </ul>
    
    
        <ul class="pagination" id="pager">
    
                     {% if book_list.has_previous %}
                        <li class="previous"><a href="/index/?page={{ book_list.previous_page_number }}">上一页</a></li>
                     {% else %}
                        <li class="previous disabled"><a href="#">上一页</a></li>
                     {% endif %}
    
    
                     {% for num in paginator.page_range %}
    
                         {% if num == currentPage %}
                           <li class="item active"><a href="/index/?page={{ num }}">{{ num }}</a></li>
                         {% else %}
                           <li class="item"><a href="/index/?page={{ num }}">{{ num }}</a></li>
    
                         {% endif %}
                     {% endfor %}
    
    
    
                     {% if book_list.has_next %}
                        <li class="next"><a href="/index/?page={{ book_list.next_page_number }}">下一页</a></li>
                     {% else %}
                        <li class="next disabled"><a href="#">下一页</a></li>
                     {% endif %}
    
                </ul>
    </div>
    </body>
    </html>
    

9.6 FBV与CBV

1 FBV :function based view

2 BCV:class based view

9.6.1 前后段分离模式

在开发Web应用中,有两种应用模式:

​ 1.前后端不分离[客户端看到的内容和所有界面效果都是由服务端提供出来的。

2.前后端分离(把前端的界面效果(html,css,js分离到另一个服务端,python服务端只需要返回数据即可)

前端形成一个独立的网站,服务端构成一个独立的网站

9.6.2 api接口

应用程序编程接口(Application Programming Interface,API接口),就是应用程序对外提供了一个操作数据的入口,这个入口可以是一个函数或类方法,也可以是一个url地址或者一个网络地址。当客户端调用这个入口,应用程序则会执行对应代码操作,给客户端完成相对应的功能。

当然,api接口在工作中是比较常见的开发内容,有时候,我们会调用其他人编写的api接口,有时候,我们也需要提供api接口给其他人操作。由此就会带来一个问题,api接口往往都是一个函数、类方法、或者url或其他网络地址,不断是哪一种,当api接口编写过程中,我们都要考虑一个问题就是这个接口应该怎么编写?接口怎么写的更加容易维护和清晰,这就需要大家在调用或者编写api接口的时候要有一个明确的编写规范!!!

为了在团队内部形成共识、防止个人习惯差异引起的混乱,我们都需要找到一种大家都觉得很好的接口实现规范,而且这种规范能够让后端写的接口,用途一目了然,减少客户端和服务端双方之间的合作成本。

目前市面上大部分公司开发人员使用的接口实现规范主要有:restful、RPC。

RPC( Remote Procedure Call ): 翻译成中文:远程过程调用[远程服务调用]. 从字面上理解就是访问/调用远程服务端提供的api接口。这种接口一般以服务或者过程式代码提供。

  • 服务端提供一个唯一的访问入口地址:http://api.xxx.com/http://www.xx.com/api 或者基于其他协议的地址

  • 客户端请求服务端的时候,所有的操作都理解为动作(action),一般web开发时,对应的就是HTTP请求的post请求

  • 通过请求体参数,指定要调用的接口名称和接口所需的参数

    action=get_all_student&class=301&sex=1

    m=get_all_student&sex=1&age=22

    command=100&sex=1&age=22

rpc接口多了,对应函数名和参数就多了,前端在请求api接口时难找.对于年代久远的rpc服务端的代码也容易出现重复的接口

restful: 翻译成中文: 资源状态转换.(表征性状态转移)

也就是说,我们仅需要通过url地址上的资源名称结合HTTP请求动作,就可以说明当前api接口的功能是什么了。restful是以资源为主的api接口规范,体现在地址上就是资源就是以名词表达。rpc则以动作为主的api接口规范,体现在接口名称上往往附带操作数据的动作。

9.6.3 RESTful API规范

REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。 它首次出现在2000年Roy Fielding的博士论文中。

RESTful是一种专门为Web 开发而定义API接口的设计风格,尤其适用于前后端分离的应用模式中。

这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。

而对于数据资源分别使用POST、DELETE、GET、UPDATE等请求动作来表达对数据的增删查改。

GET /students 获取所有学生
请求方法 请求地址 后端操作
POST /students 增加学生
GET /students/ 获取编号为pk的学生
PUT /students/ 修改编号为pk的学生
DELETE /students/ 删除编号为pk的学生

restful规范是一种通用的规范,不限制语言和开发框架的使用。事实上,我们可以使用任何一门语言,任何一个框架都可以实现符合restful规范的API接口。

参考文档:RESTful 架构详解 | 菜鸟教程

接口实现过程中,会存在幂等性。所谓幂等性是指代客户端发起多次同样请求时,是否对于服务端里面的资源产生不同结果。如果多次请求,服务端结果还是一样,则属于幂等接口,如果多次请求,服务端产生结果是不一样的,则属于非幂等接口。

请求方式 是否幂等 是否安全
GET 幂等 安全
POST 不幂等 不安全
PUT/PATCH 幂等 不安全
DELETE 幂等 不安全

9.6.4 CBV使用

之前我们用的视图函数叫FBV(也就是函数型视图函数),这里我们来试试CBV(类视图函数)的写法。类视图函数可以让代码看起来更简洁,用起来更方便。

# FBV
# def index(request):
#     if request.method == "GET":
#
#         return HttpResponse("GET")
#     elif request.method == "POST":
#
#         return HttpResponse("POST")
#
#     elif request.method == "DELETE":
#         return HttpResponse("DELETE")

# CBV模式: 基于restful开发

class IndexView(View):

    def get(self, request):
        return HttpResponse("CBV GET")

    def post(self, request):
        return HttpResponse("CBV POST")


class BookView(View):

    def get(self, request):
        # 获取数据
        book_list = Book.objects.all()

        # 序列化:json
        data_json = serializers.serialize("json", book_list)

        return HttpResponse(data_json, content_type="json")

# FBV模式
# path('index/', views.index),
# CBV模式
path("index/",views.IndexView.as_view()),
path("books/",views.BookView.as_view())

9.6.5 csrftoken(跨站请求伪造)

CSRF(Cross-Site Request Forgery,跨站点伪造请求)是一种网络攻击方式,该攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在未授权的情况下执行在权限保护之下的操作,具有很大的危害性。具体来讲,可以这样理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

-- 企业邮箱
-- pornhub.com

参考文章

token其实就是一个令牌,用于用户验证的,token的诞生离不开CSRF。正是由于上面的Cookie/Session的状态保持方式会出现CSRF,所以才有了token。

  • 解除中间件注释
  • 无csrf_token数据的post请求

基本使用
一、form表单提交

在html页面form表单中直接添加{% csrf_token%}

多了句

<input type="hidden" name="csrfmiddlewaretoken" value="HL4osWvcOA03Mv4YEaBmUGdujF4axvtcmvM02jhbrilpgRW7wLf1e2cmUE7r8CC4">

64位随机字符串,前32是salt后32是token

这样提交post请求时会多一组键值对,执行到 'django.middleware.csrf.CsrfViewMiddleware'中间件的时候内部会进行校验。浏览器请求携带cookie里的token和请求头/体里的token进行比对(两者不一样)。

下同

二、ajax提交

也要导入 {% csrf_token %} 不然没有[name="csrfmiddlewaretoken"]

方式1:放在请求数据中。

$.ajax({
  url: '/csrf_test/',
  method: 'post',
  data: {'name': $('[name="name"]').val(),
         'password': $('[name="password"]').val(),
         'csrfmiddlewaretoken':$('[name="csrfmiddlewaretoken"]').val()           
        },
  success: function (data) {
    console.log('成功了')
    console.log(data)           },
})

方式2:放在请求头(个人建议)

$.ajax({
            url: '/csrf_test/',
            method: 'post',
            headers:{'X-CSRFToken':'token值'},  // 注意放到引号里面
            data:{}
}

9.6.6 全局使用,局部禁csrf

(1) 在视图函数上加装饰器

from django.views.decorators.csrf import csrf_exempt,csrf_protect
@csrf_exempt
def 函数名(request):  # 加上装饰器后,这个视图函数,就没有csrf校验了

(2) 视图类

from django.views.decorators.csrf import csrf_exempt,csrf_protect
from django.utils.decorators import method_decorator
@method_decorator(csrf_exempt,name='dispatch')
class index(View):
    def get(self,request):
        return HttpResponse("GET")
    def post(self,request):
        return HttpResponse("POST")

示例(前后端分离 不适用渲染{%csrf_token%}

def login(request):
    if request.method == 'GET':
        print('oooooooopppppppppppp')
        return render(request, 'students/login.html')
    else:
        account = request.POST.get('account')
        pwd = request.POST.get('password')
        user = auth.authenticate(username=account, password=pwd)
        dct = {'state': False, 'msg': '登陆失败'}
        if user:
            auth.login(request, user)
            dct['state'] = True
            dct['msg'] = ''
        return JsonResponse(dct)


def get_tokens(request):
    tok = get_token(request)
    return HttpResponse(tok)

 
<script>
    $.ajax({
        url:'http://127.0.0.1:8000/students/get_tokens/',
        success:function (res){
            console.log(res)
            localStorage.setItem('token',res)
        }
    })
    $('#aja_btn').click(function (){
        $.ajax({
            // headers:{'X-CSRFToken':localStorage.getItem('token')},
            url: 'http://127.0.0.1:8000/students/login/',
            type:'post',
            data:{
                csrfmiddlewaretoken:localStorage.getItem('token'),
                account:$('#Input').val(),
                password:$('#Password').val()
            },
            success:function (res){
                console.log(res)
                if(res.state){
                    location.href = '/students/index/'
                }else {
                    $('#err').html(res.msg)
                }
            }
        })
    })
</script>

记得要确保get_tokens在白名单中。 当然上面的示例放在头里也可以,当头和体里都找不到的时候才会出问题。

原理:

我们可以看到 cookie中的csrf-token和我们的token并不一样

当我们的csrf中间件在头或者数据中找到我们提交的token时,由于每次请求携带着cookie,它会根据这两组字符串进行比对,解出唯一的secret。(前32位为加密盐,后32位为数据)

注:不光是我们自己写的网站有这一机制,其它网站也存在,如csdn:

9.6.7 上传文件

form表单上传文件
(记得修改表单提交的编码方式)

form表单上传文件

<form action="/upload_file/" method="post" enctype="multipart/form-data">
    <p><input type="file" name="upload_file_form"></p>
    <input type="submit">
</form>
def index(request):
    return render(request,"index.html")

def upload_file(request):
    print("FILES:",request.FILES)# 得到文件对象类字典
    print("POST:",request.POST)# 得到类字典里有csrftoken(如果编码方式没变 还能得到文件名)
    file = request.FILES
    with open(file.name,'wb') as f:
        for i in file:
            f.write(i)
    return HttpResponse("上传成功!")

写文件的时候记得用‘wb' 文件名可以直接 request.FILES.get('file').name

9.6.8 Ajax(基于FormData)

FormData是什么呢?

XMLHttpRequest Level 2添加了一个新的接口FormData.利用FormData对象,我们可以通过JavaScript用一些键值对来模拟一系列表单控件,我们还可以使用XMLHttpRequest的send()方法来异步的提交这个"表单".比起普通的ajax,使用FormData的最大优点就是我们可以异步上传一个二进制文件.

所有主流浏览器的较新版本都已经支持这个对象了,比如Chrome 7+、Firefox 4+、IE 10+、Opera 12+、Safari 5+。

<h3>Ajax上传文件</h3>
<p><input type="text" name="username" id="username" placeholder="username"></p>
<p><input type="file" name="upload_file_ajax" id="upload_file_ajax"></p>
<button id="upload_button">提交</button>
{#注意button标签不要用在form表单中使用#}

<script>
    $("#upload_button").click(function(){
        var username=$("#username").val();
    var upload_file=$("#upload_file_ajax")[0].files[0];

    var formData=new FormData();
    formData.append("username",username);
    formData.append("upload_file_ajax",upload_file);
     $.ajax({
        url:"/upload_file/",
        type:"POST",
        data:formData,
        contentType:false,
        processData:false,

        success:function(){
            alert("上传成功!")
        }
    });
})
</script>
def index(request):
  
    return render(request,"index.html")
  
  
def upload_file(request):
    print("FILES:",request.FILES)
    print("POST:",request.POST)
    return HttpResponse("上传成功!")

9.7 ImageField 和 FileField

ImageField 和 FileField 可以分别对图片和文件进行上传到指定的文件夹中。

1.在下面的 models.py 中 :

picture = models.ImageField(upload_to='avatars/', default="avatars/default.png",blank=True, null=True)
注:定义 ImageField 字段时必须制定参数 upload_to这个字段要写相对路径,

这个参数会加在 settings.py 中的 MEDIA_ROOT后面, 形成一个路径, 这个路径就是上 传图片的存放位置,默认在Django项目根路径下,也就是MEDIA_ROOT默认是Django根目录

所以要先设置好 mysite/settings.py中的 settings.py 中的 MEDIA_ROOT

class Userinfo(models.Model):
    name = models.CharField(max_length=32)
    avatar_img = models.FileField("avatars/") 
username = request.POST.get("username")
#获取文件对象
file = request.FILES.get("file")   
#插入数据,将图片对象直接赋值给字段
user = Userinfo.objects.create(name=username,avatar_img=file)

Django会在项目的根目录创建avatars文件夹,将上传文件下载到该文件夹中,avatar字段保存的是文件的相对路径。

2.在 mysite/settings.py中 :

MEDIA_ROOT = os.path.join(BASE_DIR,"media")
MEDIA_URL='/media/'

MEDIA_ROOT:存放 media 的路径, 这个值加上 upload_to的值就是真实存放上传图片文件位置

MEDIA_URL:给这个属性设值之后,静态文件的链接前面会加上这个值,如果设置这个值,则UserInfo.avatar.url自动替换成:/media/avatars/default.png,可以在模板中直接调用:

3.url.py:

from django.views.static import serve

# 添加media 配置

re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),

最后再给大家补充一个用户文件夹路径

def user_directory_path(instance, filename):
    return os.path.join(instance.name,"avatars", filename)

class Userinfo(models.Model):
    name = models.CharField(max_length=32)
    avatar_img = models.FileField(upload_to=user_directory_path)  

4.FileField 和 ImageFiled 相同。

posted @ 2024-07-07 16:41  小郑[努力版]  阅读(63)  评论(0编辑  收藏  举报