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

前言:

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

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

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

2.1 创建项目及配置

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

  1. 创建虚拟环境:
    打开PyCharm,切换到项目目录,新建项目,创建python虚拟环境,如下面两图所示
    在这里插入图片描述

    在这里插入图片描述

    创建好后页面如图所示:

    在这里插入图片描述

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

    在这里插入图片描述

  2. 安装Django 4.2版本:
       这里通过== 方式指定Django的特定版本,不加 ==则表示安装Django最新版本(一般不会使用最新版本,因为最新版本可能会跟其他库不兼容或者遇到bug没法及时解决,老版本遇到bug社区一般都能找到解决方案)
       在PyCharm终端中输入如下命令:

    pip install django==4.2
    
  3. 创建项目typeidea:
    在PyCharm终端中输入如下命令:

    django-admin startproject typeidea
    

    在这里插入图片描述

  4. 切换到typeidea:
    在这里插入图片描述

  5. 用PyCharm打开django:

    在这里插入图片描述
    在这里插入图片描述
    这样就可以在PyCharm中打开django了

    在这里插入图片描述
    如下图所示,django安装成功

    在这里插入图片描述

2.1.1 配置settings

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

# 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仓库中:

git add .
git commit -m '初始化提交'

之后需要配置远程仓库:

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和编写代码之前,你可以尝试创建一个新的分支:

    git checkout -b add-blog-app-model
    

    在这里插入图片描述

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

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

    在这里插入图片描述

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

    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节会详细介绍)。

  2. 编写configApp代码

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

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

在这里插入图片描述
接下来,编写conig/models.py中的代码:

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中的代码,代码如下:

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:

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,就是通过外键关联的表。

   举个例子来说:

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默认提供,可以被重写。它的完整定
      义是ia =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 中也要使用 Decima1 类型来转换数据(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 要支持链式操作。如果每次执行都要查询数据库的话,会存在性能问题,因为你可能用不到你执行的代码。举个例子,也顺便说下链式调用。

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

posts = Post.objects.all()
available_posts = posts.filter(status=1)

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

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

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

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特性。

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

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异常。所以一般情况下,我们会这么用:

      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 中的数据,可以考虑使用 1en(queryset)的方式来做判断,这样可以减少一次DB查询请求。

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

    • in bulk 接口。批量查询,接收两个参数 id_list 和filed_name。可以通过Post.objects.in_bulk([1,2,3])查询出ia为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实例时,可以使用它,用法如下:

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

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

    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部分。但是当我们需要用到这个字段时,在使用时会去加载。下面还是通过代码演示:
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_relatea 接口 。这就是用来解决外键产生的N+1问题的方案。我们先来看看什
    么情况下会产生这个问题:
posts = Post.objects.all()
for post in posts:  # 产生数据库查询
	print (post.owner)  # 产生额外的数据库查询

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

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

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

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

  • prefetch_relatea 接口。针对多对多关系的数据,可以通过这个接口来避免N+1查询。比如,post 和tag的关系可以通过这种方式来避免:
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
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表达式就可以方便地解决这个问题:

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查询的,可以这么用:
from django.db.models import Q
Post.objects.filter(Q(id=1) | Q(id=2))

   或者进行AND查询:

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

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

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类似,只是它是用来做合计的。比如想要统计目前所有文章加起来的访问量有多少,可以这么做:
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的官方文档解释的还是很详细和强大的,大家要是有不懂或者疑惑的知识点都可以去官方文档中查阅

  • 23
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不染_是非

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值