Django学习实战篇二(适合略有基础的新手小白学习)(从0开发项目)

前言:

从这一章开始,我们来创建项目typeidea,我把它放到了GitHub上。强烈建议你也到GitHub上注册一个账号(如果没有的话),然后创建这样的项目。当然,你也可以起一个属于自己的名称。这个项目就是本系列博客要开发的多人博客系统。我建议你跟着本系列博客的节奏不断完善这个项目,同时不断把它们同步到GitHub上来记录自己的成长以及项目每一步的变更。

在这一章中,我们会根据需求完成整个Model 层的创建,理解Django中Model部分的知识点。我们始终保持学习的最佳实践:先实践,后总结。

第二章:奠定项目基石:Model

2.1 创建项目及配置

这里我用的是PyCharm来完成项目开发的,Django项目需要安装PyCharm专业版 ,还没有安装PyCharm专业版的可以看我这篇博客:pycharm安装与专业版永久使用

  1. 创建虚拟环境:

打开PyCharm,切换到项目目录,新建项目,创建python虚拟环境,如下面两图所示

创建好后页面如图所示:

打开终端页面检查是否安装成功且运行成功,如下图出现(.venv)则安装成功:

  1. 安装Django 4.2版本:

这里通过== 方式指定Django的特定版本,不加 ==则表示安装Django最新版本(一般不会使用最新版本,因为最新版本可能会跟其他库不兼容或者遇到bug没法及时解决,老版本遇到bug社区一般都能找到解决方案)

在PyCharm终端中输入如下命令:

bash 复制代码
pip install django==4.2
  1. 创建项目typeidea:

在PyCharm终端中输入如下命令:

bash 复制代码
django-admin startproject typeidea
  1. 切换到typeidea:
  1. 用PyCharm打开django:


这样就可以在PyCharm中打开django了

如下图所示,django安装成功

2.1.1 配置settings

我们需要修改settings配置、时区和语言配置:

python 复制代码
# settings文件中需要修改的部分,其他部分省略
LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

2.1.2 配置Git

如果你还没有学习过git,建议你先进行git学习再来部署项目,我也写了关于git的博客专栏,里面有git的学习笔记和学习git中遇到的问题和解决方案git学习笔记(总结了常见命令与学习中遇到的问题和解决方法)

在你的GitHub页面或者公司内部的GitLab上创建项目typeidea,完成后会得到一个仓库地址。之后我们来用Git管理项目。进入项目根目录typeidea中,执行命令git init,然后创建我们需要忽略的文件配置,gitignore,具体内容如下:

*.pyC
*.SwP
*.sqlite3

创建gitignore文件

进入git管理页面

对git进行初始化

这里忽略的是那些我们不希望进入Git管理的文件。另外,一些敏感文件以及二进制文件,如果不是特别需要,尽量别放到版本管理里面。这块可以参考GitHub上一个名为gitignore的项目,里面有各种语言需要忽略的文件后缀。

然后通过下面几个命令添加项目到Git仓库中:

bash 复制代码
git add .
git commit -m '初始化提交'

之后需要配置远程仓库:

bash 复制代码
git remote add origin <你的远端仓库地址,GitHub创建时得到的>
git push -u origin master

如图所示,GitHub初始化部署成功

(实操演示)

现在完成了正式项目的第一步,下一步根据需求编写我们的Model代码。

2.1.3 总结

现在的这些操作都是为后面的开发打下一个好的基础。如果你对上面的内容不怎么熟悉,那么有必要把它们全部练习一遍。

2.1.4 参考资料

git学习视频:

https://www.bilibili.com/video/BV19E411f76x?spm_id_from=333.999.0.0
Python-gitignore:

https://github.com/github/gitignore/blob/master/Python.gitignore.

2.2 编写Model层的代码

在上一节中,我们花了一点时间来设定项目的结构,这会为之后的流程打下一个不错的基础。

你可能会觉得有些烦琐了,心里可能在想,为何还不赶紧进入编码阶段。

现在就满足你,我们开始写Model层的代码。

按照上一节的结构整理完项目之后,我们来创建Model层的代码。所谓Model,就是我们的数据模型。模型从何而来呢,就是从前面的需求中整理出来的。对于内容或者说数据驱动的项目来说,设计好模型是成功的一半,因为后续的所有操作都是基于Model的。我们先来看一下之前整理好的模型关系图,如下图所示。

需要说明的是,图中的User模型可以直接使用Django自带的。另外,需要关注其中一对多和多对多的关系。这里不得不再提醒一下,在大型项目设计中,常常需要借助一些工具,如UML、E-R图、思维导图等来帮助我们可视化地分析项目结构。所以有必要学习一两种工具,来辅助自己进行设计。

2.2.1 创建App

设计好模型之后,就相当于捋清了业务中的数据模型,接下来需要做的就是编写上层业务代码。对于其他框架来说,可能需要你自行设计项目的文件结构。但是在Django中不用担心这个问题,Django会给你一个初始化的结构。前面也提到过Django中App(应用)的概念,每个App应该是一个自组织的应用(所谓自组织,是指应用内部的所有逻辑都是相关联的,可以理解为是紧耦合的)。我们既可以把上面的所有模型放到一个App中,也可以根据Mode1的业务性质来分别处理。至于如何划分,没有标准,也没有最佳实践,因为每个公司的业务情况和团队组成都不同。但有一些原则可以参考:易维护、易扩展。

这里我们把所有Model划分为三类:blog 相关、配置相关和评论相关。这么分的好处是便于我们独立维护各个模块,也便于在开发时分配任务。

  1. blog App

在创建App和编写代码之前,你可以尝试创建一个新的分支:

bash 复制代码
git checkout -b add-blog-app-model

然后创建一个blog的App:使用cd typeidea 进入上节创建好的项目中,执行命令python manage.py startapp blog即可。

此时我们得到现在的结构:

现在来编写 blog/models.py 中的代码。根据一开始列出的模型,先创建博客内容相关的模型:

python 复制代码
from django.contrib.auth.models import User
from django.db import models


class Category(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )

    name = models.CharField(max_length=50, verbose_name='名称')
    status = models.PositiveIntegerField(choices=STATUS_ITEMS, default=STATUS_NORMAL, verbose_name='状态')
    is_nav = models.BooleanField(default=False, verbose_name='是否为导航')
    owner = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = verbose_name_plural = '分类'


class Tag(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )

    name = models.CharField(max_length=10, verbose_name='名称')
    status = models.PositiveIntegerField(choices=STATUS_ITEMS, default=STATUS_NORMAL, verbose_name='状态')
    owner = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = verbose_name_plural = '标签'


class Post(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_DRAFT = 2
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
        (STATUS_DRAFT, '草稿')
    )

    title = models.CharField(max_length=255, verbose_name='标题')
    desc = models.CharField(max_length=1024, blank=True, verbose_name='摘要')
    content = models.TextField(verbose_name='正文', help_text="正文必须为MarkDown格式")
    status = models.PositiveIntegerField(choices=STATUS_ITEMS, default=STATUS_NORMAL, verbose_name='状态')
    category = models.ForeignKey(Category, verbose_name='分类', on_delete=models.CASCADE)
    tag = models.ManyToManyField(Tag, verbose_name='标签')
    owner = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = verbose_name_plural = '文章'
        ordering = ['-id']  # 根据id进行降序排序

可以看出来,这几个Model 都是跟内容直接相关的,因此我们把它们放到一个App中。其他Model接下来再做拆分。

对于其中的models.CharFiela或者models.TextFiela,你可能有些疑惑,这些类型就是这个模型中字段的类型。比如CharFiela 和TextFiela分别对应数据库中不同的类型,其他的也类似,后面会详细解释。

每个 Model 中都会定义一个 Meta类属性,它的作用是配置Model 属性,比如Post这个Model,通过Meta配置它的展示名称为文章,排序规则是根据id降序排列。Meta还有很多其他配置,详细内容可以通过2.2.6节中的链接查看。

Model以及字段类型一起构成了ORM(关于ORM的知识,2.3.1节会详细介绍)。

  1. 编写configApp代码

接下来,创建另外一个Django的 App------confg,它用来放置其他几个模型,前面那个blog App用来放内容相关的数据,这个用来放配置相关的数据------侧边栏和友链。

跟前面一样,执行命令,python manage.py startapp config 创建 confg App。现在整体的目录结构如下:

接下来,编写conig/models.py中的代码:

python 复制代码
from django.contrib.auth.models import User
from django.db import models


class Link(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )

    title = models.CharField(max_length=50, verbose_name='标题')
    href = models.URLField(verbose_name='链接')  # 默认长度200
    status = models.PositiveIntegerField(choices=STATUS_ITEMS, default=STATUS_NORMAL, verbose_name='状态')
    weight = models.PositiveIntegerField(default=1, choices=zip(range(1, 6), range(1, 6)),
                                         verbose_name='权重', help_text="权重高展示顺序靠前")

    owner = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = verbose_name_plural = '友链'


class SideBar(models.Model):
    STATUS_SHOW = 1
    STATUS_HIDE = 0
    STATUS_ITEMS = (
        (STATUS_SHOW, '展示'),
        (STATUS_HIDE, '隐藏'),
    )
    SIDE_TYPE = (
        (1, 'HTML'),
        (2, '最新文章'),
        (3, '最热文章'),
        (4, '最近评论'),
    )
    title = models.CharField(max_length=50, verbose_name='标题')
    display_type = models.PositiveIntegerField(default=1, choices=SIDE_TYPE, verbose_name='展示类型')
    content = models.CharField(max_length=500, blank=True, verbose_name='内容',
                               help_text='如果设置的不是HTML类型,可为空')
    status = models.PositiveIntegerField(choices=STATUS_ITEMS, default=STATUS_SHOW, verbose_name='状态')
    owner = models.ForeignKey(User, verbose_name='作者', on_delete=models.CASCADE)
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = verbose_name_plural = '侧边栏'

配置部分的功能暂时如此,后面会在其中加入更多配置。这具体取决于后续的需求。任何一款产品都不是一蹴而就的,都是经过不断迭代、不断调整才形成最终或者说最合适的形态。而对于技术人员来说,需要做的就是理解产品是不断变化的,然后提供相应的技术支持。

  1. 创建评论App

最后,我们来创建评论部分,这部分单独拎出来。因为评论可以是完全独立的模块,如果往大了做,可以作为独立的系统,比如:畅言、Disqus等产品。我们可以把它耦合到文章上,创建一个一对多的关系。当然,我们也可以做得松耦合一点,评论功能完全独立,只关心针对哪个页面(或者 URL)来评论。这样做的好处是,产品可以增加新的页面类型,比如友链页增加评论或者文章列表页增加评论,只关心URL,而不用关心要评论的对象是什么。

我们暂时按照耦合的方式来做,即通过外键关联 Post的方式。

同前面一样,创建comment的 App。想必你应该轻车熟路了,这里就不多说了。创建好App之后,编写models.py中的代码,代码如下:

python 复制代码
from django.db import models

from blog.models import Post


class Comment(models.Model):
    STATUS_NORMAL = 1
    STATUS_DELETE = 0
    STATUS_ITEMS = (
        (STATUS_NORMAL, '正常'),
        (STATUS_DELETE, '删除'),
    )
    target = models.ForeignKey(Post, verbose_name='评论目标', on_delete=models.CASCADE)
    content = models.CharField(max_length=2000, verbose_name='内容')
    nickname = models.CharField(max_length=50, verbose_name='昵称')
    website = models.URLField(verbose_name='网站')
    email = models.EmailField(verbose_name='邮箱')
    status = models.PositiveIntegerField(choices=STATUS_ITEMS, default=STATUS_NORMAL, verbose_name='状态')
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:
        verbose_name = verbose_name_plural = '评论'

到此为止,Model部分已经实现完毕了。写了这么多代码,想必你可能会有很多困惑,比如代码里的CharFiela是什么意思,PositiveIntegerFiela 又是什么意思。目前你需要理解的是,每一个 Model相当于 MySQL库中的一张表,其中Field相当于数据库中的一个字段。

更详细的内容下一节来介绍。这里有了大体的概念之后,先把编好的这些代码用起来。

2.2.2 配置 INSTALLED APPS

创建好 App,编写好对应的Model代码之后,我们需要把这些App(blog、config和comment)放到settings 配置中,才能让Django启动时识别这些App。

修改settings.py

python 复制代码
INSTALLED_APPS = [
    # 新增内容
    'blog.apps.BlogConfig',
    'config.apps.ConfigConfig',
    'comment.apps.CommentConfig',
    
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

这个INSTALLED_APPS 的列表顺序需要格外注意,Django是根据这些 App的顺序来查找对应资源的。比如后面会用到 static 和templates 这些模块,Django会根据顺序挨个去这些 App下查找对应资源,这意味着如果资源路径和名称相同的话,前面的会覆盖掉后面的。

因此,如果在开发中有需要覆盖Django 自带的admin模板的需求,可以根据admin 模板的路径和名称写一份一样的。Django会加载你重写的(如果你的App靠前的话)。你现在可能还没概念,后面用到的时候就会理解。

不过这并不是覆盖自带模板的最佳方式,因为这是基于对Django的理解,有些隐晦,可能会给不熟悉的人"刨坑"。后面会介绍,可以在代码中通过制定模板的方式来自定义页面。

2.2.3 创建数据库[表]

配置好Model和App之后,需要做的就是配置数据库。理论方面可以先不做了解,单纯来看看我们所编写的代码的具体表现。

首先进入PyCharm终端,然后执行,python manage.py makemigrations,如果没报错的话,应该能看到类似下面的结果:

这里直接执行迁移操作,执行命令 python manage.py migrate ,没报错的情况下能得到如下结果:

这时在你的 typeidea 目录下(也就是settings同级目录)会多出一个db.sqlite3文件,这就是Django中默认使用的数据库。

当然,因为这里使用的是SQLite3数据库,也就是项目刚创建时Django帮我们默认创建的,但是如果配置MySQL或者其他关系型数据库,需要先创建好对应的数据库,然后再来创建表(通过上面的两个命令)。

到此为止,Model和数据库就创建完成了,我们可以尝试查看数据库。这里可以直接在PyCharm中对数据库进行操作(可能需要安装别的插件)。

可以看到,除了我们创建的Model对应的表之外,还有Django内置的Model对应的表。好了,到此为止,App和 Model配置可以告一段落了。

接下来,我们需要提交代码。

2.2.4 提交代码

我们可以通过命令git add .或者git add <对应文件>把变更提交到暂存区。

添加完之后,执行命令git commit,会进入Vim的编辑器界面(也可能是nano编辑器界面,具体取决于你的配置),然后输入本次提交说明:增加项目Model。当然,你也可以使用git commit-m'增加项目说明'的方式直接提交。

通过git add . 命令提交。然后再git commit -m '完成App和Model代码的创建'

创建完commit之后,你可以回到主分支:git checkout master,然后把 add-blog-app-model分支否开进来:git merge add-blog-app-model。如果你是通过GitHub 管理的代码,不妨尝试通过Pull Request的方式来把项目合并到主分支中。


2.2.5 总结

根据前面设计好的数据关系图,可以很容易编写出Model代码。而在Django中,当我们有了Model代码后,又可以很容易创建好对应的数据库表。这其实就是ORM的功劳,很多时候我们不需要去写SQL语句来创建表。

2.2.6 参考资料

Django Model Meta:

https://docs.djangoproject.com/zh-hans/4.2/ref/models/options/。

2.3 Model层:字段介绍

跟着上一节写完这些Model以及对应的字段之后,你可能会疑惑这些字段分别是什么意思,以及为什么要这么写。还有为什么定义好那些代码之后,Django就能帮我们创建表了?这一节就来详细解释一下。

2.3.1 ORM的基本概念

在进行详细的字段介绍之前,先来了解一下什么是ORM(Object Relational Mapping,对象关系映射)。

"对象关系映射"听起来有点学术化,不太好理解。用大白话解释一下就很容易明白,那就是把我们定义的对象(类)映射到对应的数据库的表上。所以ORM就是代码(软件)层面对于数据库表和关系的一种抽象。

Django 的Model就是ORM的一个具体实现。

简单来说,就是继承了Django 的 Model,然后定义了对应的字段,Django就会帮我们把Model对应到数据库的表上,Model中定义的属性(比如:name=models.CharField(max_length=50,verbose_name="名称"))就对应一个表的字段。所以一个Model也就对应关系数据库中的一张表,而对于有关联关系的Model,比如用到了ForeignKey 的Model,就是通过外键关联的表。

举个例子来说:

python 复制代码
class Foo(models.Model):
	name = models.CharField(max_length=20)

假设上面这个例子可以对应到数据库的表:

表中自增id是Django的Model内置字段,可以被重写。

通过这个表以及上面的代码,想必你有了一点感觉。类中的属性对应MySQL中的字段,属性的类型对应 MySOL字段的类型。属性定义时传递的参数定义了字段的其他属性,比如长度、是否允许为空等。

在MySQL中,一个表中的字段有多种类型,比如int、varchar和datetime等。因此,我们在定义Model中的字段时,就需要不同的类型,比如上面定义的name或者上一节定义的其他类型。比如created_time=models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),就能把属性created_time 对应到MySQL中类型为datetime的created_time字段上,其中verbose_name 是页面上展示用的,也相当于我们对字段的描述,auto_now_add是DateTimeFiela特有的参数,配置为True时Django会默认填充上当前时间。

Model 中字段的类型跟 MySQL中字段的类型相对应是ORM中基本的规则,理解了字段类型跟数据库的映射规则,再思考一下Model的定义跟表的对应,你就能理解什么是ORM了。其实就是把我们定义的数据模型对应到数据库的表上,或者反过来说也成立,把数据库的表对应到我们定义的数据模型上。理解了这些还不够,数据库有数据操作语言(DML),可以通过SOL语句对数据做CRUD(增、查、改、删)操作,那么Model怎么处理这样的逻辑呢?别着急,下节就来着重介绍QuerySet 的用法。这里我们先对Model中的字段定义有一个完整的了解。

在前面的代码中,你可能注意到有些字段中有choices 这样的参数:status =models.PositiveIntegerField(default=STATUS_NORMAL,choices=STATUS_ITEMS, verbose_name,"状态"),它是做什么用的呢?这就是跟展现层相关的逻辑了,后面讲到admin部分时,还会详细介绍。这里需要先理解,对于有choices的字段,在admin后台,Django会提供一个下拉列表让用户选择,而不是填写,这对于用户来说非常友好。

2.3.2 常用字段类型

理解了ORM的基本概念和规则之后,剩下需要了解的就是具体实现。有了基础规则的理解之后,下面这些工具性质的东西会变得很简单。我们把Django中常用的字段类型以及参数配置进行说明。这里可以根据类型来划分,这其实就是数据库中字段类型的划分。

  1. 数值型

    这些类型都是数值相关的,比如AutoFiela,上面也看到了它在MySQL中的类型为int(11),而BooleanFiela在MySQL中对应的类型是tinyint(1)。下面对每个字段做简单介绍。

    • AutoField int(11)。自增主键,Django Model默认提供,可以被重写。它的完整定义是id =models.AutoField(primary_key=True)。
    • BooleanField tinyint(1)。布尔类型字段,一般用于记录状态标记。
    • DecimalField decimal。开发对数据精度要求较高的业务时考虑使用,比如做支付相关、金融相关。定义时,需要指定精确到多少位,比如 cash = models.DecimalField(max_digits=8, decimal_places=2, default=0, verbose_name="消费金额")就是定义长度为8位、精度为2位的数字。比方说,你想保存666.66 这样的数字,那么你的max_digits就需要为5,decimal_places 需要为2。同时需要注意的是,在 Python 中也要使用 Decimal 类型来转换数据(from decimal import Decimal)。
    • IntegerField int(11)。它同AutoField一样,唯一的差别就是不自增。
    • PositiveIntegerField。同IntergerField,只包含正整数。
    • SmallIntegerField smallint。小整数时一般会用到。
  2. 字符型

    下面这些字段都是用来存储字符数据的,对应到MySQL中有两种类型:longtext 和 varchar除了TextField是longtext类型外,其他均属于varchar 类型。为什么会有这么多都是varchar类型但名字却不相同的字段类型呢?其实看看字段类型的命名就能猜到。数据存储都是基于varchar的,但是上层业务可以有多种展示,最常见的比如URLField,顾名思义,它用来存储URL数据。非URL数据可以在业务层就拒绝掉,不会存入数据库中。

    这些都是比较常用的字段,下面来逐个解释一下。

    • CharField varchar。基础的varchar类型。

    • URLField。继承自CharFiela,但是实现了对URL的特殊处理。

    • UUIDField char(32)。除了在 PostgreSQL中使用的是uuid类型外,在其他数据库中

      均是固定长度char(32),用来存放生成的唯一id.

    • EmailField。同URLField一样,它继承自CharField,多了对E-mail的特殊处理。

    • FileField。同URLField一样,它继承自CharField,多了对文件的特殊处理。当你定义一个字段为FileField时,在admin部分展示时会自动生成一个可上传文件的按钮。

    • TextField longtext。一般用来存放大量文本内容,比如新闻正文、博客正文。

    • ImageField继承自FileField,用来处理图片相关的数据,在展示上会有不同。

  3. 日期类型

    下面3个都是日期类型,分别对应MySQL的date、datetime和time,似乎也不需要过多解释:

    • DateFiela
    • DateTimeField
    • TimeField
  4. 关系类型

    这是关系型数据库中比较重要的字段类型,用来关联两个表,具体如下:

    • ForeignKey
    • OneToOneField
    • ManyToManyField

    其中外键和一对一其实是一种,只是一对一在外键的字段上加了unique。而多对多会创建一个中间表,来进行多对多的关联。

2.3.3参数

上面只是介绍了常用的类型以及不同字段类型的差异。接着,我们需要了解这些字段类型都提供了哪些参数供我们使用,以及这些参数的作用。这里需要意识到的一点是,这些类型就是Python 中的类,比如 models.CharField的定义就是这样:class CharField。这些参数都是类在实例化时传递的。

下面我们列一下参数,并给一个简单的说明,这样大家在使用时可以根据具体需求配置对应参数。

  • null。可以同blank对比考虑,其中null用于设定在数据库层面是否允许为空。
  • blank。针对业务层面,该值是否允许为空。
  • choices。前面介绍过,配置字段的choices后,在admin页面上就可以看到对应的可选项展示。
  • db_column。默认情况下,我们定义的Field就是对应数据库中的字段名称,通过这个参数可以指定Model中的某个字段对应数据库中的哪个字段。
  • db_index。索引配置。对于业务上需要经常作为查询条件的字段,应该配置此项。
  • default。默认值配置。
  • editable。是否可编辑、默认是True。如果不想将这个字段展示到页面上,可以配置为False。
  • error_messages.用来自定义字段值校验失败时的异常提示,它是字典格式。key的可选项为null、blank、invalid、invalidchoice、unique 和unique_for_date。
  • help_text。字段提示语,配置这一项后,在页面对应字段的下方会展示此配置。
  • primary_key。主键,一个Model只允许设置一个字段为primary_key。
  • unique。唯一约束,当需要配置唯一值时,设置unique=True,设置此项后,不需要设置db_index。
  • unique_for_date。针对date(日期)的联合约束,比如我们需要一天只能有一篇名为《学习Django实战》的文章,那么可以在定义title字段时配置参数:unique_for_date="created_time"。
    需要注意的是,这并不是数据库层面的约束。
  • unique_for_month。针对月份的联合约束。
  • unique _for_year。针对年份的联合约束。
  • verbose_name.字段对应的展示文案。
  • validators。自定义校验逻辑,同form类似,我们在介绍form时会介绍。

到此为止,你应该对Model中的字段有一些基本了解了。碍于篇幅,这里并没有完全列出所有内容,更多的内容还需要你到Django网站去查看。

这一部分只是对应了数据库的表定义,没有涉及如何操作这些数据库。

2.3.4 总结

上面的内容多少有点罗列功能点的味道,你只需要熟悉就好。随着学习的深入,你会慢慢理解的。

2.3.5 参考资料

Django Model Fields:

https://docs.djangoproject.com/zh-hans/4.2/ref/models/fields/。

2.4 Model层:QuerySet的使用

有了上一节的认识,再次看到Mode1的定义时,你就能够很容易联想到对应的MySOL中的

表以及字段类型。在这一节中,我们就来介绍如何通过Django的Model操作数据库。

2.4.1 QuerySet的概念

在Django的Model中,QuerySet 是一个重要的概念,必须了解!因为我们同数据库的所有查询以及更新交互都是通过它来完成的。

创建完Model并建好数据库表之后,接下来要做的就是创建admin界面和开发前台页面。在上一节中,我们详细介绍了Model中的字段和ORM中的字段的作用,这些都属于细节层面的东西,有助于你理解 Django 是如何帮我们从Model转换到数据库的。

在这一节中,我们将学习更高层面的东西------Model 细节之外的东西。Django 算是标准的MVC框架,虽然因为它的模板和View的概念被大家戏称为"MTV"的开发模式,但是道理都是一样的。Model作为MVC模式中的基础层(也可以称为数据层),负责为整个系统提供数据。因此,我们需要先理解一下它是如何提供数据的。

在Model 层中,Django通过给 Modcl增加一个objects属性来提供数据操作的接口。比如,想要查询所有文章的数据,可以这么写:Post.objects.all(),这样就能拿到queryset 对象。这个对象中包含了我们需要的数据,当我们用到它时,它会去 DB中获取数据。

这样的描述你可能会觉得奇怪,为什么是用到数据时才会去 DB中查询,而不是执行Post.objects.all()时去执行数据库查询语句。其原因是QuerySet 要支持链式操作。如果每次执行都要查询数据库的话,会存在性能问题,因为你可能用不到你执行的代码。举个例子,也顺便说下链式调用。

比方说,我们有下面的代码:

python 复制代码
posts = Post.objects.all()
available_posts = posts.filter(status=1)

如果这条语句要立即执行,就会出现这种情况:先执行Post.objects.all(),拿到所有的数据posts,然后再执行过滤,拿到所有上线状态的文章available_posts,这样就会产生两次数据库请求,并且两次查询存在重复的数据。

当然,平时可能不会出现这么低级的错误,但是当代码比较复杂时,谁也无法保证不会出现类似的问题。

因此,Django中的QuerySet 本质上是一个懒加载的对象,上面的两行代码执行后,都不会产生数据库查询操作,只是会返回一个QuerySet 对象,等你真正用它时才会执行查询。下面通过代码解释一下:

python 复制代码
posts =Post.objects.all() # 返回一个QuerySet 对象并赋值给posts
available_posts = posts.filter(status=1) # 继续返回一个QuerySet 对象并赋值给 available_posts
print(available_posts)# 此时会根据上面的两个条件执行数据查询操作,对应的SQL语句为:
					# SELECT * FROM blog_post where status =1;

所以这部分的重点就是理解QuerySet 是懒加载的。在日常开发中,我们遇到的一部分性能问题就是因为开发人员没有理解QuerySet特性。

另外,上面说到链式调用,这又是什么概念呢?其实根据上面的代码,你应该能猜到了。链式调用就是,执行一不对象中的方法之后得到的结果还是这个对象,这样可以接着执行对象上的其他方法。比如下面这个代码:

python 复制代码
posts = Post.objects.filter(status=1).filter(category_id=2).filter(title_icontains="不染是非")

在每个函数(或者方法)的执行结果上可以继续调用同样的方法,因为每个函数的返回值都是它自己,也就是QuerySet。

如果有兴趣的话,可以尝试自行来实现一个支持链式调用的对象。另外,也可以考虑一下这种编程方式带来的好处。这是一种更加自然的对数据进行处理的方式。想象一下数据就是水流,而方法就是管道,把不同的管道接起来形成"链",然后让数据流过。

2.4.2 常用的QuerySet接口

好了,回到正题,编程上有很多理念或者习惯,我们在接下来的实践中会不断学到。现在来看看Model层是如何通过QuerySet 为上层提供接口的。比如上面用到了 Post.objects.filter(status=1)里面的filter,除了这个外,还有哪些呢?

在这一节中,我们来看看常用的接口。这里我根据是否支持链式调用分类进行介绍。

  1. 支持链式调用的接口

    支持链式调用的接口即返回QuerySet的接口,具体如下。

    • all接口。相当于 SELECT * FROM table_name语句,用于查询所有数据。
    • filter 接口。顾名思义,根据条件过滤数据,常用的条件基本上是字段等于、不等于、大于、小于。当然,还有其他的,比如能改成产生LIKE 查询的:Model.objects.filter(content_contains="条件")。
    • exclude 接口。同filter,只是相反的逻辑。
    • reverse 接口。把QuerySet 中的结果倒序排列。
    • distinct 接口。用来进行去重查询,产生SELECT DISTINCT 这样的SQL查询。
    • none 接口。返回空的QuerySet。
  2. 不支持链式调用的接口

    不支持链式调用的接口即返回值不是QuerySet的接口,具体如下。

    • get 接口。比如Post.objects.get(id=1)用于查询id为1的文章:如果存在,则直接返回对应的Post实例;如果不存在,则抛出DoesNotExist异常。所以一般情况下,我们会这么用:

      python 复制代码
      try:
      	post = Post.objects.get(id=1)
      except Post.DoesNotExist:
      #做异常情况处理
    • create 接口。用来直接创建一个 Model对象,比如post =Post.objects.create(title="一起学习Django实战吧")。

    • get_or_create 接口。根据条件查找,如果没查找到,就调用create 创建。

    • update_or_create 接口。同get_or_create,只是用来做更新操作。

    • count 接口。用于返回 Queryset 有多少条记录,相当于 SELECT COUNT(*)FROM table_name。

    • latest 接口。用于返回最新的一条记录,但是需要在Model 的 Meta中定义:get_latest_by =<用来排序的字段>。

    • earliest 接口。同上,返回最早的一条记录。

    • first 接口。从当前QuerySet记录中获取第一条。

    • last接口。同上,获取最后一条。

    • exists 接口。返回True 或者 False,在数据库层面执行 SELECT (1) AS "a" FROM table_name LIMIT 1的查询,如果只是需要判断QuerySet 是否有数据,用这个接口是最合适的方式。不要用count 或者 len(queryset)这样的操作来判断是否存在。相反,如果可以预期接下来会用到QuerySet 中的数据,可以考虑使用 len(queryset)的方式来做判断,这样可以减少一次DB查询请求。

    • bulk_create 接口。同create,用来批量创建记录。

    • in bulk 接口。批量查询,接收两个参数 id_list 和filed_name。可以通过Post.objects.in_bulk([1,2,3])查询出id为1、2、3的数据,返回结果是字典类型,字典类型的 key 为查询条件。返回结果示例:{1:<Post 实例 1>,2:<Post实例2>,3:<Post 实例3>}。

    • update 接口。用来根据条件批量更新记录,比如:Post.objects.filter(owner_name= '不染是非').update(title='测试更新')。

    • delete 接口 。同update,这个接口是用来根据条件批量删除记录。需要注意的是,update和delete 都会触发Django的signal。

    • values 接口。当我们明确知道只需要返回某个字段的值,不需要Model实例时,可以使用它,用法如下:

    python 复制代码
    title_list = Post.objects.filter(category_id=1).values('title')
     返回的结果包含dict的QuerySet,类似这样:<QuerySet [{'title':xxx},]>
    
    • values_list 接口 。同values,但是直接返回的是包含tuple的QuerySet:
    python 复制代码
    titles_list = Post.objects.filter(category=1).values_list('title')
    返回结果类似:<QuerySet[('标题',)]>
    

    如果只是一个字段的话,可以通过增加flat=True参数,便于我们后续处理:

    python 复制代码
    title_list = Post.objects.filter(category=1).values_list('title', flat=True)
    for title in title_list:
    	print(title)

2.4.3 进阶接口

除了上面介绍的常用接口外,还有其他用来提高性能的接口,下面一一介绍。在优化Django项目时,尤其要考虑这几种接口的用法。

  • defer 接口 。把不需要展示的字段做延迟加载。比如说,需要获取到文章中除正文外的其他字段,就可以通过posts=Post.objects.all().defer('content'),这样拿到的记录中就不会包含content部分。但是当我们需要用到这个字段时,在使用时会去加载。下面还是通过代码演示:
python 复制代码
posts = Post.objects.all().defer('content')
for post in posts;# 此时会执行数据库查询
	print(post.content)# 此时会执行数据查询,获取到content

当不想加载某个过大的字段时(如text 类型的字段),会使用defer,但是上面的演示代码会产生N+1的查询问题,在实际使用时千万要注意!

注意上面的代码是一个不太典型的N+1查询的问题,一般情况下由外键查询产生的N+1问题比较多,即一条查询请求返回N条数据,当我们操作数据时,又会产生额外的请求。这就是N+1问题,所有的ORM框架都存在这样的问题。

  • only 接口。同defer 接口刚好相反,如果只想获取到所有的title记录,就可以使用only,只获取title的内容,其他值在获取时会产生额外的查询。
  • select_related 接口 。这就是用来解决外键产生的N+1问题的方案。我们先来看看什么情况下会产生这个问题:
python 复制代码
posts = Post.objects.all()
for post in posts:  # 产生数据库查询
	print (post.owner)  # 产生额外的数据库查询

代码同上面类似,只是这里用的是owenr(关联表)。

它的解决方法就是用select_related 接口:

python 复制代码
post = Post.objects.all().select_related('category')
for post in posts:  # 产生数据库查询,category 数据也会一次性查询出来
	print (post.category)

当然,这个接口只能用来解决一对多的关联关系。对于多对多的关系,还得使用下面的接口。

  • prefetch_related 接口。针对多对多关系的数据,可以通过这个接口来避免N+1查询。比如,post 和tag的关系可以通过这种方式来避免:
python 复制代码
posts = Post.objects.all().prefetch_related('tag')
for post in posts:	# 产生两条查询语句,分别查询post和tag
	print(post.tag.all())

2.4.4 常用的字段查询

上面用到的Post.objects.filter(content_contains='查询条件')中的contains就属于字段查询。这里我们把常用的查询关键字列一下,更多的还需要去查看 Django文档。

  • **contains:**包含,用来进行相似查询。
  • **icontains:**同contains,只是忽略大小写。
  • exact: 精确匹配。
  • **iexact:**同exact,忽略大小写。
  • in: 指定某个集合,比如 Post.objects.filter(id_in=[1,2,3])相当于SELECT * FROM blog_post WHERE IN (1, 2, 3);。
  • **gt:**大于某个值。
  • **gte:**大于等于某个值。
  • **lt:**小于某个值。
  • **lte:**小于等于某个值。
  • **startswith:**以某个字符串开头,与contains类似,只是会产生LIKE<关键词>号·
    这样的SQL。
  • **istartswith:**同startswith,忽略大小写。
  • **endswith:**以某个字符串结尾。
  • **iendswith:**同endswith,忽略大小写。
  • **range:**范围查询,多用于时间范围,如Post.objects.filter(created_time_range=('2024-09-01','2024-10-01'))会产生这样的查询:SELECT... WHERE created_time BETWEEN'2024-09-01' AND '2024-10-01';。关于日期类的查询还有很多,比如date、year 和month等,具体等需要时查文档即可。

这里你需要理解的是,Django之所以提供这么多的字段查询,其原因是通过ORM来操作数据库无法做到像SOL的条件查询那么灵活。因此,这些查询条件都是用来匹配对应SQL语句的这意味着,如果你知道某个查询在SQL中如何实现,可以对应来看Django提供的接口。

2.4.5 进阶查询

除了上面基础的查询表达式外,Django还提供了其他封装,用来满足更复杂的查询,比如 SELECT ... WHERE id=1 OR id=2这样的查询,用上面的基础查询就无法满足。

  • **F:**F表达式常用来执行数据库层面的计算,从而避免出现竞争状态。比如需要处理每篇文章的访问量,假设存在post.pv这样的字段,当有用户访问时,我们对其加1。
python 复制代码
post = Post.objects.get(id=1)
post.pv = post.pv + 1
pos.save()

这在多线程的情况下会出现问题,其执行逻辑是先获取到当前的pv值,然后将其加1后赋值给post.pv,最后保存。如果多个线程同时执行了post =Post.objects.get(id=1),那么每个线程里的post.pv值都是一样的,执行完加工和保存之后,相当于只执行了一个加1,而不是多个。

其原因在于我们把数据拿到Python中转了一圈,然后再保存到数据库中。这时通过F表达式就可以方便地解决这个问题:

python 复制代码
from django.db.models import F 
post = Post.objects.get(id=1)post.pv = F('pv')+ 1
post.save()

这种方式最终会产生类似这样的SOL语句:UPDATE blog_post SET pv=pv+1 WHERE ID=1。它在数据库层面执行原子性操作。

  • **Q:**Q表达式就是用来解决前面提到的那个OR查询的,可以这么用:
python 复制代码
from django.db.models import Q
Post.objects.filter(Q(id=1) | Q(id=2))

或者进行AND查询:

python 复制代码
Post.objects.filter(Q(id=1) & Q(id=2))
  • **Count:**用来做聚合查询,比如想要得到某个分类下有多少篇文章,怎么做呢?简单的做法就是:
python 复制代码
category = Category.objects.get(id=1)
posts_count = category.post_set.count ()

但是如果想要把这个结果放到category上呢?通过category.post_count 可以访问到:

python 复制代码
from django.db.mdoels import Count
categories = Category.objects.annotate(posts_count=Count ('post'))
print(categories[0].posts_count)

这相当于给category 动态增加了属性posts_count,而这个属性的值来源于Count('post')。

  • **Sum:**同Count类似,只是它是用来做合计的。比如想要统计目前所有文章加起来的访问量有多少,可以这么做:
python 复制代码
from django.db.models import Sum
post.objects.aggregate (all_pv=Sum('pv'))
# 输出类似结果:('a11_pv':487)

上面演示了QuerySet的annotate和aggregate的用法,其中前者用来给QuerySet结果增加属性,后者只用来直接计算结果,这些聚合表达式都可以与它们结合使用。

除了Count和Sum外,还有Avg、Min和Max等表达式,均用来满足我们对SQL查询的需求。

2.4.6 总结

通过上面一系列的介绍,你应该对QuerySet 有了基本了解。其实简单来说,就是Django的ORM为了达到跟SQL语句同样的表达能力,给我们提供了各种各样的接口。

因此,我们也可以知道,QuerySet 的作用就是帮助我们更友好地同数据库打交道。

2.4.7 参考资料

QuerySet API:

https://docs.djangoproject.com/zh-hans/4.2/ref/models/querysets/。

Django数据库访问优化:

https://www.the5fire.com/django-database-access-optimization.html。

查询条件语句:

https://docs.djangoproject.com/zh-hans/4.2/ref/models/querysets/。

聚合查询:

https://docs.djangoproject.com/zh-hans/4.2/topics/db/aggregation/。

2.5 本章总结

这一章主要介绍了Model层的创建以及ORM的相关知识。对于任何业务来说,数据层都是非常重要的一层。现在Model 层所提供的接口完全是Django内置的,还没进行自定义开发,这些后续会根据需求补充。

这一章中尤其需要注意的是ORM优化的部分。在操作数据库的便利性上,ORM提供了非常便利的接口,让我们不需要编写复杂的SQL 语句就能够操作数据库,但同时需要意识到的是ORM的使用必定会产生损耗。因此,Django 还提供了原生 SQL的接口 Post.objects.raw('SELECT * FROM blog_post'),它除了可以解决QuerySet 无法满足查询的情况外,还可以提高执行效率。不过,我们需要严格把控使用场景,因为过多地使用原生SQL会提高维护成本。

本章中有很多陌生的知识点,这些知识点在Django的官方文档里都有说明,Django的官方文档解释的还是很详细和强大的,大家要是有不懂或者疑惑的知识点都可以去官方文档中查阅。

相关推荐
CSDN_PBB3 小时前
[STM32 - 野火] - - - 固件库学习笔记 - - - 十五.设置FLASH的读写保护及解除
笔记·stm32·学习
m0_748256144 小时前
SpringBoot
java·spring boot·后端
多想和从前一样5 小时前
Django 创建表时 “__str__ ”方法的使用
后端·python·django
涛粒子6 小时前
Spring Bean 生命周期的执行流程
java·后端·spring
赵琳琅7 小时前
Java语言的云计算
开发语言·后端·golang
鸡啄米的时光机7 小时前
vscode的一些实用操作
vscode·学习
赵琳琅7 小时前
MDX语言的安全开发
开发语言·后端·golang
Kai HVZ7 小时前
《深度学习》——调整学习率和保存使用最优模型
人工智能·深度学习·学习
夏梓蕙8 小时前
Elixir语言的软件开发工具
开发语言·后端·golang
夏梓蕙8 小时前
R语言的Web开发
开发语言·后端·golang