Django ORM 入门笔记

前置知识 后端入门 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 执行查询

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

发表评论

发表评论

*

沙发空缺中,还不快抢~