2.ORM高级操作
一、ORM 多表操作
假设场景:
我们现在要对机房里的服务器进行管理:
-
一个机房可以 存放多个机柜。
-
一个机柜可以放置多台服务器。
-
机房有:城市、地址、楼层。
-
机柜有:品牌、机柜的型号、几 U、机柜编号、所属机房。
-
服务器有:主机名、管理 IP、所属机柜等。
以上的内容需要以下表来存储管理这些信息:
机房表
id | 名称 | 城市 | 地址 |
---|---|---|---|
1 | 亦庄机房 | 北京 | 亦庄 |
机柜表
id | 品牌 | 型号 | 自编号 | U | 机房 id |
---|---|---|---|---|---|
1 | 国普达 | GP-ZN61042 | Y-S-001 | 42 | 1 |
2 | 国普达 | GP-36A22 | Y-S-002 | 22 | 1 |
服务器表
Id | 主机名 | 管理 IP | 所属机柜 |
---|---|---|---|
1 | appserver1 | 192.168.1.10 | 1 |
2 | appserver2 | 192.168.1.11 | 1 |
3 | Dbserver1 | 192.168.1.12 | 2 |
二、Django Model 如何表示表之间的关系
-
模型之间的三种关系:一对一,多对一,多对多。
一对一:实质就是在主外键(author_id就是foreign key)的关系基础上,给外键加了一个UNIQUE=True的属性; 多对一:就是主外键关系;(foreign key) 多对多:(ManyToManyField) 自动创建第三张表(当然我们也可以自己创建第三张表:两个foreign key)
# 一对一:
models.OneToOneField(OtherModel)
# 多对一:
models.ForeignKey(OtherModel, on_delete=models.CASCADE)
# 多对多:
models.ManyToManyField(OtherModel)
Django2.x 的 多对一表关系设置时,外键需要添加
on_delete=models.CASCADE
。
表示当删除表中的数据的时候,执行级联删除动作。
三 编写 Model (模型)
class Asset(models.Model):
"""
资产信息表,所有资产公共信息(交换机,服务器,防火墙等)
"""
device_type_choices = (
(1, '服务器'),
(2, '路由器'),
(3, '交换机'),
(4, '防火墙'),
)
device_status_choices = (
(1, '上架'),
(2, '在线'),
(3, '离线'),
(4, '下架'),
)
device_type_id = models.IntegerField(choices=device_type_choices, default=1)
device_status_id = models.IntegerField(choices=device_status_choices, default=1)
# 多个资产可以放在一个机柜中,也就是多对一,
# 即:此字段会有相同的值
cabinet_id = models.ForeignKey('Cabinet', verbose_name='所属机柜', max_length=30, null=True, blank=True, on_delete=models.CASCADE)
latest_date = models.DateField(verbose_name='更新时间', null=True, blank=True)
create_at = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
"""
auto_now=True 每次更新,日期自动存为当前时间,并且在 Admin 管理中,此字段将不支持修改
default
"""
class Meta:
verbose_name = "资产表"
verbose_name_plural = verbose_name
db_table = 'asset'
def __str__(self):
return "{}-{}-{}".format(self.idc.name, self.cabinet_num, self.cabinet_order)
class IDC(models.Model):
name = models.CharField(verbose_name='机房', max_length=128)
city = models.CharField(verbose_name='城市', max_length=32)
address = models.CharField(verbose_name='地址', max_length=256)
class Meta:
verbose_name_plural = '机房表'
db_table = "idc"
def __str__(self):
return self.name
class Cabinet(models.Model):
name = models.CharField(verbose_name='机柜编号', max_length=128)
cab_lever = models.CharField(verbose_name='U 数', max_length=2) # 机柜总共几层
idc = models.ForeignKey('IDC', verbose_name='所属机房', null=True, blank=True, on_delete=models.CASCADE)
class Meta:
verbose_name_plural = '机柜表'
db_table = "cabinet"
def __str__(self):
return self.name
class Server(models.Model):
# 每个服务器都和资产表一一对应
asset = models.OneToOneField('Asset', verbose_name='对应资产', null=True, blank=True,on_delete=models.CASCADE)
hostname = models.CharField(verbose_name='主机名', max_length=128, unique=True)
sn = models.CharField(verbose_name='SN号', max_length=64, db_index=True) # 为此字段创建索引
manage_ip = models.GenericIPAddressField(verbose_name='管理IP', null=True, blank=True)
latest_date = models.DateTimeField(verbose_name='更新时间', default=timezone.now, null=True)
create_at = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
class Meta:
verbose_name = "服务器表"
verbose_name_plural = verbose_name
db_table = 'server'
def __str__(self):
return self.hostname
同步数据库
python3 manage.py makemigrations
python3 manage.py migrate
__str__()
Model.__str__()
__str__()
方法在每当你对一个对象调用str()
时候。 Django在许多地方使用str(obj)
。 最明显的是在Django 的Admin 站点显示一个对象和在模板中插入对象的值的时候。 所以,你应该始终让__str__()
方法返回模型的一个友好的、人类可读的形式。
像这样:
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible # only if you need to support Python 2
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
def __str__(self):
return '%s %s' % (self.first_name, self.last_name)
如果你希望与Python 2兼容,则可以使用python_2_unicode_compatible()
来装饰模型类,如上所示。
ORM 进阶操作
OneToOneFiel
# 一对一
# 添加数据
from users import models
obj = models.IDC.objects.create(name="兆维机房",
city="北京",
address="北京路")
models.Cabinet.objects.create(name="BJ-S-003",
cab_lever="42",
idc=obj)
zw_dic = models.IDC.objects.filter(name="兆维机房").values("id")[0]
f_id = zw_dic['id']
models.Cabinet.objects.create(name="BJ-S-003",
cab_lever="42",
idc_id=f_id)
models.Cabinet.objects.all().values()
models.Cabinet.objects.all().values("name")
models.Cabinet.objects.filter(name='BJ-S-003')
obj = models.Cabinet.objects.filter(name='BJ-S-003').last()
obj.name
obj.name="BJ-S-004"
obj.name
obj.save()
models.Cabinet.objects.all().values("name")
# 表名的小写加双下划线和要查询的字段名
asset_obj = models.Asset.objects.filter(id=1).first()
asset_obj.get_device_type_choices_display()
asset_obj.latest_date
asset_obj = models.Asset.objects.filter(id=1)
asset_val_obj = asset_obj.values('id', 'server__hostname').first()
print(asset_val_obj.keys())
print(asset_val_obj.values())
ForeignKey
添加数据
# 方式1:
# 由于绑定多对一的字段,比如 Cabinet 表的 idc 字段, 存到数据库中的字段名会是 idc_id 所以可以直接给这个字段设定对应值:
models.Cabinet.objects.create(name="Y-S-01",
cab_lever="42",
idc_id=1)
# 方式2:
# 选获取到需要关联的对象,就是多对一中那个 一
idc_obj = models.IDC.objects.filter(id=1)[0]
cab_dic = dict(name="Y-S-02",cab_lever="22",idc=idc_obj)
models.Cabinet.objects.create(**cab_dic)
更新数据
models.Cabinet.objects.filter(name="Y-S-02"
).update(name="Y-S-002")
查询数据
- 获取
QuerySet
对象时,使用.
连接 - 获取字段值时,使用
__
连接
使用 .
查对象
# 正向查,就是查我属于谁
# 使用 Frenkey 的字段名,进行连表查询
qst = models.Cabinet.objects.first()
qst.name
Out[43]: 'Y-S-01'
qst.idc.name
Out[44]: '亦庄'
# 反向查,就是查谁属于我
# 使用小写的表名加 _set,进行连表查询
qst = models.IDC.objects.last()
qst.cabinet_set.all()
基于双下划线,直接查询字段的值
# 正向,就是从含有 ForeignKey 字段的表开始查
# 先获取到一个 QueySet 对象
qst = models.Cabinet.objects.filter(id=1)
# 再用 QuerySet 对象查询字段的值
# 结果是字典
qst.values("name","idc__name")
Out[53]: <QuerySet [{'name': 'Y-S-01', 'idc__name': '亦庄'}]>
# 结果是元组
qst.values_list("name","idc__name")
Out[54]: <QuerySet [('Y-S-01', '亦庄')]>
###############################################
# 反向查,表的映射类名称的小写
qst = models.IDC.objects.filter(id=1)
# 结果是字典
qst.values("name", "cabinet__name")
Out[58]: <QuerySet [{'name': '亦庄', 'cabinet__name': 'Y-S-01'}, {'name': '亦庄', 'cabinet__name': 'Y-S-002'}]>
# 结果是元组
qst.values_list("name", "cabinet__name")
Out[60]: <QuerySet [('亦庄', 'Y-S-01'), ('亦庄', 'Y-S-002')]>
优化
假如你想查询到机柜表里的的所有数据,并且得到每条数据所关联的表中的字段的值。
那可能需要先得到机柜表里所有的数据,之后在循环每条数据时,用 ForeignKey
字段名进行跨表查询,这样的话就会进行多次跨表查询。
像下面这样:
qst_all_obj = models.Cabinet.objects.all()
for obj in qst_all_obj:
print(obj.idc.city)
解决方案一:
qst_all_dic = models.Cabinet.objects.all().values("idc__city")
# QuerySet[{},{}]
# 通过字典的方法取值
解决方案二:
适用于数据量相对较少,并且查询的频率低的情况。
qst_all_obj = models.Cabinet.objects.all().select_related("idc")
# Django 内部会用 inner join IDC 进行主动跨表查询
# 一次查询到所有关联的数据
# 通过面向对象的方法取值,即用 obj.city 的方式取值
# 但是,只要连表查询,就会影响效率
解决方案三:
适用于数据量相对大 ,并且查询频率高的情况。
qst_all_obj = models.Cabinet.objects.all().prefetch_related("idc")
# 不做跨表查询,进行多次(两个表两次)单表查询
"""
1\. select * from cabinet;
2\. Django 内部:
# 把外键的值进行去重
idc_id = set(qst_all_obj.idc_id)
# 比如 idc_id 的值是 [2, 3]
# 通过条件查询到 IDC 机房表里的数据
select * from IDC where id in idc_id;
这样的话,所需要的结果集都会在内存中了
再次循环查询相应的值时,就不会再次进行跨表查询了,而是去内存的结果集中查找了。
"""
for obj in qst_all_obj:
print(obj.city)
ManyToManyField
示例 models
class SysUsers(models.Model):
user_type_choice = (
('1', "超级管理员"),
('2', "sudo 用户"),
('3', "普通用户"),
)
name = models.CharField("用户名", max_length=16)
user_type = models.CharField("用户类型", choices=user_type_choice, max_length=1, default='3')
class Meta:
verbose_name = '用户表'
verbose_name_plural = verbose_name
db_table = "sys_users"
def __str__(self):
return self.name
class Servers(models.Model):
hostname = models.CharField("主机名", max_length=128)
sysusers = models.ManyToManyField(
SysUsers,
verbose_name="用户",
related_name="servers")
class Meta:
verbose_name = '服务器表'
verbose_name_plural = verbose_name
db_table = "servers"
def __str__(self):
return self.hostname
参考官方文档自修 官方文档连接
注意:
查单个的时候用.values()
或者 .values_list()
不管是多对一,还是多对多,查询的目标假如是多的一方,就得用.all()
ORM 查询 API 总结
# 查询相关API:
# <1>filter(**kwargs): 它包含了与所给筛选条件相匹配的对象
# <2>all(): 查询所有结果
# <3>get(**kwargs): 返回与所给筛选条件相匹配的对象,返回结果有且只有一个,如果符合筛选条件的对象超过一个或者没有都会抛出错误。
#-----------下面的方法都是对查询的结果再进行处理:比如 objects.filter.values()--------
# <4>values(*field): 返回一个ValueQuerySet——一个特殊的QuerySet,运行后得到的并不是一系列 model的实例化对象,而是一个可迭代的字典序列
# <5>exclude(**kwargs): 它包含了与所给筛选条件不匹配的对象
# <6>order_by(*field): 对查询结果排序
# <7>reverse(): 对查询结果反向排序
# <8>distinct(): 从返回结果中剔除重复纪录
# <9>values_list(*field): 它与values()非常相似,它返回的是一个元组序列,values返回的是一个字典序列
# <10>count(): 返回数据库中匹配查询(QuerySet)的对象数量。
# <11>first(): 返回第一条记录
# <12>last(): 返回最后一条记录
# <13>exists(): 如果QuerySet包含数据,就返回True,否则返回False
关于 QuerySet
models.Foo.objects.all()或者.filter()等都只是返回了一个QuerySet(查询结果集对象),它并不会马上执行sql,而是当调用QuerySet的时候才执行。
- 可以进行切片操作
- 是可迭代对象
- 有缓存机制
简单的使用if语句进行判断也会完全执行整个queryset并且把数据放入cache,虽然你并不需要这些
数据!为了避免这个,可以用exists()方法来检查是否有数据:
if qst.exists():
print("QuerSet 中有数据")
总结:
queryset的cache是用于减少程序对数据库的查询,在通常的使用下, 会保证只有在需要的时候才会查询数据库。
使用 exists()
和 iterator()
方法可以优化程序对内存的使用。不过,由于它们并不会生成 queryset cache,可能会造成额外的数据库查询。
ORM 扩展知识
事务
import traceback
from django.db.transaction import atomic
def func(request):
try:
with atomic():
models.users.objects.create(**dict)
except Exception as e:
print(traceback.format_exc())
Q 查询
# Q
Q(id__gt=10)
Q(id=8) | Q(id__gt=10)
Q(Q(id=8) | Q(id__gt=10)) & Q(username='root')
# 执行原生SQL
from django.db import connection, connections
cursor = connection.cursor()
# cursor = connections['default'].cursor()
cursor.execute("""SELECT * from db_usersprofile where id = %s""", [1])
result_row = cursor.fetchone()
F
分组查询
>>> str(models.SalaryOfDay.objects.filter(date__month='07').values('name_id').annotate(total=Sum('wages')).values('name_id', 'total').query)
'SELECT "salary_of_day"."name_id", CAST(SUM("salary_of_day"."wages") AS NUMERIC) AS "total" FROM "salary_of_day" WHERE django_date_extract(\'month\', "salary_of_day"."date") = 7 GROUP BY "salary_of_day"."name_id"'
注意上面的
values('name_id)
需要放在前面。
假如不放在前面的结果会是如下的样子:
str(models.SalaryOfDay.objects.filter(date__month='07').annotate(total=Sum('wages')).values('name_id', 'total').query)
'SELECT "salary_of_day"."name_id", CAST(SUM("salary_of_day"."wages") AS NUMERIC) AS "total" FROM "salary_of_day" WHERE django_date_extract(\'month\', "salary_of_day"."date") = 7 GROUP BY "salary_of_day"."id", "salary_of_day"."name_id", "salary_of_day"."date", "salary_of_day"."wages", "salary_of_day"."other"'
需要注意 SQL 语句最后的
group by
语句
批量插入数据
批量插入数据时,只需先生成个一要传入的Product数据的列表,然后调用bulk_create方法一次性将列表中的数据插入数据库。
product_list_to_insert = list()
for x in range(10):
product_list_to_insert.append(Product(name='product name ' + str(x), price=x))
Product.objects.bulk_create(product_list_to_insert)
给字段起别名
from django.db.models import F
MyModel.objects.annotate(renamed_value=F('cryptic_value_name')).values('renamed_value')