Django ORM 入门笔记

Django 发布于 Mar 9, 2021 更新于 Jul 19, 2022

前置知识 后端入门 Django 项目基础配置

本笔记默认读者已经对数据库有一定的认识

最少需要理解如何在数据库中建立一对一、一对多、多对多关系

本笔记建立在已经知晓并配置完数据库连接的前提下

本笔记中使用的数据库是 MySQL 8.0

使用pymysql模块连接数据库时,manage.py 在执行数据库相关指令时要求计算机shell中可以运行mysql指令

ORM 基础概念

简单了解 ORM

Object Relational Mapping 对象关系映射,简称ORM。

ORM通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系数据库中。ORM在业务逻辑层和数据库层之间充当了桥梁的作用。

简单来说,以往我们操作数据库时使用的是 SQL 语句,但有了 ORM 后,我们操作数据库只需要操作在 ORM 框架下建立的数据模型对象,然后 ORM 替我们执行背后的 SQL 语句。符合我们面向对象编程的习惯,也有效防止了 SQL 注入攻击的发生。

ORM 对象和数据库结构的对应关系

这里以 Django 为例。

以下是我们大创数据库中的一个表的模型与设计。

class SiteAllPeriod(models.Model):
    periodid = models.AutoField(db_column='periodID', primary_key=True)
    week = models.IntegerField()
    siteperiodid = models.IntegerField(db_column='sitePeriodID')
    siteid = models.ForeignKey(Site, models.DO_NOTHING, db_column='siteID', related_name='%(class)s_siteid')
    sitename = models.ForeignKey(Site, models.DO_NOTHING, db_column='siteName', related_name='%(class)s_sitename')
    date = models.DateField()
    childsiteid = models.ForeignKey('SiteChild', models.DO_NOTHING, db_column='childSiteID', blank=True, null=True, related_name='%(class)s_childsiteid')
    childsitename = models.ForeignKey('SiteChild', models.DO_NOTHING, db_column='childSiteName', blank=True, null=True, related_name='%(class)s_childsitename')
    starttime = models.TimeField(db_column='startTime', blank=True, null=True)
    endtime = models.TimeField(db_column='endTime', blank=True, null=True)
    stateid = models.ForeignKey('SitePeriodState', models.DO_NOTHING, db_column='stateID', blank=True, null=True, related_name='%(class)s_stateid')
    prompt = models.CharField(max_length=50, blank=True, null=True)
    price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
    studentprice = models.DecimalField(db_column='studentPrice', max_digits=10, decimal_places=2, blank=True, null=True)
    staffprice = models.DecimalField(db_column='staffPrice', max_digits=10, decimal_places=2, blank=True, null=True)lowercase.
    orderusername = models.CharField(db_column='orderUserName', max_length=30, blank=True, null=True)
    orderuserpaymethod = models.CharField(db_column='orderUserPayMethod', max_length=30, blank=True, null=True)
    needsubcard = models.IntegerField(db_column='needSubcard', blank=True, null=True)
    teachingperiodname = models.CharField(db_column='teachingPeriodName', max_length=30, blank=True, null=True) 
    teacher = models.CharField(max_length=30, blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'site_all_period'

image-20210308202542918image-20210308202619123

可以看出来,Django 的 ORM 的模型中:

  • 类对应某个特定的表
  • 类的实例对应着一条记录
  • 类的属性对应着表内的字段
  • 类属性的类型对应着表内字段的类型
  • 类属性类型构造时的参数与字段的参数一一对应
  • 外键拥有特殊的类型
  • 外键类型直接指向另一个 ORM 模型(如果另一个模型在本模型下方,需要用单引号包裹)

可见 ORM 的模型既能和数据库形成良好的对应关系、能体现出数据库不同表之间的对应关系。

【知识链接】👇

Django 官方文档 – ORM 中的模型

Django 官方文档 – ORM 中的字段

Django ORM 特性 — 反向查询

在上面的代码中可以发现,字段类型构造的参数中,有一个名为related_name的参数,它的作用是为 ORM 模型提供反向查询支持。

什么是反向查询

在关系型数据库中,我们使用常外键(Foreign Key)来实现关系,反向查询是使用外键寻找另一表中记录的反过程。

拿到一个记录后,反向查询可以获得使用外键指向该记录的其他表中的记录。

related_name 需要独一无二?

如果你将两个related_name设为相同值,你会发现框架无法正常启动。

这是因为,知道外键,你可以知道唯一的记录,但是可能有不同表的多个外键指向同一记录。如果存在相同的related_name,程序就无法知晓应该去哪一个表寻找外键了,就无法反向获得那些特定的记录。


在 Django 中配置 ORM 模型

创建一个 ORM 模型

为了偷懒容易理解,这里引入一个来自 Django 官方文档的样例

这个样例定义了一个 Person 模型,拥有 first_namelast_name:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

first_namelast_name 是模型的 字段。每个字段都被指定为一个类属性,并且每个属性映射为一个数据库列。

上面的 Person 模型会创建一个如下的数据库表:

CREATE TABLE myapp_person (
    "id" serial NOT NULL PRIMARY KEY,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(30) NOT NULL
);

若想详细了解各种字段,请移步Django 官方文档 – ORM 字段选项

使用新创建的模型

假设上面的模型位于myapp项目的models.py模块内

一旦你定义了你的模型,你需要告诉 Django 你准备 使用 这些模型。你需要修改设置文件中的 INSTALLED_APPS ,在这个设置中添加包含 models.py 文件的模块名称。

例如,若模型位于项目中的 myapp.models 模块( 此包结构由 manage.py startapp 命令创建), INSTALLED_APPS 应设置如下:

INSTALLED_APPS = [
    #...
    'myapp',
    #...
]

当你向 INSTALLED_APPS 添加新的应用后。

运行 manage.py migrate,你的模型会被自动映射到数据库中(新创建一个符合模型的表)

这时,数据库中就会出现一个名为 myapp_person 的表

使用 Meta 信息指定命名表

如果我想让上面的模型对应的表拥有自定义的表名呢?

答案是:定义 Meta 子类

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

    class Meta:
        db_table = 'person'

添加了该 Meta 子类后,使用manage.py migrate迁移完成,数据库中出现的表就名为 person 了。

元数据还有其他很多参数可选,具体请移步 模型可选参数参考

使用已有数据库自动建立 ORM 模型

  • Django ORM 的另一强大之处在于它可以双向构造映射
  • 创建了模型,可以生成表;同样的,设计了表,可以生成模型

首先,在settings.py配置数据库

完成后,运行指令

python manage.py dbshell

如果执行后,你进入了数据库的命令行,就说明配置成功了

python manage.py inspectdb > models.py

静待两分钟,没有任何输出,运行结束,则说明models.py生成成功

去根目录寻找models.py文件,放到喜欢的地方(?),就可以开始使用了。


在 Django 中使用 ORM 完成数据库操作

如果时间充裕,建议先去温习一下SQL,Django 的 ORM 使用了很多高级的 SQL 语句,拥有相关知识有助于理解 ORM 的运行否则就是玄学

下面会展示一些常用 SQL 语句的 ORM 对应

继续以笔记开头的第一个模型为例,为了方便观察,这里再贴一个拷贝

class SiteAllPeriod(models.Model):
    periodid = models.AutoField(db_column='periodID', primary_key=True)
    week = models.IntegerField()
    siteperiodid = models.IntegerField(db_column='sitePeriodID')
    siteid = models.ForeignKey(Site, models.DO_NOTHING, db_column='siteID', related_name='%(class)s_siteid')
    sitename = models.ForeignKey(Site, models.DO_NOTHING, db_column='siteName', related_name='%(class)s_sitename')
    date = models.DateField()
    childsiteid = models.ForeignKey('SiteChild', models.DO_NOTHING, db_column='childSiteID', blank=True, null=True, related_name='%(class)s_childsiteid')
    childsitename = models.ForeignKey('SiteChild', models.DO_NOTHING, db_column='childSiteName', blank=True, null=True, related_name='%(class)s_childsitename')
    starttime = models.TimeField(db_column='startTime', blank=True, null=True)
    endtime = models.TimeField(db_column='endTime', blank=True, null=True)
    stateid = models.ForeignKey('SitePeriodState', models.DO_NOTHING, db_column='stateID', blank=True, null=True, related_name='%(class)s_stateid')
    prompt = models.CharField(max_length=50, blank=True, null=True)
    price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
    studentprice = models.DecimalField(db_column='studentPrice', max_digits=10, decimal_places=2, blank=True, null=True)
    staffprice = models.DecimalField(db_column='staffPrice', max_digits=10, decimal_places=2, blank=True, null=True)lowercase.
    orderusername = models.CharField(db_column='orderUserName', max_length=30, blank=True, null=True)
    orderuserpaymethod = models.CharField(db_column='orderUserPayMethod', max_length=30, blank=True, null=True)
    needsubcard = models.IntegerField(db_column='needSubcard', blank=True, null=True)
    teachingperiodname = models.CharField(db_column='teachingPeriodName', max_length=30, blank=True, null=True) 
    teacher = models.CharField(max_length=30, blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'site_all_period'

1. SELECT * FROM site_all_period

认真看本条!包含了一些讲解!

选择 site_all_period 中全部的记录

ORM中,使用以下语句获取记录

querySet = SiteAllPeriod.objects.all()

SQL 语句查询返回的是一个记录列表,而 ORM 查询的返回值是QuerySet,顾名思义,这是一个储存查询结果的集合。

了解 QuerySet

QuerySet和 Python 标准的 list 类型有些相似,使用时很多时候可以把它当作一个 list 来处理,比如可以使用for ... in ...遍历选择到的记录,或者使用下标访问index位置的记录(如果记录有序,这个的用处就比较大了)。

  • 仅仅创建QuerySet不会触发数据库操作,只有在QuerySet被使用(读取具体内容)时,才会执行对应的 SQL 语句。

  • QuerySet是可以迭代的,比如你可以使用records = SiteAllPeriod.objects.all().all().all().all().all()(逃)

    请牢记它可以被迭代,它的迭代特性可以用来组装一个复杂的 SQL 语句。看文档应该是会合并成单 SQL 语句,有待验证,好奇可以QQ联络我让我验证。

  • QuerySet具有缓存功能,如果事先创建了QuerySet对象,那么对它进行遍历不会反复进行数据库操作,而是会对已有的QuerySet对象进行遍历。

    具体缓存机制请参阅 Django 官方文档 – ORM QuerySet 缓存机制

若想更全面的了解QuerySet,请阅读 Django 官方文档 – ORM 中的 QuerySet

2. SELECT * FROM site_all_period WHERE week=1

选择 site_all_period 中week=1的全部记录

ORM中,使用以下语句获取记录

querySet = SiteAllPeriod.objects.filter(week=1) # 使用参数名确定对象属性(字段)

TIP

假设你想要像2中选择week=1的记录那样,选择siteid=1的记录。为此你使用了如下语句,会怎么样?

querySet = SiteAllPeriod.objects.filter(siteid=1)

答案是无法正常运行,因为siteid是一个外键,你将它作为参数名称,ORM 会尝试去寻找外键对应的对象。

那如果确实想要获得siteid=1的记录呢?

请在该属性名称后方加上_id,使用如下语句:

querySet = SiteAllPeriod.objects.filter(siteid_id=1) # 外键属性+_id才能获取到字段的值

3. SELECT JOIN 连接查询/跨表查询

如果要实现对关系的处理,以往我们需要使用 JOIN SQL 语句来进行连接查询,Django 的 ORM 同样提供了极其简便的方式,即“双下划线”

题外话:这个笔记只能帮助读者对 ORM 建立一定的理解,鉴于这东西的复杂性,只能举一些例子,不可能完整呈现 ORM 的神奇功能,还是需要读者完整阅读文档。

比如上面的SiteAllPeriod,里面有一些外键,我们可以利用这些外键实现跨表查询

比如外键siteid,Site类型里面有个courtid字段,如果我们想选择SiteAllPeriod中(此处断句)所有外键siteid指向的Site对象的courtid字段为1的记录

这时需要使用语句

querySet = SiteAllPeriod.objects.filter(siteid__courtid_id=1)

这行语句的背后就是 SQL 的 JOIN 连接查询,Django 会帮你完成该语句的构造和执行。

4. 模型对象之间的访问

Django 的 ORM 模型还有个不错的地方就是:

siteAllPeriod_record = SiteAllPeriod.objects.filter(siteid_id=1).first() # 选择 siteid = 1 的记录,并拿取第一个(当然这种情况也只有一个)
courtid_of_this_period = siteAllPeriod_record.siteid.courtid_id
"""
siteid 是外键属性,访问该属性会直接返回对应的对象
这里可以直接获得 siteAllPeriod 归属的 site 对象的 courtid (site-siteAllPeriod 是个一对多关系)
注意,这样写看起来很美好,但是访问另一个表的记录对象,是会执行一次 SQL 语句的,性能损耗很大!
"""

5. 记录的保存

这个很容易,拿到对象,直接改属性,改完调用这个对象的save()方法就保存了

当然,对QuerySet也可以像上面那样干(python的对象默认都是引用,所以懂的都懂)

6. 记录的删除

5一模一样,就是把save()换成delete()


结语

上面只提供了部分例子,也是我认为的入门最需要理解的例子。

如果想查看详细的样例与解释,请参阅 Django 官方文档 – ORM 执行查询

后端道阻且长,希望大家一起加油呀!

标签

Noam Chi

An Innovative Quant Developer. 2018 VEX World Final THINK Award🏆