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的官方文档解释的还是很详细和强大的,大家要是有不懂或者疑惑的知识点都可以去官方文档中查阅。

相关推荐
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
Nu11PointerException1 小时前
JAVA笔记 | ResponseBodyEmitter等异步流式接口快速学习
笔记·学习
Chrikk2 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*2 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue2 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man2 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
Wx-bishekaifayuan4 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer084 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
@小博的博客4 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习