前言:
从这一章开始,我们来创建项目typeidea,我把它放到了GitHub上。强烈建议你也到GitHub上注册一个账号(如果没有的话),然后创建这样的项目。当然,你也可以起一个属于自己的名称。这个项目就是本系列博客要开发的多人博客系统。我建议你跟着本系列博客的节奏不断完善这个项目,同时不断把它们同步到GitHub上来记录自己的成长以及项目每一步的变更。
在这一章中,我们会根据需求完成整个Model 层的创建,理解Django中Model部分的知识点。我们始终保持学习的最佳实践:先实践,后总结。
第二章:奠定项目基石:Model
2.1 创建项目及配置
这里我用的是PyCharm来完成项目开发的,Django项目需要安装PyCharm专业版 ,还没有安装PyCharm专业版的可以看我这篇博客:pycharm安装与专业版永久使用
- 创建虚拟环境:
打开PyCharm,切换到项目目录,新建项目,创建python虚拟环境,如下面两图所示
创建好后页面如图所示:
打开终端页面检查是否安装成功且运行成功,如下图出现(.venv)则安装成功:
- 安装Django 4.2版本:
这里通过== 方式指定Django的特定版本,不加 ==则表示安装Django最新版本(一般不会使用最新版本,因为最新版本可能会跟其他库不兼容或者遇到bug没法及时解决,老版本遇到bug社区一般都能找到解决方案)
在PyCharm终端中输入如下命令:
bash
pip install django==4.2
- 创建项目typeidea:
在PyCharm终端中输入如下命令:
bash
django-admin startproject typeidea
- 切换到typeidea:
- 用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 相关、配置相关和评论相关。这么分的好处是便于我们独立维护各个模块,也便于在开发时分配任务。
- 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节会详细介绍)。
- 编写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 = '侧边栏'
配置部分的功能暂时如此,后面会在其中加入更多配置。这具体取决于后续的需求。任何一款产品都不是一蹴而就的,都是经过不断迭代、不断调整才形成最终或者说最合适的形态。而对于技术人员来说,需要做的就是理解产品是不断变化的,然后提供相应的技术支持。
- 创建评论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。
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中常用的字段类型以及参数配置进行说明。这里可以根据类型来划分,这其实就是数据库中字段类型的划分。
-
数值型
这些类型都是数值相关的,比如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。小整数时一般会用到。
-
字符型
下面这些字段都是用来存储字符数据的,对应到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个都是日期类型,分别对应MySQL的date、datetime和time,似乎也不需要过多解释:
- DateFiela
- DateTimeField
- TimeField
-
关系类型
这是关系型数据库中比较重要的字段类型,用来关联两个表,具体如下:
- 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,除了这个外,还有哪些呢?
在这一节中,我们来看看常用的接口。这里我根据是否支持链式调用分类进行介绍。
-
支持链式调用的接口
支持链式调用的接口即返回QuerySet的接口,具体如下。
- all接口。相当于 SELECT * FROM table_name语句,用于查询所有数据。
- filter 接口。顾名思义,根据条件过滤数据,常用的条件基本上是字段等于、不等于、大于、小于。当然,还有其他的,比如能改成产生LIKE 查询的:Model.objects.filter(content_contains="条件")。
- exclude 接口。同filter,只是相反的逻辑。
- reverse 接口。把QuerySet 中的结果倒序排列。
- distinct 接口。用来进行去重查询,产生SELECT DISTINCT 这样的SQL查询。
- none 接口。返回空的QuerySet。
-
不支持链式调用的接口
不支持链式调用的接口即返回值不是QuerySet的接口,具体如下。
-
get 接口。比如Post.objects.get(id=1)用于查询id为1的文章:如果存在,则直接返回对应的Post实例;如果不存在,则抛出DoesNotExist异常。所以一般情况下,我们会这么用:
pythontry: 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实例时,可以使用它,用法如下:
pythontitle_list = Post.objects.filter(category_id=1).values('title')
返回的结果包含dict的QuerySet,类似这样:<QuerySet [{'title':xxx},]>
- values_list 接口 。同values,但是直接返回的是包含tuple的QuerySet:
pythontitles_list = Post.objects.filter(category=1).values_list('title')
返回结果类似:<QuerySet[('标题',)]>
如果只是一个字段的话,可以通过增加flat=True参数,便于我们后续处理:
pythontitle_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的官方文档解释的还是很详细和强大的,大家要是有不懂或者疑惑的知识点都可以去官方文档中查阅。