0%

Django-ORM

概述

对象关系映射(Object Relational Mapping,简称 ORM )用于实现面向对象编程语言里不同类型系统的数据之间的转换。

ORM 在业务逻辑层和数据库层之间充当了桥梁的作用。

ORM 是通过使用描述对象和数据库之间的映射的元数据,将程序中的对象自动持久化到数据库中。

使用ORM的优点

  • 提高开发效率。
  • 不同数据库可以平滑切换。

使用ORM的缺点

  • ORM 代码转换为 SQL 语句时,需要花费一定的时间,执行效率会有所降低。
  • 长期写 ORM 代码,会降低编写 SQL 语句的能力。

ORM解析过程

  1. ORM 会将 Python 代码转成为 SQL 语句。
  2. SQL 语句通过 pymysql 传送到数据库服务端。
  3. 在数据库中执行 SQL 语句并将结果返回。

ORM开发步骤

  1. 在models.py定义模型类
  2. 生成迁移文件: python manage.py makemigrations –> 在migrations文件夹生成0001_intial.py
  3. 执行迁移生成数据库(默认会用sqite3数据库,生成数据库名为:db.sqlite3)
  4. python mange.py migrate -> 在数据库中生成对应的数据 test01_department 等
  5. 通过模型类和对象,对数据进行增删改查

ORM对应关系表

Django配置

连接MySQL

1.创建数据库

1
create database db1 default character set utf8 collate utf8_general_ci;

2.settings.py

数据库配置:

1
2
3
4
5
6
7
8
9
10
11
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': 'rm-2zebj2u1pd5c2y29jto.mysql.rds.aliyuncs.com',
'NAME': 'polaris-dev',
'PORT': 3306,
'USER': 'vcgapp',
'PASSWORD': '******',
"OPTIONS": {"init_command": "SET default_storage_engine=INNODB;"}
}
}

DEBUG日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler',
},
},
'loggers': {
'django.db.backends': {
'handlers': ['console'],
'propagate': True,
'level':'DEBUG',
},
}
}

3.安装pymysql

1
pip install pymysql
1
2
3
4
vim __init__.py  #项目根目录,和settings.py同级目录

import pymysql
pymysql.install_as_MySQLdb()

4.使用model

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

1
2
3
4
5
INSTALLED_APPS = [
#...
'myapp',
#...
]

模型Model

参考官方文档:https://docs.djangoproject.com/zh-hans/3.1/topics/db/models/

样例

1
2
3
4
5
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 模型会创建一个如下的数据库表:

1
2
3
4
5
CREATE TABLE myapp_person (
"id" serial NOT NULL PRIMARY KEY,
"first_name" varchar(30) NOT NULL,
"last_name" varchar(30) NOT NULL
);

一些技术上的说明:

  • 该表的名称 myapp_person 是自动从某些模型元数据中派生出来,但可以被改写。参阅 表名称 获取更多信息。
  • 一个 id 字段会被自动添加,但是这种行为可以被改写。请参阅 自动设置主键
  • 本例子中 创建数据表 的语法是 PostgreSQL 格式的。值得注意的是,Django 依据你在 配置文件 中指定的数据库后端生成对应的 SQL 语句。

字段

模型中最重要且唯一必要的是数据库的字段定义。字段在类属性中定义。定义字段名时应小心避免使用与 模型 API 冲突的名称, 如 clean, save, or delete 等.

举例:

1
2
3
4
5
6
7
8
9
10
11
12
from django.db import models

class Musician(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
instrument = models.CharField(max_length=100)

class Album(models.Model):
artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
release_date = models.DateField()
num_stars = models.IntegerField()

字段类型

模型中每一个字段都应该是某个 Field 类的实例, Django 利用这些字段类来实现以下功能:

  • 字段类型用以指定数据库数据类型(如:INTEGER, VARCHAR, TEXT)。
  • 在渲染表单字段时默认使用的 HTML 视图 (如: <input type="text">, <select>)。
  • 基本的有效性验证功能,用于 Django 后台和自动生成的表单。

Django 内置了数十种字段类型;你可以在 模型字段参考 中看到完整列表。如果 Django 内置类型不能满足你的需求,你可以很轻松地编写自定义的字段类型;参见 编写自定义模型字段(model fields)

Django模型属性和MySQL数据库数据类型对应关系

分类 模型属性类型 sql数据类型
自增 AutoField int
布尔 BooleanField tinyint
NullBooleanField tinyint
字符 CharField varchar
EmailField varchar
TextField longtext
数字 IntegerField int
DecimalField decimal
FloatField double
日期和时间 DateField date
TimeField time
DateTimeField datetime
文件 FileField varchar
ImageField varchar
外键 ForeignKey alter table B add constraint A_B_Ids foreign key(Aid) references A(Ids)
ManytoMany 建中间表再关联外键

字段设计

  • 避免允许 null 值的字段,null 值难以查询优化且占用额外的索引空间
  • 充分考虑每张表的数据规模,选取合适主键字段类型。比如数据创建比较频繁的表,主键建议使用 BigIntegerField
  • 使用正确的字段类型,避免TextField代替CharFieldIntegerField代替BooleanField
  • 如果字段的取值是一个有限集合,应使用 choices 选项声明枚举值
1
2
3
4
5
6
7
8
9
10
11
class Students(models.Model):
class Gender(object):
MALE = 'MALE'
FEMALE = 'FEMALE'

GENDER_CHOICES = (
(Gender.MALE, "男"),
(Gender.FEMALE, "女"),
)

gender = models.IntegerField("性别", choices=GENDER_CHOICES)
  • 如果某个字段或某组字段被频繁用于过滤或排序查询,建议建立单字段索引或联合索引
1
2
3
4
5
6
7
8
9
10
# 字段索引:使用 db_index=True 添加索引
title = models.CharField(max_length=255, db_index=True)

# 联合索引:将多个字段组合在一起建立索引
class Meta:
index_together = ['field_name_1', 'field_name_2']

# 联合唯一索引:将多个组合在一起的索引,并且字段的组合值唯一
class Meta:
unique_together = ('field_name_1', 'field_name_2')

数据的增删查改

查询

基本要求

  • 了解 Django ORM 是如何缓存数据的
  • 了解 Django ORM 何时会做查询
  • 不要以牺牲代码可读度为代价做过度优化

实际应用

  • 避免全表扫描。优先使用exists, count等方法
1
2
3
4
5
6
7
8
# 获取 project 的数量
projects = Project.objects.filter(enable=True)

# Bad: 会强制将 projects 实例化,导致全表扫描
project_count = len(projects)

# Good: 直接从数据库层面统计,避免全表扫描
project_count = projects.count()
  • 避免 N + 1 查询。可使用select_related提前将关联表进行 join,一次性获取相关数据,many-to-many 的外键则使用prefetch_related
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# select_related

# Bad
# 由于 ORM 的懒加载特性,在执行 filter 操作时,并不会将外键关联表的字段取出,而是在使用时,实时查询。这样会产生大量的数据库查询操作
students = Student.objects.all()
student_in_class = {student.name: student.cls.name for student in students}

# Good
# 使用 select_related 可以避免 N + 1 查询,一次性将外键字段取出
students = Student.objects.select_related('cls').all()
student_in_class = {student.name: student.cls.name for student in students}
# prefetch_related

# Bad
articles = Article.objects.filter(id__in=(1,2))
for item in articles:
# 会产生新的数据库查询操作
item.tags.all()

# Good
articles = Article.objects.prefetch_related("tags").filter(id__in=(1,2))
for item in articles:
# 不会产生新的数据库查询操作
item.tags.all()
  • 如果仅查询外键 ID,则无需进行连表操作。使用 外键名_id 可直接获取
1
2
3
4
5
6
7
8
# 获取学生的班级ID
student = Student.objects.first()

# Bad: 会产生一次关联查询
cls_id = student.cls.id

# Good: 不产生新的查询
cls_id = student.cls_id
  • 避免查询全部字段。可使用values, values_list, only, defer等方法进行过滤出需要使用的字段。
1
2
3
4
5
6
7
8
# 仅获取学生姓名的列表

# Bad
students = Student.objects.all()
student_names = [student.name for student in students]

# Good
students = Student.objects.all().values_list('name', flat=True)
  • 避免在循环中进行数据库操作。尽量使用 ORM 提供的批量方法,防止在数据量变大的时候产生大量数据库连接导致请求变慢
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 批量创建项目
project_names = ['ProjectA', 'ProjectB', 'ProjectC']

# Bad
for project_name in project_names:
Project.objects.create(name=project_name)

# Good
projects = []
for project_name in project_names:
project = Project(name=project_name)
projects.append(project)
Project.objects.bulk_create(projects)
# 批量查询项目
project_names = ['ProjectA', 'ProjectB', 'ProjectC']

# Bad: 每次循环都产生一次新的查询
projects = []
for project_name in project_names:
project = Project.objects.get(name=project_name)
projects.append(project)

# Good:使用 in,只需一次数据库查询
projects = Project.objects.filter(name__in=project_names)
# 批量更新项目
project_names = ['ProjectA', 'ProjectB', 'ProjectC']
projects = Project.objects.filter(name__in=project_names)

# Bad: 每次循环都产生一次新的查询
for project in projects:
project.enable = True
project.save()

# Good:批量更新,只需一次数据库查询
projects.update(enable=True)
  • 避免隐式的子查询
1
2
3
4
5
6
7
8
9
# 查询符合条件的组别中的人员

# Bad: 将查询集作为下一个查询的过滤条件,因此产生了子查询。IN 语句中的子查询在外层查询的每一行中都会被执行一次,复杂度为 O(n^2)
groups = Group.objects.filter(type="typeA")
members = Member.objects.filter(group__in=groups)

# Good: 以确定的数据作为过滤条件,避免子查询
group_ids = Group.objects.filter(type="typeA").values_list('id', flat=True)
members = Member.objects.filter(group__id__in=list(group_ids))
  • update_or_createget_or_create 不是线程安全的。因此查询条件的字段必须要有唯一性约束
1
2
3
4
5
# 为了保证逻辑的正确性,Host 表中的 ip 和 bk_cloud_id 字段必须设置为 unique_together。否则在高并发情况下,可能会创建出多条相同的记录,最终导致逻辑异常
host, is_created = Host.objects.get_or_create(
ip="127.0.0.1",
bk_cloud_id="0"
)
  • 如果查询集只用于单次循环,建议使用 iterator() 保持连接查询。当查询结果有很多对象时,QuerySet 的缓存行为会导致使用大量内存。如果你需要对查询结果进行好几次循环,这种缓存是有意义的,但是对于 QuerySet 只循环一次的情况,缓存就没什么意义了。在这种情况下,iterator()可能是更好的选择
1
2
3
4
5
6
7
# Bad
for task in Task.objects.all():
# do something

# Good
for task in Task.objects.all().iterator():
# do something